feat: 高考志愿与定价分销链路更新(管理端/API/小程序)
- 新增高考志愿核心服务、模型、控制器与迁移脚本 - 同步管理端定价/分销/设置与小程序入口、历史、测试选择和支付逻辑 - 补充相关开发文档 Made-with: Cursor
This commit is contained in:
87
miniprogram/utils/gaokao.js
Normal file
87
miniprogram/utils/gaokao.js
Normal 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
|
||||
}
|
||||
63
miniprogram/utils/gaokaoJourneyState.js
Normal file
63
miniprogram/utils/gaokaoJourneyState.js
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user