355 lines
12 KiB
TypeScript
355 lines
12 KiB
TypeScript
import {
|
||
SubscriptionMessageScene,
|
||
} from '@mp-pilates/shared'
|
||
import type {
|
||
ReportSubscriptionMessageRequestDto,
|
||
SubscriptionMessageRequestItem,
|
||
SubscriptionMessageTemplate,
|
||
SubscriptionMessageTemplateConfig,
|
||
SubscriptionQuotaItem,
|
||
SubscriptionQuotasResponse,
|
||
UserProfileResponse,
|
||
} from '@mp-pilates/shared'
|
||
import { get, post } from './request'
|
||
|
||
type TemplateResult = SubscriptionMessageRequestItem['result'] | 'tmplIds empty' | 'err' | 'undefined'
|
||
|
||
type RequestSubscribeMessageSuccess = Record<string, TemplateResult | undefined> & {
|
||
errMsg?: string
|
||
}
|
||
|
||
interface RequestSubscribeMessageFail {
|
||
errMsg?: string
|
||
errCode?: number | null
|
||
}
|
||
|
||
function stringifyDebugPayload(payload: unknown): string {
|
||
try {
|
||
return JSON.stringify(payload)
|
||
} catch {
|
||
return String(payload)
|
||
}
|
||
}
|
||
|
||
function getSubscribeDebugContext() {
|
||
try {
|
||
const windowInfo = typeof uni.getWindowInfo === 'function' ? uni.getWindowInfo() : null
|
||
const appBaseInfo = typeof uni.getAppBaseInfo === 'function' ? uni.getAppBaseInfo() : null
|
||
const deviceInfo = typeof uni.getDeviceInfo === 'function' ? uni.getDeviceInfo() : null
|
||
const host = appBaseInfo?.host as { env?: string } | string | undefined
|
||
|
||
return {
|
||
platform: deviceInfo?.platform ?? 'unknown',
|
||
hostEnv: typeof host === 'object' && host ? host.env : undefined,
|
||
app: appBaseInfo?.appName,
|
||
system: deviceInfo?.system,
|
||
language: appBaseInfo?.language,
|
||
version: appBaseInfo?.version,
|
||
SDKVersion: appBaseInfo?.SDKVersion,
|
||
windowWidth: windowInfo?.windowWidth,
|
||
}
|
||
} catch {
|
||
return {
|
||
platform: 'unknown',
|
||
}
|
||
}
|
||
}
|
||
|
||
function buildSubscribeError(err: RequestSubscribeMessageFail, scene: SubscriptionMessageScene, templateIds: string[]): Error {
|
||
const debugContext = getSubscribeDebugContext()
|
||
const rawMessage = (err.errMsg || '').trim()
|
||
|
||
if (!rawMessage && debugContext.platform === 'devtools') {
|
||
return new Error(
|
||
`开发者工具当前环境不支持订阅消息唤起。请退出游客模式,使用已登录的微信开发者工具并在真机中重试。调试信息: ${stringifyDebugPayload({ scene, templateIds, err, debugContext })}`,
|
||
)
|
||
}
|
||
|
||
return new Error(
|
||
`订阅消息授权失败: ${stringifyDebugPayload({ scene, templateIds, err, debugContext })}`,
|
||
)
|
||
}
|
||
|
||
const TEMPLATE_CONFIG_STORAGE_KEY = 'subscriptionMessageTemplateConfig'
|
||
|
||
let cachedConfig: SubscriptionMessageTemplateConfig | null = null
|
||
|
||
function isMpWeixin(): boolean {
|
||
// #ifdef MP-WEIXIN
|
||
return true
|
||
// #endif
|
||
return false
|
||
}
|
||
|
||
function normalizeResult(result?: TemplateResult): SubscriptionMessageRequestItem['result'] | null {
|
||
if (result === 'accept' || result === 'reject' || result === 'ban' || result === 'filter') {
|
||
return result
|
||
}
|
||
return null
|
||
}
|
||
|
||
function getTemplateConfigSync(): SubscriptionMessageTemplateConfig | null {
|
||
if (cachedConfig) {
|
||
return cachedConfig
|
||
}
|
||
|
||
const stored = uni.getStorageSync(TEMPLATE_CONFIG_STORAGE_KEY) as SubscriptionMessageTemplateConfig | ''
|
||
if (!stored || !Array.isArray(stored.templates)) {
|
||
return null
|
||
}
|
||
|
||
cachedConfig = {
|
||
templates: stored.templates.filter((item) => item.templateId),
|
||
}
|
||
return cachedConfig
|
||
}
|
||
|
||
async function fetchTemplateConfig(): Promise<SubscriptionMessageTemplateConfig> {
|
||
const config = getTemplateConfigSync()
|
||
if (!config) {
|
||
throw new Error('订阅消息模板尚未初始化,请重新进入页面后重试')
|
||
}
|
||
return config
|
||
}
|
||
|
||
function normalizeTemplateConfig(config?: Partial<SubscriptionMessageTemplateConfig> | null): SubscriptionMessageTemplateConfig {
|
||
const templates = Array.isArray(config?.templates) ? config.templates : []
|
||
|
||
return {
|
||
templates: templates.filter((item): item is SubscriptionMessageTemplate => !!item?.templateId),
|
||
}
|
||
}
|
||
|
||
export function cacheSubscriptionMessageTemplateConfig(
|
||
config?: Partial<SubscriptionMessageTemplateConfig> | null,
|
||
): SubscriptionMessageTemplateConfig {
|
||
const normalized = normalizeTemplateConfig(config)
|
||
cachedConfig = normalized
|
||
uni.setStorageSync(TEMPLATE_CONFIG_STORAGE_KEY, normalized)
|
||
return normalized
|
||
}
|
||
|
||
function getTemplatesByScene(
|
||
config: SubscriptionMessageTemplateConfig,
|
||
scene: SubscriptionMessageScene,
|
||
): SubscriptionMessageTemplate[] {
|
||
return config.templates.filter((item) => item.scene === scene && item.templateId)
|
||
}
|
||
|
||
async function reportResults(requests: SubscriptionMessageRequestItem[]): Promise<void> {
|
||
if (requests.length === 0) {
|
||
return
|
||
}
|
||
|
||
const payload: ReportSubscriptionMessageRequestDto = { requests }
|
||
await post('/user/subscription-messages/report', payload as unknown as Record<string, unknown>)
|
||
}
|
||
|
||
function normalizeSubscribeResults(
|
||
templates: SubscriptionMessageTemplate[],
|
||
result: RequestSubscribeMessageSuccess,
|
||
): SubscriptionMessageRequestItem[] {
|
||
return templates
|
||
.map<SubscriptionMessageRequestItem | null>((item) => {
|
||
const normalized = normalizeResult(result[item.templateId])
|
||
if (!normalized) {
|
||
return null
|
||
}
|
||
|
||
return {
|
||
templateId: item.templateId,
|
||
scene: item.scene,
|
||
result: normalized,
|
||
}
|
||
})
|
||
.filter((item): item is SubscriptionMessageRequestItem => item !== null)
|
||
}
|
||
|
||
export const SUBSCRIBE_BUNDLE_BOOKING: SubscriptionMessageScene[] = [
|
||
SubscriptionMessageScene.BOOKING_CREATED,
|
||
SubscriptionMessageScene.CLASS_REMINDER,
|
||
SubscriptionMessageScene.BOOKING_CANCELLED,
|
||
]
|
||
|
||
export const SUBSCRIBE_BUNDLE_CANCEL: SubscriptionMessageScene[] = [
|
||
SubscriptionMessageScene.BOOKING_CANCELLED,
|
||
SubscriptionMessageScene.CLASS_REMINDER,
|
||
]
|
||
|
||
/**
|
||
* 在当前调用栈同步调起 `uni.requestSubscribeMessage`(支持多场景组合打包,最多 3 个模板)。
|
||
* 微信要求授权框必须落在 tap / 支付 success 的同步栈里,因此这里不能先 `await`。
|
||
*/
|
||
export function requestSubscriptionBundle(scenes: SubscriptionMessageScene[]): Promise<SubscriptionMessageRequestItem[]> {
|
||
if (!isMpWeixin()) {
|
||
return Promise.resolve([])
|
||
}
|
||
|
||
const config = getTemplateConfigSync()
|
||
if (!config) {
|
||
return Promise.reject(new Error('订阅消息模板尚未初始化,请重新进入页面后重试'))
|
||
}
|
||
|
||
// 按场景收集模板,去重 templateId,微信单次最多支持 3 个模板
|
||
const templateMap = new Map<string, SubscriptionMessageTemplate>()
|
||
for (const scene of scenes) {
|
||
const list = getTemplatesByScene(config, scene)
|
||
for (const tpl of list) {
|
||
if (tpl.templateId && !templateMap.has(tpl.templateId)) {
|
||
templateMap.set(tpl.templateId, tpl)
|
||
}
|
||
if (templateMap.size >= 3) break
|
||
}
|
||
if (templateMap.size >= 3) break
|
||
}
|
||
|
||
const templates = Array.from(templateMap.values())
|
||
if (templates.length === 0) {
|
||
console.error('[subscribe] no templates matched bundle', stringifyDebugPayload({ scenes, config, debugContext: getSubscribeDebugContext() }))
|
||
return Promise.resolve([])
|
||
}
|
||
|
||
const templateIds = templates.map((item) => item.templateId)
|
||
const debugContext = getSubscribeDebugContext()
|
||
console.log('[subscribe] requestSubscriptionBundle:start', stringifyDebugPayload({ scenes, templateIds, templates, debugContext }))
|
||
|
||
return new Promise<SubscriptionMessageRequestItem[]>((resolve, reject) => {
|
||
uni.requestSubscribeMessage({
|
||
tmplIds: templateIds,
|
||
success: (res) => {
|
||
const response = res as RequestSubscribeMessageSuccess
|
||
const requests = normalizeSubscribeResults(templates, response)
|
||
console.log('[subscribe] requestSubscriptionBundle:success', stringifyDebugPayload({ scenes, response, templateIds, debugContext }))
|
||
console.log('[subscribe] requestSubscriptionBundle:normalized', stringifyDebugPayload({ scenes, result: response, requests, templateIds, debugContext }))
|
||
void reportResults(requests)
|
||
.then(() => resolve(requests))
|
||
.catch(reject)
|
||
},
|
||
fail: (err) => {
|
||
console.error('[subscribe] requestSubscriptionBundle:fail', stringifyDebugPayload({ scenes, error: err, templateIds, debugContext }))
|
||
reject(buildSubscribeError(err as RequestSubscribeMessageFail, scenes[0], templateIds))
|
||
},
|
||
})
|
||
})
|
||
}
|
||
|
||
export function requestSubscriptionMessage(scene: SubscriptionMessageScene): Promise<SubscriptionMessageRequestItem[]> {
|
||
return requestSubscriptionBundle([scene])
|
||
}
|
||
|
||
/**
|
||
* 约课三合一组合授权:包含约课成功确认、开课前1小时提醒、课程取消通知
|
||
* 一次点击,三个场景额度同时 +1!
|
||
*/
|
||
export function requestBookingBundleSubscriptionMessage(): Promise<SubscriptionMessageRequestItem[]> {
|
||
return requestSubscriptionBundle(SUBSCRIBE_BUNDLE_BOOKING)
|
||
}
|
||
|
||
/**
|
||
* 兼容原有命名,直接升级为约课三合一组合授权
|
||
*/
|
||
export function requestBookingCreatedSubscriptionMessage(): Promise<SubscriptionMessageRequestItem[]> {
|
||
return requestBookingBundleSubscriptionMessage()
|
||
}
|
||
|
||
/**
|
||
* 取消预约时的组合授权:课程取消通知 + 下次上课提醒
|
||
*/
|
||
export function requestBookingCancelSubscriptionMessage(): Promise<SubscriptionMessageRequestItem[]> {
|
||
return requestSubscriptionBundle(SUBSCRIBE_BUNDLE_CANCEL)
|
||
}
|
||
|
||
/**
|
||
* 上课前1小时提醒单项授权
|
||
*/
|
||
export function requestClassReminderSubscriptionMessage(): Promise<SubscriptionMessageRequestItem[]> {
|
||
return requestSubscriptionMessage(SubscriptionMessageScene.CLASS_REMINDER)
|
||
}
|
||
|
||
/**
|
||
* 获取当前登录用户的各订阅场景额度水位
|
||
*/
|
||
export async function fetchUserSubscriptionQuotas(): Promise<SubscriptionQuotaItem[]> {
|
||
try {
|
||
const res = await get<SubscriptionQuotasResponse>('/user/subscription-messages/quotas')
|
||
return res.quotas || []
|
||
} catch (error) {
|
||
console.warn('[subscribe] fetchUserSubscriptionQuotas failed', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 引导打开微信系统设置页,供用户恢复通知授权
|
||
*/
|
||
export function openSubscribeSettings(): Promise<boolean> {
|
||
return new Promise<boolean>((resolve) => {
|
||
uni.openSetting({
|
||
success: (res) => {
|
||
resolve(!!res.authSetting)
|
||
},
|
||
fail: () => resolve(false),
|
||
})
|
||
})
|
||
}
|
||
|
||
export async function requestAdminBookingSubscriptionCount(): Promise<UserProfileResponse | null> {
|
||
if (!isMpWeixin()) {
|
||
return null
|
||
}
|
||
|
||
const config = await fetchTemplateConfig()
|
||
const templates = getTemplatesByScene(config, SubscriptionMessageScene.ADMIN_BOOKING_CREATED)
|
||
.filter((item) => item.usageTarget === 'counter')
|
||
|
||
if (templates.length === 0) {
|
||
console.error('[subscribe] no admin counter template configured', stringifyDebugPayload({
|
||
scene: SubscriptionMessageScene.ADMIN_BOOKING_CREATED,
|
||
config,
|
||
debugContext: getSubscribeDebugContext(),
|
||
}))
|
||
return null
|
||
}
|
||
|
||
const templateId = templates[0].templateId
|
||
const debugContext = getSubscribeDebugContext()
|
||
|
||
console.log('[subscribe] requestSubscribeMessage:adminCounter:start', stringifyDebugPayload({
|
||
templateId,
|
||
debugContext,
|
||
}))
|
||
|
||
const result = await new Promise<RequestSubscribeMessageSuccess>((resolve, reject) => {
|
||
uni.requestSubscribeMessage({
|
||
tmplIds: [templateId],
|
||
success: (res) => {
|
||
console.log('[subscribe] requestSubscribeMessage:adminCounter:success', stringifyDebugPayload({ response: res, templateId, debugContext }))
|
||
resolve(res as RequestSubscribeMessageSuccess)
|
||
},
|
||
fail: (err) => {
|
||
console.error('[subscribe] requestSubscribeMessage:adminCounter:fail', stringifyDebugPayload({ error: err, templateId, debugContext }))
|
||
reject(buildSubscribeError(err as RequestSubscribeMessageFail, SubscriptionMessageScene.ADMIN_BOOKING_CREATED, [templateId]))
|
||
},
|
||
})
|
||
})
|
||
|
||
const normalized = normalizeResult(result[templateId])
|
||
console.log('[subscribe] requestSubscribeMessage:adminCounter:normalized', stringifyDebugPayload({
|
||
templateId,
|
||
normalized,
|
||
rawResult: result,
|
||
debugContext,
|
||
}))
|
||
|
||
if (normalized !== 'accept') {
|
||
return null
|
||
}
|
||
|
||
return post<UserProfileResponse>('/user/subscription-messages/admin-booking-count')
|
||
}
|
||
|
||
export function resetSubscriptionMessageTemplateCache(): void {
|
||
cachedConfig = null
|
||
uni.removeStorageSync(TEMPLATE_CONFIG_STORAGE_KEY)
|
||
}
|