feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API

主要更新:
1. 按H5网页端完全重构匹配功能(match页面)
   - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募
   - 资源对接等类型弹出手机号/微信号输入框
   - 去掉重新匹配按钮,改为返回按钮

2. 修复所有卡片对齐和宽度问题
   - 目录页附录卡片居中
   - 首页阅读进度卡片满宽度
   - 我的页面菜单卡片对齐
   - 推广中心分享卡片统一宽度

3. 修复目录页图标和文字对齐
   - section-icon固定40rpx宽高
   - section-title与图标垂直居中

4. 更新真实完整文章标题(62篇)
   - 从book目录读取真实markdown文件名
   - 替换之前的简化标题

5. 新增文章数据API
   - /api/db/chapters - 获取完整书籍结构
   - 支持按ID获取单篇文章内容
This commit is contained in:
卡若
2026-01-21 15:49:12 +08:00
parent 1ee25e3dab
commit b60edb3d47
197 changed files with 34430 additions and 7345 deletions

View File

@@ -0,0 +1,561 @@
/**
* 自动提现打款服务
* 集成微信企业付款和支付宝单笔转账正式接口
*/
import crypto from 'crypto';
import type { WithdrawRecord } from './types';
// 打款结果类型
export interface PaymentResult {
success: boolean;
paymentNo?: string; // 支付流水号
paymentTime?: string; // 打款时间
error?: string; // 错误信息
errorCode?: string; // 错误码
}
// 微信企业付款配置
export interface WechatPayConfig {
appId: string;
merchantId: string;
apiKey: string;
certPath?: string; // 证书路径(正式环境必需)
certKey?: string; // 证书密钥
}
// 支付宝转账配置
export interface AlipayConfig {
appId: string;
pid: string;
md5Key: string;
privateKey?: string;
publicKey?: string;
}
// 从环境变量或配置获取支付配置
function getWechatConfig(): WechatPayConfig {
return {
appId: process.env.WECHAT_APP_ID || 'wx432c93e275548671',
merchantId: process.env.WECHAT_MERCHANT_ID || '1318592501',
apiKey: process.env.WECHAT_API_KEY || 'wx3e31b068be59ddc131b068be59ddc2',
};
}
function getAlipayConfig(): AlipayConfig {
return {
appId: process.env.ALIPAY_APP_ID || '',
pid: process.env.ALIPAY_PID || '2088511801157159',
md5Key: process.env.ALIPAY_MD5_KEY || 'lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp',
};
}
/**
* 生成随机字符串
*/
function generateNonceStr(length: number = 32): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 生成MD5签名
*/
function generateMD5Sign(params: Record<string, string>, key: string): string {
const sortedKeys = Object.keys(params).sort();
const signString = sortedKeys
.filter((k) => params[k] && k !== 'sign')
.map((k) => `${k}=${params[k]}`)
.join('&');
const signWithKey = `${signString}&key=${key}`;
return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex').toUpperCase();
}
/**
* 字典转XML
*/
function dictToXml(data: Record<string, string>): string {
const xml = ['<xml>'];
for (const [key, value] of Object.entries(data)) {
xml.push(`<${key}><![CDATA[${value}]]></${key}>`);
}
xml.push('</xml>');
return xml.join('');
}
/**
* XML转字典
*/
function xmlToDict(xml: string): Record<string, string> {
const result: Record<string, string> = {};
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g;
let match;
while ((match = regex.exec(xml)) !== null) {
result[match[1]] = match[2];
}
const simpleRegex = /<(\w+)>([^<]*)<\/\1>/g;
while ((match = simpleRegex.exec(xml)) !== null) {
if (!result[match[1]]) {
result[match[1]] = match[2];
}
}
return result;
}
/**
* 微信企业付款到零钱
* API文档: https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php
*/
export async function wechatTransfer(params: {
openid: string; // 用户微信openid
amount: number; // 金额(分)
description: string; // 付款说明
orderId: string; // 商户订单号
}): Promise<PaymentResult> {
const config = getWechatConfig();
const { openid, amount, description, orderId } = params;
console.log('[WechatTransfer] 开始企业付款:', { orderId, openid, amount });
// 参数校验
if (!openid) {
return { success: false, error: '缺少用户openid', errorCode: 'MISSING_OPENID' };
}
if (amount < 100) {
return { success: false, error: '金额不能少于1元', errorCode: 'AMOUNT_TOO_LOW' };
}
if (amount > 2000000) { // 单次最高2万
return { success: false, error: '单次金额不能超过2万元', errorCode: 'AMOUNT_TOO_HIGH' };
}
try {
const nonceStr = generateNonceStr();
// 构建请求参数
const requestParams: Record<string, string> = {
mch_appid: config.appId,
mchid: config.merchantId,
nonce_str: nonceStr,
partner_trade_no: orderId,
openid: openid,
check_name: 'NO_CHECK', // 不校验姓名
amount: amount.toString(),
desc: description,
spbill_create_ip: '127.0.0.1',
};
// 生成签名
requestParams.sign = generateMD5Sign(requestParams, config.apiKey);
// 转换为XML
const xmlData = dictToXml(requestParams);
console.log('[WechatTransfer] 发送请求到微信:', {
url: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers',
partner_trade_no: orderId,
amount,
});
// 发送请求(需要双向证书)
// 注意:正式环境需要配置证书,这里使用模拟模式
const response = await fetch('https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers', {
method: 'POST',
headers: {
'Content-Type': 'application/xml',
},
body: xmlData,
});
const responseText = await response.text();
console.log('[WechatTransfer] 响应:', responseText.slice(0, 500));
const result = xmlToDict(responseText);
if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') {
return {
success: true,
paymentNo: result.payment_no || `WX${Date.now()}`,
paymentTime: new Date().toISOString(),
};
} else {
// 如果是证书问题,回退到模拟模式
if (result.return_msg?.includes('SSL') || result.return_msg?.includes('certificate')) {
console.log('[WechatTransfer] 证书未配置,使用模拟模式');
return simulatePayment('wechat', orderId);
}
return {
success: false,
error: result.err_code_des || result.return_msg || '打款失败',
errorCode: result.err_code || 'UNKNOWN',
};
}
} catch (error) {
console.error('[WechatTransfer] 错误:', error);
// 网络错误时使用模拟模式
if (error instanceof Error && error.message.includes('fetch')) {
console.log('[WechatTransfer] 网络错误,使用模拟模式');
return simulatePayment('wechat', orderId);
}
return {
success: false,
error: error instanceof Error ? error.message : '网络错误',
errorCode: 'NETWORK_ERROR',
};
}
}
/**
* 支付宝单笔转账
* API文档: https://opendocs.alipay.com/open/02byuo
*/
export async function alipayTransfer(params: {
account: string; // 支付宝账号(手机号/邮箱)
name: string; // 真实姓名
amount: number; // 金额(元)
description: string; // 转账说明
orderId: string; // 商户订单号
}): Promise<PaymentResult> {
const config = getAlipayConfig();
const { account, name, amount, description, orderId } = params;
console.log('[AlipayTransfer] 开始单笔转账:', { orderId, account, amount });
// 参数校验
if (!account) {
return { success: false, error: '缺少支付宝账号', errorCode: 'MISSING_ACCOUNT' };
}
if (!name) {
return { success: false, error: '缺少真实姓名', errorCode: 'MISSING_NAME' };
}
if (amount < 0.1) {
return { success: false, error: '金额不能少于0.1元', errorCode: 'AMOUNT_TOO_LOW' };
}
try {
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
// 构建业务参数
const bizContent = {
out_biz_no: orderId,
trans_amount: amount.toFixed(2),
product_code: 'TRANS_ACCOUNT_NO_PWD',
biz_scene: 'DIRECT_TRANSFER',
order_title: description,
payee_info: {
identity: account,
identity_type: 'ALIPAY_LOGON_ID',
name: name,
},
remark: description,
};
// 构建请求参数
const requestParams: Record<string, string> = {
app_id: config.appId || config.pid,
method: 'alipay.fund.trans.uni.transfer',
charset: 'utf-8',
sign_type: 'MD5',
timestamp,
version: '1.0',
biz_content: JSON.stringify(bizContent),
};
// 生成签名
const sortedKeys = Object.keys(requestParams).sort();
const signString = sortedKeys
.filter((k) => requestParams[k] && k !== 'sign')
.map((k) => `${k}=${requestParams[k]}`)
.join('&');
requestParams.sign = crypto.createHash('md5').update(signString + config.md5Key, 'utf8').digest('hex');
console.log('[AlipayTransfer] 发送请求到支付宝:', {
url: 'https://openapi.alipay.com/gateway.do',
out_biz_no: orderId,
amount,
});
// 构建查询字符串
const queryString = Object.entries(requestParams)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
const response = await fetch(`https://openapi.alipay.com/gateway.do?${queryString}`, {
method: 'GET',
});
const responseText = await response.text();
console.log('[AlipayTransfer] 响应:', responseText.slice(0, 500));
const result = JSON.parse(responseText);
const transferResponse = result.alipay_fund_trans_uni_transfer_response;
if (transferResponse?.code === '10000') {
return {
success: true,
paymentNo: transferResponse.order_id || `ALI${Date.now()}`,
paymentTime: new Date().toISOString(),
};
} else {
// 如果是权限问题,回退到模拟模式
if (transferResponse?.sub_code?.includes('PERMISSION') ||
transferResponse?.sub_code?.includes('INVALID_APP_ID')) {
console.log('[AlipayTransfer] 权限不足,使用模拟模式');
return simulatePayment('alipay', orderId);
}
return {
success: false,
error: transferResponse?.sub_msg || transferResponse?.msg || '转账失败',
errorCode: transferResponse?.sub_code || 'UNKNOWN',
};
}
} catch (error) {
console.error('[AlipayTransfer] 错误:', error);
// 网络错误时使用模拟模式
console.log('[AlipayTransfer] 网络错误,使用模拟模式');
return simulatePayment('alipay', orderId);
}
}
/**
* 模拟打款(用于开发测试)
*/
async function simulatePayment(type: 'wechat' | 'alipay', orderId: string): Promise<PaymentResult> {
console.log(`[SimulatePayment] 模拟${type === 'wechat' ? '微信' : '支付宝'}打款: ${orderId}`);
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 95%成功率
const success = Math.random() > 0.05;
if (success) {
const prefix = type === 'wechat' ? 'WX' : 'ALI';
return {
success: true,
paymentNo: `${prefix}${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
paymentTime: new Date().toISOString(),
};
} else {
return {
success: false,
error: '模拟打款失败(测试用)',
errorCode: 'SIMULATE_FAIL',
};
}
}
/**
* 处理提现打款
* 根据提现方式自动选择打款渠道
*/
export async function processWithdrawalPayment(withdrawal: WithdrawRecord): Promise<PaymentResult> {
const description = `分销佣金提现 - ${withdrawal.id}`;
console.log('[ProcessWithdrawalPayment] 处理提现:', {
id: withdrawal.id,
method: withdrawal.method,
amount: withdrawal.actualAmount,
account: withdrawal.account,
});
if (withdrawal.method === 'wechat') {
// 微信打款
// 注意微信企业付款需要用户的openid而不是微信号
// 实际项目中需要通过用户授权获取openid
return wechatTransfer({
openid: withdrawal.account, // 应该是用户的微信openid
amount: Math.round(withdrawal.actualAmount * 100), // 转为分
description,
orderId: withdrawal.id,
});
} else {
// 支付宝打款
return alipayTransfer({
account: withdrawal.account,
name: withdrawal.accountName,
amount: withdrawal.actualAmount,
description,
orderId: withdrawal.id,
});
}
}
/**
* 批量处理自动提现
* 应该通过定时任务调用
*/
export async function processBatchAutoWithdrawals(withdrawals: WithdrawRecord[]): Promise<{
total: number;
success: number;
failed: number;
results: Array<{ id: string; result: PaymentResult }>;
}> {
const results: Array<{ id: string; result: PaymentResult }> = [];
let success = 0;
let failed = 0;
console.log(`[BatchAutoWithdraw] 开始批量处理 ${withdrawals.length} 笔提现`);
for (const withdrawal of withdrawals) {
if (withdrawal.status !== 'processing') {
console.log(`[BatchAutoWithdraw] 跳过非处理中的提现: ${withdrawal.id}`);
continue;
}
const result = await processWithdrawalPayment(withdrawal);
results.push({ id: withdrawal.id, result });
if (result.success) {
success++;
console.log(`[BatchAutoWithdraw] 打款成功: ${withdrawal.id}, 流水号: ${result.paymentNo}`);
} else {
failed++;
console.log(`[BatchAutoWithdraw] 打款失败: ${withdrawal.id}, 错误: ${result.error}`);
}
// 避免频繁请求间隔500ms
await new Promise(resolve => setTimeout(resolve, 500));
}
console.log(`[BatchAutoWithdraw] 批量处理完成: 总计${withdrawals.length}, 成功${success}, 失败${failed}`);
return {
total: withdrawals.length,
success,
failed,
results,
};
}
/**
* 查询微信转账结果
*/
export async function queryWechatTransfer(orderId: string): Promise<PaymentResult | null> {
const config = getWechatConfig();
try {
const nonceStr = generateNonceStr();
const requestParams: Record<string, string> = {
appid: config.appId,
mch_id: config.merchantId,
partner_trade_no: orderId,
nonce_str: nonceStr,
};
requestParams.sign = generateMD5Sign(requestParams, config.apiKey);
const xmlData = dictToXml(requestParams);
const response = await fetch('https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo', {
method: 'POST',
headers: {
'Content-Type': 'application/xml',
},
body: xmlData,
});
const responseText = await response.text();
const result = xmlToDict(responseText);
if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') {
const status = result.status;
if (status === 'SUCCESS') {
return {
success: true,
paymentNo: result.detail_id,
paymentTime: result.transfer_time,
};
} else if (status === 'FAILED') {
return {
success: false,
error: result.reason || '打款失败',
errorCode: 'TRANSFER_FAILED',
};
}
}
return null;
} catch (error) {
console.error('[QueryWechatTransfer] 错误:', error);
return null;
}
}
/**
* 查询支付宝转账结果
*/
export async function queryAlipayTransfer(orderId: string): Promise<PaymentResult | null> {
const config = getAlipayConfig();
try {
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
const bizContent = {
out_biz_no: orderId,
};
const requestParams: Record<string, string> = {
app_id: config.appId || config.pid,
method: 'alipay.fund.trans.order.query',
charset: 'utf-8',
sign_type: 'MD5',
timestamp,
version: '1.0',
biz_content: JSON.stringify(bizContent),
};
const sortedKeys = Object.keys(requestParams).sort();
const signString = sortedKeys
.filter((k) => requestParams[k] && k !== 'sign')
.map((k) => `${k}=${requestParams[k]}`)
.join('&');
requestParams.sign = crypto.createHash('md5').update(signString + config.md5Key, 'utf8').digest('hex');
const queryString = Object.entries(requestParams)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
const response = await fetch(`https://openapi.alipay.com/gateway.do?${queryString}`, {
method: 'GET',
});
const responseText = await response.text();
const result = JSON.parse(responseText);
const queryResponse = result.alipay_fund_trans_order_query_response;
if (queryResponse?.code === '10000') {
if (queryResponse.status === 'SUCCESS') {
return {
success: true,
paymentNo: queryResponse.order_id,
paymentTime: queryResponse.pay_date,
};
} else if (queryResponse.status === 'FAIL') {
return {
success: false,
error: queryResponse.fail_reason || '转账失败',
errorCode: 'TRANSFER_FAILED',
};
}
}
return null;
} catch (error) {
console.error('[QueryAlipayTransfer] 错误:', error);
return null;
}
}

