feat: 企业版合作模式(后端、管理端、小程序)

- API:合作模式配置、用户选择、企业版入口与迁移 SQL

- 管理端:合作方式管理、企业/用户侧设置与 CSV 导出等

- 小程序:合作模式弹窗与结果/简历等页对接;开发脚本小调整

Made-with: Cursor
This commit is contained in:
Ghost
2026-04-23 16:08:13 +08:00
parent d7137f9349
commit 830e3611d7
40 changed files with 2518 additions and 33 deletions

View File

@@ -58,8 +58,8 @@ App({
defaultEnterpriseId: null,
// API 基础地址:默认走线上;本机/内网调试可在开发者工具执行
// wx.setStorageSync('apiBaseOverride', 'https://你的调试域名') 后重启小程序
apiBase: 'https://mbtiapi.quwanzhi.com',
//apiBase: 'http://mbti.com',
//apiBase: 'https://mbtiapi.quwanzhi.com',
apiBase: 'http://mbti.com',
// VIP信息
vipInfo: null,
// 测试次数

View File

@@ -0,0 +1,77 @@
const { request } = require('../../utils/request.js')
Component({
properties: {
visible: {
type: Boolean,
value: false
},
modes: {
type: Array,
value: []
}
},
data: {
selected: '',
submitting: false
},
observers: {
visible(v) {
if (v) {
const modes = this.properties.modes || []
const first = modes.length ? modes[0].code : ''
this.setData({ selected: first || '', submitting: false })
}
},
modes(modes) {
if (this.properties.visible && modes && modes.length) {
const cur = this.data.selected
const ok = modes.some((m) => m && m.code === cur)
if (!ok) this.setData({ selected: modes[0].code })
}
}
},
methods: {
noop() {},
onMask() {},
onRadioChange(e) {
const v = e.detail && e.detail.value ? String(e.detail.value) : ''
if (v) this.setData({ selected: v })
},
onConfirm() {
const code = (this.data.selected || '').trim()
if (!code) {
wx.showToast({ title: '请选择一项', icon: 'none' })
return
}
if (this.data.submitting) return
this.setData({ submitting: true })
request({
url: '/api/user/cooperation-preference',
method: 'POST',
data: { modeCode: code },
success: (res) => {
const body = res.data || {}
if (res.statusCode === 200 && body.code === 200) {
wx.showToast({ title: body.message || '已提交', icon: 'success' })
this.triggerEvent('success', body.data || {})
} else {
wx.showToast({ title: body.message || '提交失败', icon: 'none' })
}
},
fail: () => {
wx.showToast({ title: '网络错误', icon: 'none' })
},
complete: () => {
this.setData({ submitting: false })
}
})
}
}
})

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,20 @@
<view class="cmd-mask" wx:if="{{visible}}" catchtouchmove="noop" catchtap="onMask">
<view class="cmd-panel" catchtap="noop">
<text class="cmd-title">请选择合作模式</text>
<text class="cmd-desc">三项测评已完成,请选择与您更契合的合作方式(提交后可在企业侧活动中使用)。</text>
<radio-group class="cmd-list" bindchange="onRadioChange">
<label class="cmd-item" wx:for="{{modes}}" wx:key="code">
<radio value="{{item.code}}" checked="{{selected === item.code}}" color="#ff6b8a" />
<view class="cmd-item-main">
<text class="cmd-item-title">{{item.title}}</text>
<text class="cmd-item-desc">{{item.description}}</text>
</view>
</label>
</radio-group>
<view class="cmd-actions">
<button class="cmd-btn cmd-btn--primary" hover-class="none" loading="{{submitting}}" bindtap="onConfirm">确认提交</button>
</view>
</view>
</view>

View File

@@ -0,0 +1,104 @@
.cmd-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 12500;
background: rgba(15, 23, 42, 0.48);
display: flex;
align-items: center;
justify-content: center;
padding: 48rpx;
box-sizing: border-box;
}
.cmd-panel {
width: 100%;
max-width: 620rpx;
background: #fff;
border-radius: 28rpx;
padding: 40rpx 36rpx 36rpx;
box-shadow: 0 24rpx 80rpx rgba(15, 23, 42, 0.12);
}
.cmd-title {
display: block;
font-size: 34rpx;
font-weight: 700;
color: #111827;
margin-bottom: 16rpx;
}
.cmd-desc {
display: block;
font-size: 24rpx;
line-height: 1.55;
color: #6b7280;
margin-bottom: 28rpx;
}
.cmd-list {
display: block;
}
.cmd-item {
display: flex;
align-items: flex-start;
gap: 16rpx;
padding: 20rpx 0;
border-bottom: 2rpx solid #f3f4f6;
}
.cmd-item:last-child {
border-bottom: none;
}
.cmd-item radio {
margin-top: 6rpx;
}
.cmd-item-main {
flex: 1;
min-width: 0;
}
.cmd-item-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #111827;
margin-bottom: 8rpx;
}
.cmd-item-desc {
display: block;
font-size: 24rpx;
line-height: 1.45;
color: #6b7280;
}
.cmd-actions {
margin-top: 28rpx;
}
.cmd-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
font-size: 28rpx;
font-weight: 600;
border-radius: 16rpx;
padding: 0;
margin: 0;
border: none;
}
.cmd-btn::after {
border: none;
}
.cmd-btn--primary {
background: linear-gradient(135deg, #ff6b8a 0%, #ff8e53 100%);
color: #fff;
}

View File

@@ -54,6 +54,7 @@ function mergeSceneBlocks(apiData, baseResult) {
}
}
const { getEnterpriseIdForApiPayload } = require('../../utils/enterpriseContext.js')
const enterpriseCooperation = require('../../utils/enterpriseCooperation.js')
Page({
data: {
@@ -119,7 +120,9 @@ Page({
reportTitle: '分析报告',
aiAnalysisText: '智能分析',
showInviteCodeDialog: false,
hideInviteCodeEntry: false
hideInviteCodeEntry: false,
showCooperationModal: false,
cooperationModes: []
},
onLoad(options) {
@@ -278,6 +281,7 @@ Page({
aiAnalysisText: tc.aiAnalysisText || '智能分析'
})
}
enterpriseCooperation.maybeShowCooperationModal(this)
},
// 面相/骨相 Tab 切换
@@ -598,6 +602,10 @@ Page({
inviteCodeGate.finishInviteCodeGate(this, true)
},
onCooperationSuccess() {
this.setData({ showCooperationModal: false, cooperationModes: [] })
},
/** 支付成功后:仅绑定手机号,不重复发起支付 */
onPostPayBindPhone(e) {
const { code, errMsg } = e.detail || {}

View File

@@ -5,6 +5,7 @@
"enableShareAppMessage": true,
"enableShareTimeline": true,
"usingComponents": {
"invite-code-dialog": "/components/invite-code-dialog/invite-code-dialog"
"invite-code-dialog": "/components/invite-code-dialog/invite-code-dialog",
"cooperation-mode-dialog": "/components/cooperation-mode-dialog/cooperation-mode-dialog"
}
}

View File

@@ -465,4 +465,9 @@
bind:skip="onInviteCodeSkip"
bind:success="onInviteCodeSuccess"
/>
<cooperation-mode-dialog
visible="{{showCooperationModal}}"
modes="{{cooperationModes}}"
bind:success="onCooperationSuccess"
/>
</view>

View File

@@ -1,8 +1,8 @@
{
"navigationBarTitleText": "推广中心",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextStyle": "black",
"usingComponents": {
"invite-code-dialog": "/components/invite-code-dialog/invite-code-dialog"
}
{
"navigationBarTitleText": "推广中心",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextStyle": "black",
"usingComponents": {
"invite-code-dialog": "/components/invite-code-dialog/invite-code-dialog"
}
}

View File

@@ -28,6 +28,7 @@ const {
markCamera
} = require('../../utils/resultJourneyState.js')
const resultScrollSync = require('../../utils/resultSectionScrollSync.js')
const enterpriseCooperation = require('../../utils/enterpriseCooperation.js')
Page({
data: {
@@ -77,7 +78,9 @@ Page({
activeSection: '',
showInviteCodeDialog: false,
/** 审核/提审时隐藏填写邀请码入口(与 runtime 开关同步) */
hideInviteCodeEntry: false
hideInviteCodeEntry: false,
showCooperationModal: false,
cooperationModes: []
},
onTapSectionNav(e) {
@@ -136,6 +139,7 @@ Page({
if (raw) this.applyResult(raw)
}
this._syncJourney()
enterpriseCooperation.maybeShowCooperationModal(this)
},
goCompleteProfile() {
@@ -445,6 +449,10 @@ Page({
inviteCodeGate.finishInviteCodeGate(this, true)
},
onCooperationSuccess() {
this.setData({ showCooperationModal: false, cooperationModes: [] })
},
// 付费解锁按钮:就地触发微信手机号授权,然后调用 unlockFullReport
onGetPhoneNumberForMbtiPay(e) {
const { code, errMsg } = e.detail || {}

View File

@@ -3,6 +3,7 @@
"navigationBarBackgroundColor": "#FF6B8A",
"navigationBarTextStyle": "white",
"usingComponents": {
"invite-code-dialog": "/components/invite-code-dialog/invite-code-dialog"
"invite-code-dialog": "/components/invite-code-dialog/invite-code-dialog",
"cooperation-mode-dialog": "/components/cooperation-mode-dialog/cooperation-mode-dialog"
}
}

View File

@@ -336,4 +336,9 @@
bind:skip="onInviteCodeSkip"
bind:success="onInviteCodeSuccess"
/>
<cooperation-mode-dialog
visible="{{showCooperationModal}}"
modes="{{cooperationModes}}"
bind:success="onCooperationSuccess"
/>
</view>

View File

@@ -5,6 +5,7 @@ const inviteCodeGate = require('../../utils/inviteCodeGate.js')
const { shouldHideInviteCodeEntry } = require('../../utils/miniprogramAuditGate.js')
const { getEnterpriseIdForApiPayload } = require('../../utils/enterpriseContext.js')
const { triggerTestResultCompleted } = require('../../utils/pushHook')
const enterpriseCooperation = require('../../utils/enterpriseCooperation.js')
Page({
data: {
@@ -27,7 +28,9 @@ Page({
paying: false,
profileGate: false,
showInviteCodeDialog: false,
hideInviteCodeEntry: false
hideInviteCodeEntry: false,
showCooperationModal: false,
cooperationModes: []
},
onLoad(options) {
@@ -66,6 +69,7 @@ Page({
if (id && this.data.profileGate) {
this.loadFromHistory(String(id))
}
enterpriseCooperation.maybeShowCooperationModal(this)
},
goCompleteProfile() {
@@ -291,6 +295,10 @@ Page({
inviteCodeGate.finishInviteCodeGate(this, true)
},
onCooperationSuccess() {
this.setData({ showCooperationModal: false, cooperationModes: [] })
},
goHome() {
const scope = (app.globalData && app.globalData.appScope) || 'personal'
if (scope === 'enterprise') {

View File

@@ -1,7 +1,8 @@
{
"navigationBarTitleText": "简历综合分析",
"usingComponents": {
"invite-code-dialog": "/components/invite-code-dialog/invite-code-dialog"
}
}
{
"navigationBarTitleText": "简历综合分析",
"usingComponents": {
"invite-code-dialog": "/components/invite-code-dialog/invite-code-dialog",
"cooperation-mode-dialog": "/components/cooperation-mode-dialog/cooperation-mode-dialog"
}
}

View File

@@ -107,5 +107,10 @@
bind:skip="onInviteCodeSkip"
bind:success="onInviteCodeSuccess"
/>
<cooperation-mode-dialog
visible="{{showCooperationModal}}"
modes="{{cooperationModes}}"
bind:success="onCooperationSuccess"
/>
</view>

View File

@@ -0,0 +1,105 @@
/**
* 企业版合作模式弹框:仅在 enterprise 定价 + 企业上下文 + 非审核隐藏 + 三项完成且未选择时触发
*/
const { getEffectiveEnterpriseId } = require('./enterpriseContext.js')
const { shouldHideInviteCodeEntry } = require('./miniprogramAuditGate.js')
let flowLock = false
function shouldRunCooperationFlow(app) {
const gd = app && app.globalData
if (!gd) return false
try {
if (shouldHideInviteCodeEntry(gd)) return false
} catch (e) {}
return gd.appScope === 'enterprise' || getEffectiveEnterpriseId() != null
}
/**
* @param {WechatMiniprogram.Page.TrivialInstance | null} page
*/
function maybeShowCooperationModal(page) {
const app = getApp()
if (!page || typeof page.setData !== 'function') return
if (flowLock) return
if (!app.ensureLogin) return
app.ensureLogin().then((ok) => {
if (!ok) return
flowLock = true
const unlock = () => {
flowLock = false
}
Promise.resolve(app.getRuntimeConfig ? app.getRuntimeConfig() : null)
.then((cfg) => {
if (!cfg || cfg.pricingType !== 'enterprise') {
unlock()
return
}
if (!shouldRunCooperationFlow(app)) {
unlock()
return
}
try {
const phoneAuth = require('./phoneAuth.js')
if (!phoneAuth.isReportProfileComplete()) {
wx.showToast({ title: '请先完善手机号与资料', icon: 'none' })
setTimeout(() => {
wx.navigateTo({
url: '/pages/user-profile/index?from=cooperation_gate',
fail: () => {}
})
}, 400)
unlock()
return
}
} catch (e) {
unlock()
return
}
const apiBase = (app.globalData && app.globalData.apiBase) || ''
const token = wx.getStorageSync('token') || (app.globalData && app.globalData.token) || ''
if (!apiBase || !token) {
unlock()
return
}
wx.request({
url: `${apiBase.replace(/\/$/, '')}/api/user/cooperation-status`,
method: 'GET',
header: { Authorization: `Bearer ${token}` },
complete: unlock,
success: (res) => {
const body = res.data || {}
if (res.statusCode !== 200 || body.code !== 200 || !body.data) return
const d = body.data
const eid = d.enterpriseId != null ? Number(d.enterpriseId) : 0
if (!eid) return
if (d.chosen) return
if (!d.allDone) return
const modes = Array.isArray(d.modes) ? d.modes : []
if (modes.length === 0) return
page.setData({
showCooperationModal: true,
cooperationModes: modes
})
}
})
})
.catch(unlock)
})
}
module.exports = {
shouldRunCooperationFlow,
maybeShowCooperationModal
}