主要更新: 1. 按H5网页端完全重构匹配功能(match页面) - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募 - 资源对接等类型弹出手机号/微信号输入框 - 去掉重新匹配按钮,改为返回按钮 2. 修复所有卡片对齐和宽度问题 - 目录页附录卡片居中 - 首页阅读进度卡片满宽度 - 我的页面菜单卡片对齐 - 推广中心分享卡片统一宽度 3. 修复目录页图标和文字对齐 - section-icon固定40rpx宽高 - section-title与图标垂直居中 4. 更新真实完整文章标题(62篇) - 从book目录读取真实markdown文件名 - 替换之前的简化标题 5. 新增文章数据API - /api/db/chapters - 获取完整书籍结构 - 支持按ID获取单篇文章内容
315 lines
8.1 KiB
TypeScript
315 lines
8.1 KiB
TypeScript
/**
|
||
* 分销模块WebSocket实时推送服务
|
||
* 用于推送绑定过期提醒、提现状态更新等实时消息
|
||
*/
|
||
|
||
// 消息类型定义
|
||
export type WebSocketMessageType =
|
||
| 'binding_expiring' // 绑定即将过期
|
||
| 'binding_expired' // 绑定已过期
|
||
| 'binding_converted' // 绑定已转化(用户付款)
|
||
| 'withdrawal_approved' // 提现已通过
|
||
| 'withdrawal_completed' // 提现已完成
|
||
| 'withdrawal_rejected' // 提现已拒绝
|
||
| 'earnings_added' // 收益增加
|
||
| 'system_notice'; // 系统通知
|
||
|
||
// 消息结构
|
||
export interface WebSocketMessage {
|
||
type: WebSocketMessageType;
|
||
userId: string; // 目标用户ID
|
||
data: Record<string, unknown>;
|
||
timestamp: string;
|
||
messageId: string;
|
||
}
|
||
|
||
// 绑定过期提醒数据
|
||
export interface BindingExpiringData {
|
||
bindingId: string;
|
||
visitorNickname?: string;
|
||
visitorPhone?: string;
|
||
daysRemaining: number;
|
||
expireTime: string;
|
||
}
|
||
|
||
// 提现状态更新数据
|
||
export interface WithdrawalUpdateData {
|
||
withdrawalId: string;
|
||
amount: number;
|
||
status: string;
|
||
paymentNo?: string;
|
||
rejectReason?: string;
|
||
}
|
||
|
||
// 收益增加数据
|
||
export interface EarningsAddedData {
|
||
orderId: string;
|
||
orderAmount: number;
|
||
commission: number;
|
||
visitorNickname?: string;
|
||
}
|
||
|
||
/**
|
||
* WebSocket消息队列(服务端存储待发送的消息)
|
||
* 实际项目中应该使用Redis或其他消息队列
|
||
*/
|
||
const messageQueue: Map<string, WebSocketMessage[]> = new Map();
|
||
|
||
/**
|
||
* 生成消息ID
|
||
*/
|
||
function generateMessageId(): string {
|
||
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||
}
|
||
|
||
/**
|
||
* 添加消息到队列
|
||
*/
|
||
export function pushMessage(message: Omit<WebSocketMessage, 'messageId' | 'timestamp'>): void {
|
||
const fullMessage: WebSocketMessage = {
|
||
...message,
|
||
messageId: generateMessageId(),
|
||
timestamp: new Date().toISOString(),
|
||
};
|
||
|
||
const userMessages = messageQueue.get(message.userId) || [];
|
||
userMessages.push(fullMessage);
|
||
|
||
// 保留最近100条消息
|
||
if (userMessages.length > 100) {
|
||
userMessages.shift();
|
||
}
|
||
|
||
messageQueue.set(message.userId, userMessages);
|
||
|
||
console.log('[WebSocket] 消息已入队:', {
|
||
type: fullMessage.type,
|
||
userId: fullMessage.userId,
|
||
messageId: fullMessage.messageId,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取用户待处理的消息
|
||
*/
|
||
export function getMessages(userId: string, since?: string): WebSocketMessage[] {
|
||
const userMessages = messageQueue.get(userId) || [];
|
||
|
||
if (since) {
|
||
return userMessages.filter(m => m.timestamp > since);
|
||
}
|
||
|
||
return userMessages;
|
||
}
|
||
|
||
/**
|
||
* 清除用户已读消息
|
||
*/
|
||
export function clearMessages(userId: string, messageIds: string[]): void {
|
||
const userMessages = messageQueue.get(userId) || [];
|
||
const filtered = userMessages.filter(m => !messageIds.includes(m.messageId));
|
||
messageQueue.set(userId, filtered);
|
||
}
|
||
|
||
/**
|
||
* 推送绑定即将过期提醒
|
||
*/
|
||
export function pushBindingExpiringReminder(params: {
|
||
userId: string;
|
||
bindingId: string;
|
||
visitorNickname?: string;
|
||
visitorPhone?: string;
|
||
daysRemaining: number;
|
||
expireTime: string;
|
||
}): void {
|
||
pushMessage({
|
||
type: 'binding_expiring',
|
||
userId: params.userId,
|
||
data: {
|
||
bindingId: params.bindingId,
|
||
visitorNickname: params.visitorNickname,
|
||
visitorPhone: params.visitorPhone,
|
||
daysRemaining: params.daysRemaining,
|
||
expireTime: params.expireTime,
|
||
message: `用户 ${params.visitorNickname || params.visitorPhone || '未知'} 的绑定将在 ${params.daysRemaining} 天后过期`,
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 推送绑定已过期通知
|
||
*/
|
||
export function pushBindingExpiredNotice(params: {
|
||
userId: string;
|
||
bindingId: string;
|
||
visitorNickname?: string;
|
||
visitorPhone?: string;
|
||
}): void {
|
||
pushMessage({
|
||
type: 'binding_expired',
|
||
userId: params.userId,
|
||
data: {
|
||
bindingId: params.bindingId,
|
||
visitorNickname: params.visitorNickname,
|
||
visitorPhone: params.visitorPhone,
|
||
message: `用户 ${params.visitorNickname || params.visitorPhone || '未知'} 的绑定已过期`,
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 推送绑定转化通知(用户付款)
|
||
*/
|
||
export function pushBindingConvertedNotice(params: {
|
||
userId: string;
|
||
bindingId: string;
|
||
orderId: string;
|
||
orderAmount: number;
|
||
commission: number;
|
||
visitorNickname?: string;
|
||
}): void {
|
||
pushMessage({
|
||
type: 'binding_converted',
|
||
userId: params.userId,
|
||
data: {
|
||
bindingId: params.bindingId,
|
||
orderId: params.orderId,
|
||
orderAmount: params.orderAmount,
|
||
commission: params.commission,
|
||
visitorNickname: params.visitorNickname,
|
||
message: `恭喜!用户 ${params.visitorNickname || '未知'} 已付款 ¥${params.orderAmount},您获得佣金 ¥${params.commission.toFixed(2)}`,
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 推送提现状态更新
|
||
*/
|
||
export function pushWithdrawalUpdate(params: {
|
||
userId: string;
|
||
withdrawalId: string;
|
||
amount: number;
|
||
status: 'approved' | 'completed' | 'rejected';
|
||
paymentNo?: string;
|
||
rejectReason?: string;
|
||
}): void {
|
||
const type: WebSocketMessageType =
|
||
params.status === 'approved' ? 'withdrawal_approved' :
|
||
params.status === 'completed' ? 'withdrawal_completed' : 'withdrawal_rejected';
|
||
|
||
const messages: Record<string, string> = {
|
||
approved: `您的提现申请 ¥${params.amount.toFixed(2)} 已通过审核,正在打款中...`,
|
||
completed: `您的提现 ¥${params.amount.toFixed(2)} 已成功到账,流水号: ${params.paymentNo}`,
|
||
rejected: `您的提现申请 ¥${params.amount.toFixed(2)} 已被拒绝,原因: ${params.rejectReason || '未说明'}`,
|
||
};
|
||
|
||
pushMessage({
|
||
type,
|
||
userId: params.userId,
|
||
data: {
|
||
withdrawalId: params.withdrawalId,
|
||
amount: params.amount,
|
||
status: params.status,
|
||
paymentNo: params.paymentNo,
|
||
rejectReason: params.rejectReason,
|
||
message: messages[params.status],
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 推送收益增加通知
|
||
*/
|
||
export function pushEarningsAdded(params: {
|
||
userId: string;
|
||
orderId: string;
|
||
orderAmount: number;
|
||
commission: number;
|
||
visitorNickname?: string;
|
||
}): void {
|
||
pushMessage({
|
||
type: 'earnings_added',
|
||
userId: params.userId,
|
||
data: {
|
||
orderId: params.orderId,
|
||
orderAmount: params.orderAmount,
|
||
commission: params.commission,
|
||
visitorNickname: params.visitorNickname,
|
||
message: `收益 +¥${params.commission.toFixed(2)}`,
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 推送系统通知
|
||
*/
|
||
export function pushSystemNotice(params: {
|
||
userId: string;
|
||
title: string;
|
||
content: string;
|
||
link?: string;
|
||
}): void {
|
||
pushMessage({
|
||
type: 'system_notice',
|
||
userId: params.userId,
|
||
data: {
|
||
title: params.title,
|
||
content: params.content,
|
||
link: params.link,
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 客户端WebSocket Hook(用于React组件)
|
||
* 使用轮询模式获取实时消息
|
||
*/
|
||
export function createWebSocketClient(userId: string, onMessage: (message: WebSocketMessage) => void) {
|
||
let lastTimestamp = new Date().toISOString();
|
||
let isRunning = false;
|
||
let intervalId: NodeJS.Timeout | null = null;
|
||
|
||
const fetchMessages = async () => {
|
||
if (!isRunning) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/distribution/messages?userId=${userId}&since=${encodeURIComponent(lastTimestamp)}`);
|
||
if (!response.ok) return;
|
||
|
||
const data = await response.json();
|
||
if (data.success && data.messages?.length > 0) {
|
||
for (const message of data.messages) {
|
||
onMessage(message);
|
||
if (message.timestamp > lastTimestamp) {
|
||
lastTimestamp = message.timestamp;
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('[WebSocketClient] 获取消息失败:', error);
|
||
}
|
||
};
|
||
|
||
return {
|
||
connect: () => {
|
||
isRunning = true;
|
||
// 每3秒轮询一次
|
||
intervalId = setInterval(fetchMessages, 3000);
|
||
// 立即获取一次
|
||
fetchMessages();
|
||
console.log('[WebSocketClient] 已连接,用户:', userId);
|
||
},
|
||
|
||
disconnect: () => {
|
||
isRunning = false;
|
||
if (intervalId) {
|
||
clearInterval(intervalId);
|
||
intervalId = null;
|
||
}
|
||
console.log('[WebSocketClient] 已断开连接');
|
||
},
|
||
|
||
isConnected: () => isRunning,
|
||
};
|
||
}
|