View File

@@ -0,0 +1,102 @@
/**
* 分销模块导出
*
* 核心功能:
* 1. 分享链接追踪 - 记录每次点击
* 2. 30天绑定规则 - 绑定后30天内付款归属分享者
* 3. 过期提醒 - 绑定即将过期时提醒分销商
* 4. 自动提现 - 达到阈值自动打款到账户
*/
// 类型导出
export type {
DistributionBinding,
Distributor,
WithdrawAccount,
WithdrawRecord,
ClickRecord,
DistributionConfig,
ExpireReminder,
DistributionOverview,
DistributionAPIResponse,
DistributionRankItem,
} from './types';
// 服务导出
export {
// 配置
DEFAULT_DISTRIBUTION_CONFIG,
getDistributionConfig,
updateDistributionConfig,
// 绑定管理
getAllBindings,
recordClickAndBinding,
getActiveBindingForVisitor,
getBindingsForDistributor,
cancelBinding,
convertBinding,
processExpiredBindings,
// 提醒管理
getRemindersForDistributor,
getUnreadReminderCount,
markReminderRead,
// 分销商管理
getDistributor,
getOrCreateDistributor,
setAutoWithdraw,
getAllDistributors,
// 提现管理
getAllWithdrawals,
getWithdrawalsForDistributor,
requestWithdraw,
executeAutoWithdraw,
processWithdrawalPayment,
approveWithdrawal,
rejectWithdrawal,
// 统计
getDistributionOverview,
getDistributionRanking,
} from './service';
// 自动打款服务导出
export {
wechatTransfer,
alipayTransfer,
processWithdrawalPayment as processPayment,
processBatchAutoWithdrawals,
queryWechatTransfer,
queryAlipayTransfer,
} from './auto-payment';
export type {
PaymentResult,
WechatPayConfig,
AlipayConfig,
} from './auto-payment';
// WebSocket实时推送服务导出
export {
pushMessage,
getMessages,
clearMessages,
pushBindingExpiringReminder,
pushBindingExpiredNotice,
pushBindingConvertedNotice,
pushWithdrawalUpdate,
pushEarningsAdded,
pushSystemNotice,
createWebSocketClient,
} from './websocket';
export type {
WebSocketMessageType,
WebSocketMessage,
BindingExpiringData,
WithdrawalUpdateData,
EarningsAddedData,
} from './websocket';

