feat: CRM 报告与双端结果页、管理端用户/设置与默认企业配置
1、修复了用户详情与人脸/DISC 展示、手机号与资料校验、相机页与测试选择等体验问题。 2、新增了 SystemDefaultEnterprise、discDisplay、管理端设置扩展接口;CrmReport/Test/Analyze 等 API 与路由能力补强。 3、优化了 resultFormat、descriptions、payment、企业上下文;海报与 PDP/DISC 文案;超管 Commerce/Users 与 admin Settings/Users。 Made-with: Cursor
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ function getEffectiveEnterpriseId() {
|
||||
/**
|
||||
* 提交给后端的 enterpriseId(analyze、test/submit、支付创建订单、简历分析等)
|
||||
* - 个人版 Tab(appScope=personal):仅传递「本次入口携带的 eid」(enterpriseIdFromScene),
|
||||
* 不因「已绑定企业」或「超管默认企业」带参,避免个人入口却走企业定价/落库企业字段。
|
||||
* 不因「已绑定企业」或 runtime 默认企业带参,避免客户端误把个人页当成强制企业入口。
|
||||
* - 企业版(appScope=enterprise):与 getEffectiveEnterpriseId 一致。
|
||||
* - 未传 enterpriseId 时,后端 Test::submit / Analyze 会按「绑定企业 → 系统默认企业」回落并写入 test_results.enterpriseId。
|
||||
* @returns {number|null}
|
||||
*/
|
||||
function getEnterpriseIdForApiPayload() {
|
||||
|
||||
@@ -130,6 +130,7 @@ function wxPay(options) {
|
||||
// 支付成功后轮询后端订单状态 3~5 次,确保通过微信订单查询接口确认成功
|
||||
pollOrderStatus(orderId, 5, 1000, (ok, order) => {
|
||||
if (ok) {
|
||||
try { reportCrmTestPaymentAfterSuccess(productType, testResultId, enterpriseId || 0) } catch (e) {}
|
||||
try { require('./analytics').reportPayResult(true, { productType: productType || '', orderId, amount }) } catch (e) {}
|
||||
wx.showToast({
|
||||
title: '支付成功',
|
||||
@@ -266,6 +267,35 @@ function pollOrderStatus(orderId, maxAttempts = 5, intervalMs = 1000, done) {
|
||||
tick()
|
||||
}
|
||||
|
||||
/**
|
||||
* 测评类付费(人脸/MBTI/PDP/DISC)支付确认成功后上报存客宝线索;KEY 由后端按企业 cunkebao_keys 解析(对齐深度服务「完成付款」)
|
||||
*/
|
||||
function reportCrmTestPaymentAfterSuccess(productType, testResultId, contextEnterpriseId) {
|
||||
const types = { face: true, mbti: true, disc: true, pdp: true }
|
||||
if (!types[productType]) return
|
||||
const token = wx.getStorageSync('token')
|
||||
if (!token) return
|
||||
wx.request({
|
||||
url: `${paymentApiBase()}/api/crm/report`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
testType: productType,
|
||||
testResultId: testResultId || 0,
|
||||
contextEnterpriseId: contextEnterpriseId != null ? Number(contextEnterpriseId) : 0
|
||||
},
|
||||
success(res) {
|
||||
console.log('[CRM] 测评付费线索', res.data)
|
||||
},
|
||||
fail(err) {
|
||||
console.warn('[CRM] 测评付费线索请求失败', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买VIP会员
|
||||
* @param {String} vipType - 'month' | 'quarter' | 'year' | 'lifetime'
|
||||
|
||||
@@ -20,6 +20,19 @@ function isProfileComplete() {
|
||||
return nickname.length > 0 && avatar.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 与后端 Test::isWechatProfileComplete 一致:昵称、头像、手机号均必填时才解锁完整报告
|
||||
*/
|
||||
function isReportProfileComplete() {
|
||||
const app = getApp()
|
||||
const user = app.globalData.userInfo || wx.getStorageSync('userInfo')
|
||||
if (!user) return false
|
||||
const phone = (user.phone || user.phoneNumber || '').trim()
|
||||
const nickname = (user.nickname || user.nickName || user.username || '').trim()
|
||||
const avatar = (user.avatar || user.avatarUrl || '').trim()
|
||||
return nickname.length > 0 && avatar.length > 0 && phone.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 若资料未完善则跳转到个人资料页,需登录
|
||||
* @returns {boolean} true=已完善可继续,false=已跳转
|
||||
@@ -112,5 +125,6 @@ module.exports = {
|
||||
hasPhone,
|
||||
bindPhoneByCode,
|
||||
isProfileComplete,
|
||||
isReportProfileComplete,
|
||||
ensureProfileCompleteAndRedirect,
|
||||
}
|
||||
|
||||
@@ -1,268 +1,268 @@
|
||||
/**
|
||||
* 测试结果摘要格式化:类型 + 整数百分比(与管理后台一致)
|
||||
*/
|
||||
|
||||
function toIntPercent(value) {
|
||||
if (value == null) return 0
|
||||
const n = typeof value === 'number' ? value : Number(value)
|
||||
return Number.isFinite(n) ? Math.round(n) : 0
|
||||
}
|
||||
|
||||
const PDP_EN_TO_CN = {
|
||||
Tiger: '老虎型',
|
||||
Peacock: '孔雀型',
|
||||
Koala: '考拉型',
|
||||
Owl: '猫头鹰型',
|
||||
Chameleon: '变色龙型'
|
||||
}
|
||||
|
||||
function pdpOrderedFromScores(scores) {
|
||||
if (!scores || typeof scores !== 'object') return ['', '']
|
||||
const pairs = []
|
||||
for (const k of Object.keys(scores)) {
|
||||
if (!PDP_EN_TO_CN[k]) continue
|
||||
pairs.push([k, Number(scores[k]) || 0])
|
||||
}
|
||||
pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
return [pairs[0] ? pairs[0][0] : '', pairs[1] ? pairs[1][0] : '']
|
||||
}
|
||||
|
||||
function discOrderedFromScores(scores) {
|
||||
if (!scores || typeof scores !== 'object') return ['', '']
|
||||
const allow = { D: 1, I: 1, S: 1, C: 1 }
|
||||
const pairs = []
|
||||
for (const k of Object.keys(scores)) {
|
||||
const u = String(k).trim().toUpperCase().charAt(0)
|
||||
if (!allow[u]) continue
|
||||
pairs.push([u, Number(scores[k]) || 0])
|
||||
}
|
||||
pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
return [pairs[0] ? pairs[0][0] : '', pairs[1] ? pairs[1][0] : '']
|
||||
}
|
||||
|
||||
/** 与 scores 相同维度;DISC 接口常以 percentages 存四维而无 scores */
|
||||
function discOrderedFromPercentages(pct) {
|
||||
return discOrderedFromScores(pct)
|
||||
}
|
||||
|
||||
/** DISC:「S+I型」,仅最后一项带「型」 */
|
||||
function discPrimaryLetter(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const dType = data.description && data.description.type
|
||||
if (typeof dType === 'string' && dType) {
|
||||
const noXing = String(dType).trim().replace(/型$/, '')
|
||||
if (noXing.length === 1) {
|
||||
const u = noXing.toUpperCase()
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
}
|
||||
if (data.dominantType) {
|
||||
const u = String(data.dominantType).trim().toUpperCase().charAt(0)
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
if (data.disc) {
|
||||
const noXing = String(data.disc).trim().replace(/型$/, '')
|
||||
if (noXing.length === 1) {
|
||||
const u = noXing.toUpperCase()
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function discResolveTwoLetters(data) {
|
||||
let f = discPrimaryLetter(data)
|
||||
let s = ''
|
||||
if (data.secondaryType) {
|
||||
const u = String(data.secondaryType).trim().toUpperCase().charAt(0)
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) s = u
|
||||
}
|
||||
let a0 = ''
|
||||
let b = ''
|
||||
if (data.scores && typeof data.scores === 'object') {
|
||||
const ord = discOrderedFromScores(data.scores)
|
||||
a0 = ord[0] || ''
|
||||
b = ord[1] || ''
|
||||
}
|
||||
if (!a0 && !b && data.percentages && typeof data.percentages === 'object') {
|
||||
const ord = discOrderedFromPercentages(data.percentages)
|
||||
a0 = ord[0] || ''
|
||||
b = ord[1] || ''
|
||||
}
|
||||
if (!f && a0) f = a0
|
||||
if (!s || s === f) {
|
||||
if (b && b !== f) s = b
|
||||
else s = ''
|
||||
}
|
||||
return [f, s]
|
||||
}
|
||||
|
||||
/** 旧接口/库存:「S型 + I型」「S型+I型」→ S+I型 */
|
||||
function discNormalizeLegacyDualType(desc) {
|
||||
if (typeof desc !== 'string' || !desc) return ''
|
||||
const t = desc.replace(/\s+/g, '').replace(/\uFF0B/g, '+')
|
||||
let m = t.match(/^([DISC])型\+([DISC])型$/i)
|
||||
if (m) return m[1].toUpperCase() + '+' + m[2].toUpperCase() + '型'
|
||||
m = t.match(/^([DISC])型$/i)
|
||||
if (m) return m[1].toUpperCase() + '型'
|
||||
return ''
|
||||
}
|
||||
|
||||
function discTopTwoLabel(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const [fL, sL] = discResolveTwoLetters(data)
|
||||
if (fL || sL) {
|
||||
if (!fL) return sL ? sL + '型' : ''
|
||||
if (!sL || sL === fL) return fL + '型'
|
||||
return fL + '+' + sL + '型'
|
||||
}
|
||||
return discNormalizeLegacyDualType(data.description && data.description.type)
|
||||
}
|
||||
|
||||
function pdpPrimaryFull(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const desc = data.description && data.description.type
|
||||
if (typeof desc === 'string' && desc) return desc.trim()
|
||||
if (data.dominantType) {
|
||||
const key = String(data.dominantType).trim()
|
||||
return PDP_EN_TO_CN[key] || key
|
||||
}
|
||||
if (data.pdp) return String(data.pdp).trim()
|
||||
return ''
|
||||
}
|
||||
|
||||
function pdpResolveTwoFull(data) {
|
||||
let f = pdpPrimaryFull(data)
|
||||
let s = ''
|
||||
if (data.secondaryType) {
|
||||
const k = String(data.secondaryType).trim()
|
||||
s = PDP_EN_TO_CN[k] || k
|
||||
}
|
||||
let aEn = ''
|
||||
let bEn = ''
|
||||
if (data.scores && typeof data.scores === 'object') {
|
||||
const ord = pdpOrderedFromScores(data.scores)
|
||||
aEn = ord[0] || ''
|
||||
bEn = ord[1] || ''
|
||||
}
|
||||
if (!aEn && !bEn && data.percentages && typeof data.percentages === 'object') {
|
||||
const ord = pdpOrderedFromScores(data.percentages)
|
||||
aEn = ord[0] || ''
|
||||
bEn = ord[1] || ''
|
||||
}
|
||||
if (!f && aEn) f = PDP_EN_TO_CN[aEn] || aEn
|
||||
if (!s || s === f) {
|
||||
if (bEn) {
|
||||
const cand = PDP_EN_TO_CN[bEn] || bEn
|
||||
if (cand !== f) s = cand
|
||||
}
|
||||
}
|
||||
return [f, s]
|
||||
}
|
||||
|
||||
/** PDP:「孔雀+老虎型」,仅最后一项带「型」 */
|
||||
function pdpTopTwoLabel(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const [ff, sf] = pdpResolveTwoFull(data)
|
||||
if (!ff) return sf || ''
|
||||
if (!sf || sf === ff) return ff
|
||||
const short = String(ff).replace(/型$/, '')
|
||||
return short + '+' + sf
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据测试类型和原始结果,生成带整数百分比的摘要文案
|
||||
* @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 two = discTopTwoLabel(data)
|
||||
const desc = data.description && data.description.type
|
||||
const label = two || ((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 two = pdpTopTwoLabel(data)
|
||||
const desc = data.description && data.description.type
|
||||
const label = two || ((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 two = discTopTwoLabel(data)
|
||||
if (two) return two
|
||||
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 two = pdpTopTwoLabel(data)
|
||||
if (two) return two
|
||||
const desc = data.description?.type
|
||||
if (typeof desc === 'string' && desc) return desc
|
||||
if (data.dominantType) {
|
||||
const k = String(data.dominantType).trim()
|
||||
return PDP_EN_TO_CN[k] || k
|
||||
}
|
||||
return String(data.pdp ?? '')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
toIntPercent,
|
||||
formatTestSummary,
|
||||
getTypeOnly,
|
||||
discTopTwoLabel,
|
||||
pdpTopTwoLabel
|
||||
}
|
||||
/**
|
||||
* 测试结果摘要格式化:类型 + 整数百分比(与管理后台一致)
|
||||
*/
|
||||
|
||||
function toIntPercent(value) {
|
||||
if (value == null) return 0
|
||||
const n = typeof value === 'number' ? value : Number(value)
|
||||
return Number.isFinite(n) ? Math.round(n) : 0
|
||||
}
|
||||
|
||||
const PDP_EN_TO_CN = {
|
||||
Tiger: '老虎型',
|
||||
Peacock: '孔雀型',
|
||||
Koala: '无尾熊型',
|
||||
Owl: '猫头鹰型',
|
||||
Chameleon: '变色龙型'
|
||||
}
|
||||
|
||||
function pdpOrderedFromScores(scores) {
|
||||
if (!scores || typeof scores !== 'object') return ['', '']
|
||||
const pairs = []
|
||||
for (const k of Object.keys(scores)) {
|
||||
if (!PDP_EN_TO_CN[k]) continue
|
||||
pairs.push([k, Number(scores[k]) || 0])
|
||||
}
|
||||
pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
return [pairs[0] ? pairs[0][0] : '', pairs[1] ? pairs[1][0] : '']
|
||||
}
|
||||
|
||||
function discOrderedFromScores(scores) {
|
||||
if (!scores || typeof scores !== 'object') return ['', '']
|
||||
const allow = { D: 1, I: 1, S: 1, C: 1 }
|
||||
const pairs = []
|
||||
for (const k of Object.keys(scores)) {
|
||||
const u = String(k).trim().toUpperCase().charAt(0)
|
||||
if (!allow[u]) continue
|
||||
pairs.push([u, Number(scores[k]) || 0])
|
||||
}
|
||||
pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
return [pairs[0] ? pairs[0][0] : '', pairs[1] ? pairs[1][0] : '']
|
||||
}
|
||||
|
||||
/** 与 scores 相同维度;DISC 接口常以 percentages 存四维而无 scores */
|
||||
function discOrderedFromPercentages(pct) {
|
||||
return discOrderedFromScores(pct)
|
||||
}
|
||||
|
||||
/** DISC:「S+I型」,仅最后一项带「型」 */
|
||||
function discPrimaryLetter(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const dType = data.description && data.description.type
|
||||
if (typeof dType === 'string' && dType) {
|
||||
const noXing = String(dType).trim().replace(/型$/, '')
|
||||
if (noXing.length === 1) {
|
||||
const u = noXing.toUpperCase()
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
}
|
||||
if (data.dominantType) {
|
||||
const u = String(data.dominantType).trim().toUpperCase().charAt(0)
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
if (data.disc) {
|
||||
const noXing = String(data.disc).trim().replace(/型$/, '')
|
||||
if (noXing.length === 1) {
|
||||
const u = noXing.toUpperCase()
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function discResolveTwoLetters(data) {
|
||||
let f = discPrimaryLetter(data)
|
||||
let s = ''
|
||||
if (data.secondaryType) {
|
||||
const u = String(data.secondaryType).trim().toUpperCase().charAt(0)
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) s = u
|
||||
}
|
||||
let a0 = ''
|
||||
let b = ''
|
||||
if (data.scores && typeof data.scores === 'object') {
|
||||
const ord = discOrderedFromScores(data.scores)
|
||||
a0 = ord[0] || ''
|
||||
b = ord[1] || ''
|
||||
}
|
||||
if (!a0 && !b && data.percentages && typeof data.percentages === 'object') {
|
||||
const ord = discOrderedFromPercentages(data.percentages)
|
||||
a0 = ord[0] || ''
|
||||
b = ord[1] || ''
|
||||
}
|
||||
if (!f && a0) f = a0
|
||||
if (!s || s === f) {
|
||||
if (b && b !== f) s = b
|
||||
else s = ''
|
||||
}
|
||||
return [f, s]
|
||||
}
|
||||
|
||||
/** 旧接口/库存:「S型 + I型」「S型+I型」→ S+I型 */
|
||||
function discNormalizeLegacyDualType(desc) {
|
||||
if (typeof desc !== 'string' || !desc) return ''
|
||||
const t = desc.replace(/\s+/g, '').replace(/\uFF0B/g, '+')
|
||||
let m = t.match(/^([DISC])型\+([DISC])型$/i)
|
||||
if (m) return m[1].toUpperCase() + '+' + m[2].toUpperCase() + '型'
|
||||
m = t.match(/^([DISC])型$/i)
|
||||
if (m) return m[1].toUpperCase() + '型'
|
||||
return ''
|
||||
}
|
||||
|
||||
function discTopTwoLabel(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const [fL, sL] = discResolveTwoLetters(data)
|
||||
if (fL || sL) {
|
||||
if (!fL) return sL ? sL + '型' : ''
|
||||
if (!sL || sL === fL) return fL + '型'
|
||||
return fL + '+' + sL + '型'
|
||||
}
|
||||
return discNormalizeLegacyDualType(data.description && data.description.type)
|
||||
}
|
||||
|
||||
function pdpPrimaryFull(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const desc = data.description && data.description.type
|
||||
if (typeof desc === 'string' && desc) return desc.trim()
|
||||
if (data.dominantType) {
|
||||
const key = String(data.dominantType).trim()
|
||||
return PDP_EN_TO_CN[key] || key
|
||||
}
|
||||
if (data.pdp) return String(data.pdp).trim()
|
||||
return ''
|
||||
}
|
||||
|
||||
function pdpResolveTwoFull(data) {
|
||||
let f = pdpPrimaryFull(data)
|
||||
let s = ''
|
||||
if (data.secondaryType) {
|
||||
const k = String(data.secondaryType).trim()
|
||||
s = PDP_EN_TO_CN[k] || k
|
||||
}
|
||||
let aEn = ''
|
||||
let bEn = ''
|
||||
if (data.scores && typeof data.scores === 'object') {
|
||||
const ord = pdpOrderedFromScores(data.scores)
|
||||
aEn = ord[0] || ''
|
||||
bEn = ord[1] || ''
|
||||
}
|
||||
if (!aEn && !bEn && data.percentages && typeof data.percentages === 'object') {
|
||||
const ord = pdpOrderedFromScores(data.percentages)
|
||||
aEn = ord[0] || ''
|
||||
bEn = ord[1] || ''
|
||||
}
|
||||
if (!f && aEn) f = PDP_EN_TO_CN[aEn] || aEn
|
||||
if (!s || s === f) {
|
||||
if (bEn) {
|
||||
const cand = PDP_EN_TO_CN[bEn] || bEn
|
||||
if (cand !== f) s = cand
|
||||
}
|
||||
}
|
||||
return [f, s]
|
||||
}
|
||||
|
||||
/** PDP:「孔雀+老虎型」,仅最后一项带「型」 */
|
||||
function pdpTopTwoLabel(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const [ff, sf] = pdpResolveTwoFull(data)
|
||||
if (!ff) return sf || ''
|
||||
if (!sf || sf === ff) return ff
|
||||
const short = String(ff).replace(/型$/, '')
|
||||
return short + '+' + sf
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据测试类型和原始结果,生成带整数百分比的摘要文案
|
||||
* @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 two = discTopTwoLabel(data)
|
||||
const desc = data.description && data.description.type
|
||||
const label = two || ((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 two = pdpTopTwoLabel(data)
|
||||
const desc = data.description && data.description.type
|
||||
const label = two || ((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 two = discTopTwoLabel(data)
|
||||
if (two) return two
|
||||
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 two = pdpTopTwoLabel(data)
|
||||
if (two) return two
|
||||
const desc = data.description?.type
|
||||
if (typeof desc === 'string' && desc) return desc
|
||||
if (data.dominantType) {
|
||||
const k = String(data.dominantType).trim()
|
||||
return PDP_EN_TO_CN[k] || k
|
||||
}
|
||||
return String(data.pdp ?? '')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
toIntPercent,
|
||||
formatTestSummary,
|
||||
getTypeOnly,
|
||||
discTopTwoLabel,
|
||||
pdpTopTwoLabel
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user