feat: 高考志愿与定价分销链路更新(管理端/API/小程序)

- 新增高考志愿核心服务、模型、控制器与迁移脚本

- 同步管理端定价/分销/设置与小程序入口、历史、测试选择和支付逻辑

- 补充相关开发文档

Made-with: Cursor
This commit is contained in:
Ghost
2026-04-25 16:19:08 +08:00
parent 513093a5e0
commit c4ac0f8a08
56 changed files with 6100 additions and 68 deletions

View File

@@ -0,0 +1,87 @@
const { requestPromise } = require('./request')
function gaokaoPricingQueryParts(extraParams) {
const app = getApp()
const gd = (app && app.globalData) || {}
const scope =
(extraParams && extraParams.pricingScope) ||
(gd.appScope === 'enterprise' ? 'enterprise' : 'personal')
const parts = [`pricingScope=${encodeURIComponent(scope)}`]
try {
const { getEnterpriseIdForApiPayload } = require('./enterpriseContext.js')
const eid = getEnterpriseIdForApiPayload()
if (eid != null && Number(eid) > 0) {
parts.push(`enterpriseId=${encodeURIComponent(String(eid))}`)
}
} catch (e) {}
return parts
}
function getTaskStatus(params = {}) {
const query = []
if (params.referrerId) query.push(`referrerId=${encodeURIComponent(params.referrerId)}`)
if (params.channelCode) query.push(`channelCode=${encodeURIComponent(params.channelCode)}`)
if (params.scene) query.push(`scene=${encodeURIComponent(params.scene)}`)
gaokaoPricingQueryParts(params).forEach((p) => query.push(p))
const qs = query.length ? `?${query.join('&')}` : ''
return requestPromise({
url: `/api/gaokao/task-status${qs}`,
method: 'GET'
}).then((res) => (res.data && res.data.data) || {})
}
function getForm() {
return requestPromise({
url: '/api/gaokao/form',
method: 'GET'
}).then((res) => (res.data && res.data.data) || {})
}
function saveForm(data) {
return requestPromise({
url: '/api/gaokao/form',
method: 'POST',
data
}).then((res) => (res.data && res.data.data) || {})
}
function analyze(extra = {}) {
const app = getApp()
const gd = (app && app.globalData) || {}
const pricingScope =
(extra && extra.pricingScope) || (gd.appScope === 'enterprise' ? 'enterprise' : 'personal')
let enterpriseId = 0
try {
const { getEnterpriseIdForApiPayload } = require('./enterpriseContext.js')
const eid = getEnterpriseIdForApiPayload()
if (eid != null && Number(eid) > 0) enterpriseId = Number(eid)
} catch (e) {}
return requestPromise({
url: '/api/gaokao/analyze',
method: 'POST',
data: Object.assign({}, extra, { pricingScope, enterpriseId }),
timeout: 120000
}).then((res) => {
const body = res.data || {}
if (body.code !== 200) {
throw new Error(body.message || '分析失败')
}
return body.data || {}
})
}
function latestReport() {
return requestPromise({
url: '/api/gaokao/report/my-latest',
method: 'GET'
}).then((res) => (res.data && res.data.data) || {})
}
module.exports = {
getTaskStatus,
getForm,
saveForm,
analyze,
latestReport
}

View File

@@ -0,0 +1,63 @@
/**
* 高考报告页「两步解锁」:看全文 → 分享朋友圈(不与 MBTI 的 mbti_journey_unlocks 混用)
* storage: gaokao_journey_<testResultId> = { sharedMoment: ts }
*/
function storageKey(testResultId) {
const id =
testResultId != null && String(testResultId) !== '' && String(testResultId) !== '0'
? String(testResultId)
: '0'
return 'gaokao_journey_' + id
}
function read(testResultId) {
try {
const v = wx.getStorageSync(storageKey(testResultId))
if (v && typeof v === 'object') return v
} catch (e) {}
return {}
}
function write(testResultId, obj) {
try {
wx.setStorageSync(storageKey(testResultId), obj || {})
} catch (e) {}
}
function isStep1Unlocked({ profileGate, payRequired, isPaid }) {
if (profileGate) return false
if (payRequired && !isPaid) return false
return true
}
function markShared(testResultId) {
const v = read(testResultId)
v.sharedMoment = Date.now()
write(testResultId, v)
}
/**
* @param {{ profileGate: boolean, payRequired: boolean, isPaid: boolean }} ctx
* @param {string|number} testResultId
* @returns {{ step1Unlocked: boolean, step2Unlocked: boolean, activeStep: number }}
*/
function computeJourney(ctx, testResultId) {
const s1 = isStep1Unlocked(ctx)
const s2 = s1 && !!read(testResultId).sharedMoment
let activeStep = 1
if (!s1) activeStep = 1
else if (!s2) activeStep = 2
else activeStep = 0
return {
step1Unlocked: s1,
step2Unlocked: s2,
activeStep
}
}
module.exports = {
computeJourney,
markShared,
isStep1Unlocked
}