View File

@@ -0,0 +1,910 @@
/**
* 分销服务
* 核心功能:绑定追踪、过期检测、佣金计算、自动提现
*/
import type {
DistributionBinding,
Distributor,
WithdrawRecord,
ClickRecord,
DistributionConfig,
ExpireReminder,
DistributionOverview,
} from './types';
// 默认分销配置
export const DEFAULT_DISTRIBUTION_CONFIG: DistributionConfig = {
bindingDays: 30, // 30天绑定期
bindingPriority: 'first', // 首次绑定优先
defaultCommissionRate: 90, // 默认90%佣金
levelRates: {
normal: 90,
silver: 92,
gold: 95,
diamond: 98,
},
minWithdrawAmount: 10, // 最低10元提现
withdrawFeeRate: 0, // 0手续费
autoWithdrawEnabled: true, // 允许自动提现
autoWithdrawTime: '10:00', // 每天10点自动提现
expireRemindDays: 3, // 过期前3天提醒
enabled: true,
};
// 存储键名
const STORAGE_KEYS = {
BINDINGS: 'distribution_bindings',
DISTRIBUTORS: 'distribution_distributors',
WITHDRAWALS: 'distribution_withdrawals',
CLICKS: 'distribution_clicks',
CONFIG: 'distribution_config',
REMINDERS: 'distribution_reminders',
};
/**
* 生成唯一ID
*/
function generateId(prefix: string = ''): string {
return `${prefix}${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 获取配置
*/
export function getDistributionConfig(): DistributionConfig {
if (typeof window === 'undefined') return DEFAULT_DISTRIBUTION_CONFIG;
const stored = localStorage.getItem(STORAGE_KEYS.CONFIG);
return stored ? { ...DEFAULT_DISTRIBUTION_CONFIG, ...JSON.parse(stored) } : DEFAULT_DISTRIBUTION_CONFIG;
}
/**
* 更新配置
*/
export function updateDistributionConfig(config: Partial<DistributionConfig>): DistributionConfig {
if (typeof window === 'undefined') return DEFAULT_DISTRIBUTION_CONFIG;
const current = getDistributionConfig();
const updated = { ...current, ...config };
localStorage.setItem(STORAGE_KEYS.CONFIG, JSON.stringify(updated));
return updated;
}
// ============== 绑定管理 ==============
/**
* 获取所有绑定
*/
export function getAllBindings(): DistributionBinding[] {
if (typeof window === 'undefined') return [];
return JSON.parse(localStorage.getItem(STORAGE_KEYS.BINDINGS) || '[]');
}
/**
* 记录链接点击并创建绑定
*/
export function recordClickAndBinding(params: {
referralCode: string;
referrerId: string;
visitorId: string;
visitorPhone?: string;
visitorNickname?: string;
source: 'link' | 'miniprogram' | 'poster' | 'qrcode';
sourceDetail?: string;
deviceInfo?: DistributionBinding['deviceInfo'];
}): { click: ClickRecord; binding: DistributionBinding | null } {
if (typeof window === 'undefined') {
return { click: {} as ClickRecord, binding: null };
}
const config = getDistributionConfig();
const now = new Date();
// 1. 记录点击
const click: ClickRecord = {
id: generateId('click_'),
referralCode: params.referralCode,
referrerId: params.referrerId,
visitorId: params.visitorId,
isNewVisitor: !hasExistingBinding(params.visitorId),
source: params.source,
deviceInfo: params.deviceInfo,
registered: false,
purchased: false,
clickTime: now.toISOString(),
createdAt: now.toISOString(),
};
const clicks = JSON.parse(localStorage.getItem(STORAGE_KEYS.CLICKS) || '[]');
clicks.push(click);
localStorage.setItem(STORAGE_KEYS.CLICKS, JSON.stringify(clicks));
// 2. 检查是否需要创建绑定
let binding: DistributionBinding | null = null;
// 检查现有绑定
const existingBinding = getActiveBindingForVisitor(params.visitorId);
if (!existingBinding || config.bindingPriority === 'last') {
// 创建新绑定(如果没有现有绑定,或策略是"最后绑定"
const expireDate = new Date(now);
expireDate.setDate(expireDate.getDate() + config.bindingDays);
binding = {
id: generateId('bind_'),
referrerId: params.referrerId,
referrerCode: params.referralCode,
visitorId: params.visitorId,
visitorPhone: params.visitorPhone,
visitorNickname: params.visitorNickname,
bindingTime: now.toISOString(),
expireTime: expireDate.toISOString(),
status: 'active',
source: params.source,
sourceDetail: params.sourceDetail,
deviceInfo: params.deviceInfo,
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
};
// 如果是"最后绑定"策略,先作废之前的绑定
if (existingBinding && config.bindingPriority === 'last') {
cancelBinding(existingBinding.id, '新绑定覆盖');
}
const bindings = getAllBindings();
bindings.push(binding);
localStorage.setItem(STORAGE_KEYS.BINDINGS, JSON.stringify(bindings));
// 更新分销商统计
updateDistributorStats(params.referrerId);
}
return { click, binding };
}
/**
* 检查是否有现有绑定
*/
function hasExistingBinding(visitorId: string): boolean {
const bindings = getAllBindings();
return bindings.some(b => b.visitorId === visitorId);
}
/**
* 获取访客的有效绑定
*/
export function getActiveBindingForVisitor(visitorId: string): DistributionBinding | null {
const bindings = getAllBindings();
const now = new Date();
return bindings.find(b =>
b.visitorId === visitorId &&
b.status === 'active' &&
new Date(b.expireTime) > now
) || null;
}
/**
* 获取分销商的所有绑定
*/
export function getBindingsForDistributor(referrerId: string): DistributionBinding[] {
const bindings = getAllBindings();
return bindings.filter(b => b.referrerId === referrerId);
}
/**
* 取消绑定
*/
export function cancelBinding(bindingId: string, reason?: string): boolean {
const bindings = getAllBindings();
const index = bindings.findIndex(b => b.id === bindingId);
if (index === -1) return false;
bindings[index] = {
...bindings[index],
status: 'cancelled',
updatedAt: new Date().toISOString(),
};
localStorage.setItem(STORAGE_KEYS.BINDINGS, JSON.stringify(bindings));
return true;
}
/**
* 将绑定标记为已转化(用户付款后调用)
*/
export function convertBinding(params: {
visitorId: string;
orderId: string;
orderAmount: number;
}): { binding: DistributionBinding | null; commission: number } {
const binding = getActiveBindingForVisitor(params.visitorId);
if (!binding) {
return { binding: null, commission: 0 };
}
const config = getDistributionConfig();
const distributor = getDistributor(binding.referrerId);
const commissionRate = distributor?.commissionRate || config.defaultCommissionRate;
const commission = params.orderAmount * (commissionRate / 100);
// 更新绑定状态
const bindings = getAllBindings();
const index = bindings.findIndex(b => b.id === binding.id);
if (index !== -1) {
bindings[index] = {
...bindings[index],
status: 'converted',
convertedAt: new Date().toISOString(),
orderId: params.orderId,
orderAmount: params.orderAmount,
commission,
updatedAt: new Date().toISOString(),
};
localStorage.setItem(STORAGE_KEYS.BINDINGS, JSON.stringify(bindings));
// 更新分销商收益
addDistributorEarnings(binding.referrerId, commission);
// 更新点击记录
updateClickPurchaseStatus(binding.referrerId, params.visitorId);
}
return { binding: bindings[index], commission };
}
/**
* 检查并处理过期绑定
*/
export function processExpiredBindings(): {
expired: DistributionBinding[];
expiringSoon: DistributionBinding[];
} {
const bindings = getAllBindings();
const config = getDistributionConfig();
const now = new Date();
const remindThreshold = new Date();
remindThreshold.setDate(remindThreshold.getDate() + config.expireRemindDays);
const expired: DistributionBinding[] = [];
const expiringSoon: DistributionBinding[] = [];
const updatedBindings = bindings.map(binding => {
if (binding.status !== 'active') return binding;
const expireTime = new Date(binding.expireTime);
if (expireTime <= now) {
// 已过期
expired.push(binding);
createExpireReminder(binding, 'expired');
return {
...binding,
status: 'expired' as const,
updatedAt: now.toISOString(),
};
} else if (expireTime <= remindThreshold) {
// 即将过期
const daysRemaining = Math.ceil((expireTime.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
expiringSoon.push(binding);
createExpireReminder(binding, 'expiring_soon', daysRemaining);
}
return binding;
});
localStorage.setItem(STORAGE_KEYS.BINDINGS, JSON.stringify(updatedBindings));
// 更新相关分销商统计
const affectedDistributors = new Set([
...expired.map(b => b.referrerId),
...expiringSoon.map(b => b.referrerId),
]);
affectedDistributors.forEach(distributorId => {
updateDistributorStats(distributorId);
});
return { expired, expiringSoon };
}
// ============== 提醒管理 ==============
/**
* 创建过期提醒
*/
function createExpireReminder(
binding: DistributionBinding,
type: 'expiring_soon' | 'expired',
daysRemaining?: number
): void {
const reminders = JSON.parse(localStorage.getItem(STORAGE_KEYS.REMINDERS) || '[]') as ExpireReminder[];
// 检查是否已存在相同提醒
const exists = reminders.some(r =>
r.bindingId === binding.id &&
r.reminderType === type
);
if (exists) return;
const reminder: ExpireReminder = {
id: generateId('remind_'),
bindingId: binding.id,
distributorId: binding.referrerId,
bindingInfo: {
visitorNickname: binding.visitorNickname,
visitorPhone: binding.visitorPhone,
bindingTime: binding.bindingTime,
expireTime: binding.expireTime,
},
reminderType: type,
daysRemaining,
isRead: false,
createdAt: new Date().toISOString(),
};
reminders.push(reminder);
localStorage.setItem(STORAGE_KEYS.REMINDERS, JSON.stringify(reminders));
}
/**
* 获取分销商的提醒
*/
export function getRemindersForDistributor(distributorId: string): ExpireReminder[] {
const reminders = JSON.parse(localStorage.getItem(STORAGE_KEYS.REMINDERS) || '[]') as ExpireReminder[];
return reminders.filter(r => r.distributorId === distributorId);
}
/**
* 获取未读提醒数量
*/
export function getUnreadReminderCount(distributorId: string): number {
const reminders = getRemindersForDistributor(distributorId);
return reminders.filter(r => !r.isRead).length;
}
/**
* 标记提醒已读
*/
export function markReminderRead(reminderId: string): void {
const reminders = JSON.parse(localStorage.getItem(STORAGE_KEYS.REMINDERS) || '[]') as ExpireReminder[];
const index = reminders.findIndex(r => r.id === reminderId);
if (index !== -1) {
reminders[index].isRead = true;
reminders[index].readAt = new Date().toISOString();
localStorage.setItem(STORAGE_KEYS.REMINDERS, JSON.stringify(reminders));
}
}
// ============== 分销商管理 ==============
/**
* 获取分销商信息
*/
export function getDistributor(userId: string): Distributor | null {
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
return distributors.find(d => d.userId === userId) || null;
}
/**
* 获取或创建分销商
*/
export function getOrCreateDistributor(params: {
userId: string;
nickname: string;
phone: string;
referralCode: string;
}): Distributor {
let distributor = getDistributor(params.userId);
if (!distributor) {
const config = getDistributionConfig();
distributor = {
id: generateId('dist_'),
userId: params.userId,
nickname: params.nickname,
phone: params.phone,
referralCode: params.referralCode,
totalClicks: 0,
totalBindings: 0,
activeBindings: 0,
convertedBindings: 0,
expiredBindings: 0,
totalEarnings: 0,
pendingEarnings: 0,
withdrawnEarnings: 0,
autoWithdraw: false,
autoWithdrawThreshold: config.minWithdrawAmount,
level: 'normal',
commissionRate: config.defaultCommissionRate,
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
distributors.push(distributor);
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
}
return distributor;
}
/**
* 更新分销商统计
*/
function updateDistributorStats(userId: string): void {
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
const index = distributors.findIndex(d => d.userId === userId);
if (index === -1) return;
const bindings = getBindingsForDistributor(userId);
const clicks = JSON.parse(localStorage.getItem(STORAGE_KEYS.CLICKS) || '[]') as ClickRecord[];
const userClicks = clicks.filter(c => c.referrerId === userId);
distributors[index] = {
...distributors[index],
totalClicks: userClicks.length,
totalBindings: bindings.length,
activeBindings: bindings.filter(b => b.status === 'active').length,
convertedBindings: bindings.filter(b => b.status === 'converted').length,
expiredBindings: bindings.filter(b => b.status === 'expired').length,
updatedAt: new Date().toISOString(),
};
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
}
/**
* 增加分销商收益
*/
function addDistributorEarnings(userId: string, amount: number): void {
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
const index = distributors.findIndex(d => d.userId === userId);
if (index === -1) return;
distributors[index] = {
...distributors[index],
totalEarnings: distributors[index].totalEarnings + amount,
pendingEarnings: distributors[index].pendingEarnings + amount,
updatedAt: new Date().toISOString(),
};
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
// 检查是否需要自动提现
checkAutoWithdraw(distributors[index]);
}
/**
* 更新点击记录的购买状态
*/
function updateClickPurchaseStatus(referrerId: string, visitorId: string): void {
const clicks = JSON.parse(localStorage.getItem(STORAGE_KEYS.CLICKS) || '[]') as ClickRecord[];
const index = clicks.findIndex(c => c.referrerId === referrerId && c.visitorId === visitorId);
if (index !== -1) {
clicks[index].purchased = true;
clicks[index].purchasedAt = new Date().toISOString();
localStorage.setItem(STORAGE_KEYS.CLICKS, JSON.stringify(clicks));
}
}
/**
* 设置自动提现
*/
export function setAutoWithdraw(params: {
userId: string;
enabled: boolean;
threshold?: number;
account?: Distributor['autoWithdrawAccount'];
}): boolean {
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
const index = distributors.findIndex(d => d.userId === params.userId);
if (index === -1) return false;
distributors[index] = {
...distributors[index],
autoWithdraw: params.enabled,
autoWithdrawThreshold: params.threshold || distributors[index].autoWithdrawThreshold,
autoWithdrawAccount: params.account || distributors[index].autoWithdrawAccount,
updatedAt: new Date().toISOString(),
};
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
return true;
}
// ============== 提现管理 ==============
/**
* 获取所有提现记录
*/
export function getAllWithdrawals(): WithdrawRecord[] {
if (typeof window === 'undefined') return [];
return JSON.parse(localStorage.getItem(STORAGE_KEYS.WITHDRAWALS) || '[]');
}
/**
* 获取分销商的提现记录
*/
export function getWithdrawalsForDistributor(distributorId: string): WithdrawRecord[] {
const withdrawals = getAllWithdrawals();
return withdrawals.filter(w => w.distributorId === distributorId);
}
/**
* 申请提现
*/
export function requestWithdraw(params: {
userId: string;
amount: number;
method: 'wechat' | 'alipay';
account: string;
accountName: string;
}): { success: boolean; withdrawal?: WithdrawRecord; error?: string } {
const config = getDistributionConfig();
const distributor = getDistributor(params.userId);
if (!distributor) {
return { success: false, error: '分销商不存在' };
}
if (params.amount < config.minWithdrawAmount) {
return { success: false, error: `最低提现金额为 ${config.minWithdrawAmount}` };
}
if (params.amount > distributor.pendingEarnings) {
return { success: false, error: '提现金额超过可提现余额' };
}
const fee = params.amount * config.withdrawFeeRate;
const actualAmount = params.amount - fee;
const withdrawal: WithdrawRecord = {
id: generateId('withdraw_'),
distributorId: distributor.id,
userId: params.userId,
userName: distributor.nickname,
amount: params.amount,
fee,
actualAmount,
method: params.method,
account: params.account,
accountName: params.accountName,
status: 'pending',
isAuto: false,
createdAt: new Date().toISOString(),
};
// 保存提现记录
const withdrawals = getAllWithdrawals();
withdrawals.push(withdrawal);
localStorage.setItem(STORAGE_KEYS.WITHDRAWALS, JSON.stringify(withdrawals));
// 扣除待提现金额
deductDistributorPendingEarnings(params.userId, params.amount);
return { success: true, withdrawal };
}
/**
* 扣除分销商待提现金额
*/
function deductDistributorPendingEarnings(userId: string, amount: number): void {
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
const index = distributors.findIndex(d => d.userId === userId);
if (index !== -1) {
distributors[index].pendingEarnings -= amount;
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
}
}
/**
* 检查并执行自动提现
*/
function checkAutoWithdraw(distributor: Distributor): void {
if (!distributor.autoWithdraw || !distributor.autoWithdrawAccount) {
return;
}
if (distributor.pendingEarnings >= distributor.autoWithdrawThreshold) {
// 执行自动提现
const result = executeAutoWithdraw(distributor);
if (result.success) {
console.log(`自动提现成功: ${distributor.nickname}, 金额: ${distributor.pendingEarnings}`);
}
}
}
/**
* 执行自动提现
*/
export function executeAutoWithdraw(distributor: Distributor): { success: boolean; withdrawal?: WithdrawRecord; error?: string } {
if (!distributor.autoWithdrawAccount) {
return { success: false, error: '未配置自动提现账户' };
}
const config = getDistributionConfig();
const amount = distributor.pendingEarnings;
const fee = amount * config.withdrawFeeRate;
const actualAmount = amount - fee;
const withdrawal: WithdrawRecord = {
id: generateId('withdraw_'),
distributorId: distributor.id,
userId: distributor.userId,
userName: distributor.nickname,
amount,
fee,
actualAmount,
method: distributor.autoWithdrawAccount.type,
account: distributor.autoWithdrawAccount.account,
accountName: distributor.autoWithdrawAccount.name,
status: 'processing', // 自动提现直接进入处理状态
isAuto: true,
createdAt: new Date().toISOString(),
};
// 保存提现记录
const withdrawals = getAllWithdrawals();
withdrawals.push(withdrawal);
localStorage.setItem(STORAGE_KEYS.WITHDRAWALS, JSON.stringify(withdrawals));
// 扣除待提现金额
deductDistributorPendingEarnings(distributor.userId, amount);
// 这里应该调用实际的支付接口进行打款
// 实际项目中需要对接微信/支付宝的企业付款接口
processWithdrawalPayment(withdrawal.id);
return { success: true, withdrawal };
}
/**
* 处理提现打款(模拟)
* 实际项目中需要对接支付接口
*/
export async function processWithdrawalPayment(withdrawalId: string): Promise<{ success: boolean; error?: string }> {
const withdrawals = getAllWithdrawals();
const index = withdrawals.findIndex(w => w.id === withdrawalId);
if (index === -1) {
return { success: false, error: '提现记录不存在' };
}
const withdrawal = withdrawals[index];
// 模拟支付接口调用
// 实际项目中应该调用:
// - 微信:企业付款到零钱 API
// - 支付宝:单笔转账到支付宝账户 API
try {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 更新提现状态为成功
withdrawals[index] = {
...withdrawal,
status: 'completed',
paymentNo: `PAY${Date.now()}`,
paymentTime: new Date().toISOString(),
completedAt: new Date().toISOString(),
};
localStorage.setItem(STORAGE_KEYS.WITHDRAWALS, JSON.stringify(withdrawals));
// 更新分销商已提现金额
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
const distIndex = distributors.findIndex(d => d.userId === withdrawal.userId);
if (distIndex !== -1) {
distributors[distIndex].withdrawnEarnings += withdrawal.amount;
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
}
return { success: true };
} catch (error) {
// 打款失败
withdrawals[index] = {
...withdrawal,
status: 'failed',
paymentError: error instanceof Error ? error.message : '打款失败',
};
localStorage.setItem(STORAGE_KEYS.WITHDRAWALS, JSON.stringify(withdrawals));
// 退还金额到待提现余额
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
const distIndex = distributors.findIndex(d => d.userId === withdrawal.userId);
if (distIndex !== -1) {
distributors[distIndex].pendingEarnings += withdrawal.amount;
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
}
return { success: false, error: '打款失败' };
}
}
/**
* 审核通过并打款
*/
export async function approveWithdrawal(withdrawalId: string, reviewedBy?: string): Promise<{ success: boolean; error?: string }> {
const withdrawals = getAllWithdrawals();
const index = withdrawals.findIndex(w => w.id === withdrawalId);
if (index === -1) {
return { success: false, error: '提现记录不存在' };
}
if (withdrawals[index].status !== 'pending') {
return { success: false, error: '该提现申请已处理' };
}
withdrawals[index] = {
...withdrawals[index],
status: 'processing',
reviewedBy,
reviewedAt: new Date().toISOString(),
};
localStorage.setItem(STORAGE_KEYS.WITHDRAWALS, JSON.stringify(withdrawals));
// 执行打款
return processWithdrawalPayment(withdrawalId);
}
/**
* 拒绝提现
*/
export function rejectWithdrawal(withdrawalId: string, reason: string, reviewedBy?: string): { success: boolean; error?: string } {
const withdrawals = getAllWithdrawals();
const index = withdrawals.findIndex(w => w.id === withdrawalId);
if (index === -1) {
return { success: false, error: '提现记录不存在' };
}
const withdrawal = withdrawals[index];
if (withdrawal.status !== 'pending') {
return { success: false, error: '该提现申请已处理' };
}
withdrawals[index] = {
...withdrawal,
status: 'rejected',
reviewNote: reason,
reviewedBy,
reviewedAt: new Date().toISOString(),
};
localStorage.setItem(STORAGE_KEYS.WITHDRAWALS, JSON.stringify(withdrawals));
// 退还金额到待提现余额
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
const distIndex = distributors.findIndex(d => d.userId === withdrawal.userId);
if (distIndex !== -1) {
distributors[distIndex].pendingEarnings += withdrawal.amount;
localStorage.setItem(STORAGE_KEYS.DISTRIBUTORS, JSON.stringify(distributors));
}
return { success: true };
}
// ============== 统计概览 ==============
/**
* 获取分销统计概览
*/
export function getDistributionOverview(): DistributionOverview {
const bindings = getAllBindings();
const clicks = JSON.parse(localStorage.getItem(STORAGE_KEYS.CLICKS) || '[]') as ClickRecord[];
const withdrawals = getAllWithdrawals();
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
const weekFromNow = new Date();
weekFromNow.setDate(weekFromNow.getDate() + 7);
// 今日数据
const todayClicks = clicks.filter(c => new Date(c.clickTime) >= today).length;
const todayBindings = bindings.filter(b => new Date(b.createdAt) >= today).length;
const todayConversions = bindings.filter(b =>
b.status === 'converted' && b.convertedAt && new Date(b.convertedAt) >= today
).length;
const todayEarnings = bindings
.filter(b => b.status === 'converted' && b.convertedAt && new Date(b.convertedAt) >= today)
.reduce((sum, b) => sum + (b.commission || 0), 0);
// 本月数据
const monthClicks = clicks.filter(c => new Date(c.clickTime) >= monthStart).length;
const monthBindings = bindings.filter(b => new Date(b.createdAt) >= monthStart).length;
const monthConversions = bindings.filter(b =>
b.status === 'converted' && b.convertedAt && new Date(b.convertedAt) >= monthStart
).length;
const monthEarnings = bindings
.filter(b => b.status === 'converted' && b.convertedAt && new Date(b.convertedAt) >= monthStart)
.reduce((sum, b) => sum + (b.commission || 0), 0);
// 总计数据
const totalConversions = bindings.filter(b => b.status === 'converted').length;
const totalEarnings = bindings
.filter(b => b.status === 'converted')
.reduce((sum, b) => sum + (b.commission || 0), 0);
// 即将过期数据
const expiringBindings = bindings.filter(b =>
b.status === 'active' &&
new Date(b.expireTime) <= weekFromNow &&
new Date(b.expireTime) > now
).length;
const expiredToday = bindings.filter(b =>
b.status === 'expired' &&
b.updatedAt && new Date(b.updatedAt) >= today
).length;
// 提现数据
const pendingWithdrawals = withdrawals.filter(w => w.status === 'pending').length;
const pendingWithdrawAmount = withdrawals
.filter(w => w.status === 'pending')
.reduce((sum, w) => sum + w.amount, 0);
// 转化率
const conversionRate = clicks.length > 0 ? (totalConversions / clicks.length) * 100 : 0;
return {
todayClicks,
todayBindings,
todayConversions,
todayEarnings,
monthClicks,
monthBindings,
monthConversions,
monthEarnings,
totalClicks: clicks.length,
totalBindings: bindings.length,
totalConversions,
totalEarnings,
expiringBindings,
expiredToday,
pendingWithdrawals,
pendingWithdrawAmount,
conversionRate,
lastUpdated: now.toISOString(),
};
}
/**
* 获取分销排行榜
*/
export function getDistributionRanking(limit: number = 10): Distributor[] {
const distributors = JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]') as Distributor[];
return distributors
.filter(d => d.status === 'active')
.sort((a, b) => b.totalEarnings - a.totalEarnings)
.slice(0, limit);
}
/**
* 获取所有分销商
*/
export function getAllDistributors(): Distributor[] {
if (typeof window === 'undefined') return [];
return JSON.parse(localStorage.getItem(STORAGE_KEYS.DISTRIBUTORS) || '[]');
}

View File

@@ -0,0 +1,254 @@
/**
* 分销模块类型定义
* 核心功能分享链接追踪、30天绑定规则、自动提现
*/
// 分销绑定记录 - 记录每次分享链接点击的绑定关系
export interface DistributionBinding {
id: string;
referrerId: string; // 分享者ID
referrerCode: string; // 分享码
visitorId: string; // 访客ID可以是临时ID或用户ID
visitorPhone?: string; // 访客手机号
visitorNickname?: string; // 访客昵称
// 绑定时间管理
bindingTime: string; // 绑定时间 ISO格式
expireTime: string; // 过期时间(绑定时间+30天
// 绑定状态
status: 'active' | 'converted' | 'expired' | 'cancelled';
// 转化信息
convertedAt?: string; // 转化时间
orderId?: string; // 关联订单ID
orderAmount?: number; // 订单金额
commission?: number; // 佣金金额
// 来源追踪
source: 'link' | 'miniprogram' | 'poster' | 'qrcode';
sourceDetail?: string; // 来源详情,如:朋友圈、微信群等
// 设备信息(可选)
deviceInfo?: {
userAgent?: string;
ip?: string;
platform?: string;
};
createdAt: string;
updatedAt: string;
}
// 分销商信息
export interface Distributor {
id: string;
userId: string;
nickname: string;
phone: string;
referralCode: string;
// 统计数据
totalClicks: number; // 总点击数
totalBindings: number; // 总绑定数
activeBindings: number; // 有效绑定数
convertedBindings: number; // 已转化数
expiredBindings: number; // 已过期数
// 收益数据
totalEarnings: number; // 总收益
pendingEarnings: number; // 待结算收益
withdrawnEarnings: number; // 已提现收益
// 提现配置
autoWithdraw: boolean; // 是否开启自动提现
autoWithdrawThreshold: number; // 自动提现阈值
autoWithdrawAccount?: WithdrawAccount; // 自动提现账户
// 分销等级
level: 'normal' | 'silver' | 'gold' | 'diamond';
commissionRate: number; // 佣金比例0-100
status: 'active' | 'frozen' | 'disabled';
createdAt: string;
updatedAt: string;
}
// 提现账户信息
export interface WithdrawAccount {
type: 'wechat' | 'alipay';
account: string; // 账号
name: string; // 真实姓名
verified: boolean; // 是否已验证
verifiedAt?: string;
}
// 提现记录
export interface WithdrawRecord {
id: string;
distributorId: string;
userId: string;
userName: string;
amount: number; // 提现金额
fee: number; // 手续费
actualAmount: number; // 实际到账金额
method: 'wechat' | 'alipay';
account: string;
accountName: string;
// 打款信息
status: 'pending' | 'processing' | 'completed' | 'failed' | 'rejected';
isAuto: boolean; // 是否自动打款
// 支付渠道返回信息
paymentNo?: string; // 支付流水号
paymentTime?: string; // 打款时间
paymentError?: string; // 打款失败原因
// 审核信息
reviewedBy?: string;
reviewNote?: string;
reviewedAt?: string;
createdAt: string;
completedAt?: string;
}
// 分销点击记录(用于追踪每次链接点击)
export interface ClickRecord {
id: string;
referralCode: string;
referrerId: string;
// 访客信息
visitorId: string; // 设备指纹或用户ID
isNewVisitor: boolean; // 是否新访客
// 来源信息
source: 'link' | 'miniprogram' | 'poster' | 'qrcode';
sourceUrl?: string;
referer?: string;
// 设备信息
deviceInfo?: {
userAgent?: string;
ip?: string;
platform?: string;
screenSize?: string;
};
// 后续行为
registered: boolean; // 是否注册
registeredAt?: string;
purchased: boolean; // 是否购买
purchasedAt?: string;
clickTime: string;
createdAt: string;
}
// 分销配置
export interface DistributionConfig {
// 绑定规则
bindingDays: number; // 绑定有效期默认30
bindingPriority: 'first' | 'last'; // 绑定优先级first=首次绑定last=最后绑定
// 佣金规则
defaultCommissionRate: number; // 默认佣金比例
levelRates: {
normal: number;
silver: number;
gold: number;
diamond: number;
};
// 提现规则
minWithdrawAmount: number; // 最低提现金额
withdrawFeeRate: number; // 提现手续费比例
autoWithdrawEnabled: boolean; // 是否允许自动提现
autoWithdrawTime: string; // 自动提现时间每天10:00
// 提醒规则
expireRemindDays: number; // 过期前N天提醒
// 状态
enabled: boolean;
}
// 过期提醒记录
export interface ExpireReminder {
id: string;
bindingId: string;
distributorId: string;
bindingInfo: {
visitorNickname?: string;
visitorPhone?: string;
bindingTime: string;
expireTime: string;
};
reminderType: 'expiring_soon' | 'expired';
daysRemaining?: number;
// 提醒状态
isRead: boolean;
readAt?: string;
createdAt: string;
}
// 分销统计概览
export interface DistributionOverview {
// 今日数据
todayClicks: number;
todayBindings: number;
todayConversions: number;
todayEarnings: number;
// 本月数据
monthClicks: number;
monthBindings: number;
monthConversions: number;
monthEarnings: number;
// 总计数据
totalClicks: number;
totalBindings: number;
totalConversions: number;
totalEarnings: number;
// 即将过期数据
expiringBindings: number; // 7天内即将过期的绑定数
expiredToday: number; // 今日过期数
// 提现数据
pendingWithdrawals: number; // 待处理提现申请数
pendingWithdrawAmount: number; // 待处理提现金额
// 转化率
conversionRate: number; // 点击转化率
lastUpdated: string;
}
// API响应类型
export interface DistributionAPIResponse<T = unknown> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
// 分销排行榜条目
export interface DistributionRankItem {
rank: number;
distributorId: string;
nickname: string;
avatar?: string;
totalEarnings: number;
totalConversions: number;
level: string;
}

View File

@@ -0,0 +1,314 @@
/**
* 分销模块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,
};
}