View File

@@ -36,7 +36,8 @@ function generateOrderId(productType) {
single_test: 'TSGL',
recharge: 'RCG',
deep_personal: 'DPER',
deep_team: 'DTEAM'
deep_team: 'DTEAM',
gaokao: 'GAOKAO'
}
const prefix = prefixMap[productType] || (productType || 'ORD').toUpperCase().slice(0, 6)
@@ -69,7 +70,18 @@ function generateOrderId(productType) {
* @param {Function} options.fail - 失败回调
*/
function wxPay(options) {
const { orderId, amount = 0, description, productType, testResultId, deepProductId, enterpriseId, success, fail } = options
const {
orderId,
amount = 0,
description,
productType,
testResultId,
deepProductId,
enterpriseId,
pricingScope,
success,
fail
} = options
if (app.globalData && app.globalData.miniprogramAuditMode) {
const msg = '版本审核期间不可发起支付'
@@ -116,14 +128,34 @@ function wxPay(options) {
// 创建订单时将本次测试记录ID传给后端避免每次都只更新“最新一条”
testResultId: testResultId || 0,
// 深度服务使用的具体套餐ID/产品Key用于从 categories 中选择价格)
deepProductId: deepProductId || ''
deepProductId: deepProductId || '',
// 高考:与 Tab appScope 一致,后端按个人/企业档刷新 paidAmount
pricingScope:
productType === 'gaokao'
? pricingScope ||
((app.globalData && app.globalData.appScope) || 'personal')
: pricingScope || ''
},
success: (res) => {
wx.hideLoading()
if (res.statusCode === 200 && res.data.code === 200) {
const paymentData = res.data.data
// 后端 0 元单(如高考志愿定价为 0不调 wx.requestPayment直接视为成功
if (paymentData && paymentData.skipWxPay) {
wx.hideLoading()
try {
triggerOrderPaid(orderId)
} catch (e) {}
try {
require('./analytics').reportPayResult(true, { productType: productType || '', orderId, amount: 0, note: 'zero_skip_wx' })
} catch (e) {}
wx.showToast({ title: '已解锁', icon: 'success', duration: 2000 })
success && success({ skipWxPay: true, order: paymentData })
return
}
// 2. 调起微信支付
wx.requestPayment({
timeStamp: paymentData.timeStamp,
@@ -421,7 +453,7 @@ function purchaseByPricing(productType, description, extra, maybeFail) {
opts = extra || {}
}
const { testResultId, success, fail } = opts
const { testResultId, success, fail, pricingScope } = opts
const orderId = generateOrderId(productType)
wxPay({
@@ -431,6 +463,7 @@ function purchaseByPricing(productType, description, extra, maybeFail) {
productType,
testResultId,
enterpriseId: enterpriseIdForOrder(),
pricingScope,
success,
fail
})
@@ -499,6 +532,26 @@ function purchaseTeamAnalysis(success, fail) {
purchaseByPricing('team_analysis', '团队性格组合与冲突分析服务', success, fail)
}
/** 高考志愿分析报告(统一定价键 gaokao走 orders + test_results */
function purchaseGaokaoReport(extra, maybeFail) {
let opts = {}
if (typeof extra === 'function' || extra === undefined) {
opts.success = extra
opts.fail = maybeFail
} else {
opts = extra || {}
}
const { success, fail, testResultId } = opts
const pricingScope =
(app.globalData && app.globalData.appScope) === 'enterprise' ? 'enterprise' : 'personal'
purchaseByPricing('gaokao', '高考志愿分析报告', {
success,
fail,
testResultId,
pricingScope
})
}
/**
* 企业充值
* 支持两种调用:
@@ -692,6 +745,7 @@ module.exports = {
purchaseResumeAnalysis,
purchaseFullReport,
purchaseTeamAnalysis,
purchaseGaokaoReport,
recharge,
purchasePersonalDeepService,
purchaseTeamDeepService,