第一次提交:小程序前端最新版本

This commit is contained in:
XingQue
2026-06-14 02:38:05 +08:00
commit 5e90f7c87c
677 changed files with 153758 additions and 0 deletions

0
utils/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
utils/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
utils/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class UtilsConfig(AppConfig):
name = "utils"

110
utils/celery_utils.py Normal file
View File

@@ -0,0 +1,110 @@
"""
阿龙电竞 - Celery工具函数
提供通用的Celery辅助函数确保数据安全和操作稳定
"""
import logging
from django.db import transaction
from datetime import datetime
from decimal import Decimal, InvalidOperation
# 配置日志
logger = logging.getLogger(__name__)
def safe_decimal_operation(value, default=Decimal('0.00')):
"""
安全处理Decimal操作防止类型转换错误
:param value: 输入值
:param default: 默认值
:return: Decimal对象
"""
try:
if value is None:
return default
if isinstance(value, Decimal):
return value
if isinstance(value, (int, float)):
return Decimal(str(value))
if isinstance(value, str):
# 移除可能的空白字符和特殊字符
value = value.strip()
if not value:
return default
return Decimal(value)
# 其他类型尝试转换
return Decimal(str(value))
except (InvalidOperation, TypeError, ValueError) as e:
logger.warning(f"Decimal转换失败: {e}, 输入值: {value}, 类型: {type(value)}, 使用默认值: {default}")
return default
except Exception as e:
logger.error(f"Decimal转换发生未知错误: {e}, 输入值: {value}, 使用默认值: {default}")
return default
def log_task_execution(task_name, success=True, details=""):
"""
记录任务执行日志
:param task_name: 任务名称
:param success: 是否成功
:param details: 详细信息
"""
try:
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
status = "成功" if success else "失败"
log_message = f"[{timestamp}] {task_name} - {status}"
if details:
log_message += f" | {details}"
if success:
logger.info(log_message)
else:
logger.error(log_message)
return log_message
except Exception as e:
# 即使日志记录失败也不影响主流程
print(f"日志记录失败: {e}")
return f"日志记录失败: {e}"
def rollback_on_failure(operation_name):
"""
事务回滚装饰器
:param operation_name: 操作名称
"""
def decorator(func):
def wrapper(*args, **kwargs):
try:
with transaction.atomic():
return func(*args, **kwargs)
except Exception as e:
logger.error(f"{operation_name}事务执行失败: {str(e)}")
raise
return wrapper
return decorator
def retry_on_failure(max_retries=3, delay=60):
"""
重试装饰器可用于Celery任务
:param max_retries: 最大重试次数
:param delay: 重试延迟(秒)
"""
def decorator(func):
def wrapper(*args, **kwargs):
retries = 0
while retries < max_retries:
try:
return func(*args, **kwargs)
except Exception as e:
retries += 1
if retries == max_retries:
logger.error(f"任务{func.__name__}重试{max_retries}次后失败: {str(e)}")
raise
logger.warning(f"任务{func.__name__}{retries}次失败,{delay}秒后重试: {str(e)}")
import time
time.sleep(delay)
return wrapper
return decorator

662
utils/chat-core.js Normal file
View File

@@ -0,0 +1,662 @@
// utils/chat-core.js
import GoEasy from '../static/lib/goeasy-2.13.24.esm.min';
import { jianquanxian } from './imAuth/jianquanxian';
let _globalPrivateHandler = null;
let _globalGroupHandler = null;
let _globalConvHandler = null;
function initGlobalMessageSystem(app) {
app.updateUnreadCount = (count) => updateUnreadCount(app, count);
app.disconnectGoEasy = () => disconnectGoEasy(app);
app.ensureConnection = () => ensureConnection(app);
app.connectWithIdentity = (identityType, userId, isAutoRestore) => connectWithIdentity(app, identityType, userId, isAutoRestore);
app.connectForCurrentRole = () => connectForCurrentRole(app);
app.switchRoleAndReconnect = (newRole) => switchRoleAndReconnect(app, newRole);
app.toggleDoNotDisturb = (enabled) => toggleDoNotDisturb(app, enabled);
app.toggleNotificationMute = (enabled) => toggleNotificationMute(app, enabled);
app.closeNotification = () => closeNotification(app);
app.loadConversations = () => loadConversations(app);
app.updateCurrentPageState = () => updateCurrentPageState(app);
app.getSavedConnection = () => getSavedConnection(app);
app.clearSavedConnection = () => clearSavedConnection(app);
app.saveConnectionState = () => saveConnectionState(app);
app.handleNewMessage = (message) => handleNewMessage(app, message);
loadUserSettings(app);
initNetworkListener(app);
initAppStateListener(app);
checkAndRestoreConnection(app);
setupEventSystem(app);
setupMessageListeners(app);
}
function getCurrentGoEasyUserId() {
return (wx.goEasy && wx.goEasy.im && wx.goEasy.im.userId) || null;
}
function loadUserSettings(app) {
try {
const messageSettings = wx.getStorageSync(app.globalData.messageManager.cacheKeys.messageSettings);
if (messageSettings) {
app.globalData.messageManager = { ...app.globalData.messageManager, ...messageSettings };
}
const connectionSettings = wx.getStorageSync(app.globalData.goEasyConnection.cacheKeys.savedConnection);
if (connectionSettings) {
app.globalData.goEasyConnection = { ...app.globalData.goEasyConnection, ...connectionSettings };
}
const savedUnread = wx.getStorageSync(app.globalData.messageManager.cacheKeys.unreadTotal);
if (savedUnread !== '') {
app.globalData.messageManager.unreadTotal = savedUnread;
updateTabBarBadge(app, savedUnread);
}
const savedMessages = wx.getStorageSync(app.globalData.messageManager.cacheKeys.latestMessages);
if (savedMessages) {
app.globalData.messageManager.latestMessages = savedMessages;
}
const notificationMuted = wx.getStorageSync(app.globalData.messageManager.cacheKeys.notificationMuted);
if (notificationMuted !== '') {
app.globalData.messageManager.notificationMuted = notificationMuted;
}
} catch (error) {
console.warn('加载用户设置失败:', error);
}
}
function initNetworkListener(app) {
wx.onNetworkStatusChange((res) => {
if (res.isConnected) {
ensureConnection(app);
} else {
app.globalData.goEasyConnection.status = 'disconnected';
app.emitEvent('connectionChanged', { status: 'disconnected', reason: 'network' });
}
});
}
function initAppStateListener(app) {
wx.onAppShow(() => {
ensureConnection(app);
updateCurrentPageState(app);
// ✅ 连接正常时,主动刷一次未读角标
if (wx.goEasy.getConnectionStatus && wx.goEasy.getConnectionStatus() === 'connected') {
loadConversations(app);
}
});
wx.onAppHide(() => {
pauseHeartbeat(app);
saveConnectionState(app);
});
}
function checkAndRestoreConnection(app) {
try {
const savedConnection = wx.getStorageSync(app.globalData.goEasyConnection.cacheKeys.savedConnection);
if (savedConnection && savedConnection.userId && savedConnection.identityType) {
const now = Date.now();
const lastTime = savedConnection.lastConnectTime || 0;
const hoursDiff = (now - lastTime) / (1000 * 60 * 60);
const validityHours = app.globalData.goEasyConnection.config.cacheValidityHours || 12;
if (hoursDiff > validityHours) {
clearSavedConnection(app);
} else {
ensureConnection(app);
}
}
} catch (error) {
console.warn('检查保存的连接信息失败:', error);
}
}
function setupEventSystem(app) {
app.globalData.eventListeners = {};
}
// 🔥 修改点:不依赖 saved 缓存,只要连接状态正常就刷新角标
function ensureConnection(app) {
const connectionStatus = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
if (connectionStatus === 'connected' || connectionStatus === 'reconnected') {
setupMessageListeners(app);
loadConversations(app); // 每次进入前台都刷新未读
return;
}
// 连接不存在时尝试恢复
const saved = getSavedConnection(app);
if (!saved || !saved.userId || !saved.identityType) return;
connectWithIdentity(app, saved.identityType, saved.userId, true);
}
function getSavedConnection(app) {
try {
const saved = wx.getStorageSync(app.globalData.goEasyConnection.cacheKeys.savedConnection);
return saved ? JSON.parse(saved) : null;
} catch (error) {
console.error('获取保存的连接信息失败:', error);
return null;
}
}
function saveConnectionState(app) {
try {
const { goEasyConnection } = app.globalData;
const connectionState = {
userId: goEasyConnection.userId,
identityType: goEasyConnection.identityType,
lastConnectTime: Date.now(),
autoReconnect: goEasyConnection.autoReconnect,
status: goEasyConnection.status,
};
wx.setStorageSync(goEasyConnection.cacheKeys.savedConnection, JSON.stringify(connectionState));
} catch (error) {
console.warn('保存连接状态失败:', error);
}
}
function clearSavedConnection(app) {
const { cacheKeys } = app.globalData.goEasyConnection;
wx.removeStorageSync(cacheKeys.savedConnection);
wx.removeStorageSync(cacheKeys.userId);
wx.removeStorageSync(cacheKeys.identityType);
app.globalData.goEasyConnection.userId = '';
app.globalData.goEasyConnection.identityType = '';
app.globalData.goEasyConnection.lastConnectTime = 0;
}
function disconnectGoEasy(app) {
return new Promise((resolve) => {
app.globalData.goEasyConnection.status = 'disconnected';
app.globalData.goEasyConnection.autoReconnect = false;
stopHeartbeat(app);
removeGlobalMessageListeners();
if (wx.goEasy && wx.goEasy.disconnect) {
wx.goEasy.disconnect({
onSuccess: () => {
clearSavedConnection(app);
app.globalData.messageManager.unreadTotal = 0;
updateTabBarBadge(app, 0);
app.emitEvent('connectionChanged', { status: 'disconnected', manual: true });
resolve();
},
onFailed: () => {
clearSavedConnection(app);
app.globalData.messageManager.unreadTotal = 0;
updateTabBarBadge(app, 0);
resolve();
}
});
} else {
clearSavedConnection(app);
app.globalData.messageManager.unreadTotal = 0;
updateTabBarBadge(app, 0);
resolve();
}
});
}
async function connectWithIdentity(app, identityType, userId, isAutoRestore = false) {
const quanxian = await jianquanxian(app);
if (!quanxian.allowed) return Promise.reject(quanxian.reason);
app.globalData.goEasyConnection.autoReconnect = true;
app.globalData.goEasyConnection.status = 'connecting';
app.globalData.goEasyConnection.userId = userId;
app.globalData.goEasyConnection.identityType = identityType;
if (!isAutoRestore) app.globalData.goEasyConnection.reconnectAttempts = 0;
saveConnectionState(app);
const uid = wx.getStorageSync('uid');
const currentUser = app.globalData.currentUser || {
id: uid,
name: '用户' + (uid ? uid.substring(0, 6) : ''),
avatar: app.globalData.ossImageUrl + app.globalData.morentouxiang,
};
return new Promise((resolve, reject) => {
wx.goEasy.connect({
id: userId,
data: { name: currentUser.name, avatar: currentUser.avatar, identityType },
onSuccess: () => {
app.globalData.goEasyConnection.status = 'connected';
app.globalData.goEasyConnection.lastConnectTime = Date.now();
app.globalData.goEasyConnection.reconnectAttempts = 0;
saveConnectionState(app);
startHeartbeat(app);
setupMessageListeners(app);
loadConversations(app); // 连接成功立刻拉未读数
app.emitEvent('connectionChanged', { status: 'connected', identityType, userId });
if (!isAutoRestore) {
wx.showToast({ title: '连接成功', icon: 'success', duration: 2000 });
}
resolve();
},
onFailed: (error) => {
console.error('GoEasy连接失败:', error);
if (error.code === 900) {
app.globalData.goEasyConnection.autoReconnect = false;
app.globalData.goEasyConnection.status = 'disconnected';
clearSavedConnection(app);
stopHeartbeat(app);
wx.showModal({
title: '消息功能暂不可用',
content: '当前使用人数爆满,消息功能暂时无法使用。请联系管理员升级套餐后重试。',
showCancel: false,
confirmText: '我知道了'
});
app.emitEvent('connectionChanged', { status: 'disconnected', error, reason: 'overcapacity' });
reject(error);
return;
}
app.globalData.goEasyConnection.status = 'disconnected';
app.emitEvent('connectionChanged', { status: 'disconnected', error, isAutoRestore });
const { autoReconnect, reconnectAttempts, maxReconnectAttempts } = app.globalData.goEasyConnection;
if (autoReconnect && reconnectAttempts < maxReconnectAttempts) {
app.globalData.goEasyConnection.reconnectAttempts++;
setTimeout(() => {
if (app.globalData.goEasyConnection.autoReconnect) {
connectWithIdentity(app, identityType, userId, true);
}
}, app.globalData.goEasyConnection.config.reconnectDelay);
reject(error);
} else {
reject(error);
}
},
});
});
}
function connectForCurrentRole(app) {
const uid = wx.getStorageSync('uid');
if (!uid) return;
const role = app.globalData.currentRole || 'normal';
const prefixMap = { normal: 'Boss', dashou: 'Ds', shangjia: 'Sj', guanshi: 'Gs', zuzhang: 'Zz' , kaoheguan: 'Kh'};
const prefix = prefixMap[role] || 'Boss';
const targetUserId = prefix + uid;
const currentUserId = getCurrentGoEasyUserId();
if (currentUserId === targetUserId) {
loadConversations(app);
setupMessageListeners(app);
return;
}
if (currentUserId) {
wx.goEasy.disconnect({ onSuccess: () => {}, onFailed: () => {} });
}
connectWithIdentity(app, role, targetUserId, false);
}
async function switchRoleAndReconnect(app, newRole) {
try {
await disconnectGoEasy(app);
app.globalData.currentRole = newRole;
wx.setStorageSync('currentRole', newRole);
const uid = wx.getStorageSync('uid');
const prefixMap = { normal: 'Boss', dashou: 'Ds', shangjia: 'Sj', guanshi: 'Gs', zuzhang: 'Zz' , kaoheguan: 'Kh'};
const prefix = prefixMap[newRole] || 'Boss';
const targetUserId = prefix + uid;
await connectWithIdentity(app, newRole, targetUserId, false);
return true;
} catch (error) {
wx.showToast({ title: '聊天功能不可用,可能是人数爆满或未充值会员', icon: 'none' });
throw error;
}
}
function startHeartbeat(app) {
const { goEasyConnection } = app.globalData;
stopHeartbeat(app);
goEasyConnection.heartbeatInterval = setInterval(() => {
if (goEasyConnection.status === 'connected' && wx.goEasy?.im) {
wx.goEasy.im.ping({
onSuccess: () => {},
onFailed: (error) => {
console.error('心跳失败:', error);
goEasyConnection.status = 'reconnecting';
attemptReconnect(app);
},
});
}
}, goEasyConnection.config.heartbeatInterval);
}
function stopHeartbeat(app) {
if (app.globalData.goEasyConnection.heartbeatInterval) {
clearInterval(app.globalData.goEasyConnection.heartbeatInterval);
app.globalData.goEasyConnection.heartbeatInterval = null;
}
}
function pauseHeartbeat(app) { stopHeartbeat(app); }
function attemptReconnect(app) {
const { goEasyConnection } = app.globalData;
if (!goEasyConnection.autoReconnect) return;
if (goEasyConnection.userId && goEasyConnection.identityType) {
connectWithIdentity(app, goEasyConnection.identityType, goEasyConnection.userId, true);
}
}
/* ---------- 消息监听器 ---------- */
function removeGlobalMessageListeners() {
if (_globalPrivateHandler) {
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _globalPrivateHandler);
_globalPrivateHandler = null;
}
if (_globalGroupHandler) {
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _globalGroupHandler);
_globalGroupHandler = null;
}
if (_globalConvHandler) {
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _globalConvHandler);
_globalConvHandler = null;
}
}
function setupMessageListeners(app) {
removeGlobalMessageListeners();
_globalPrivateHandler = (message) => {
app.handleNewMessage(message);
};
_globalGroupHandler = (message) => {
app.handleNewMessage(message);
};
_globalConvHandler = (conversations) => {
const unreadTotal = conversations.unreadTotal || 0;
app.globalData.messageManager.unreadTotal = unreadTotal;
updateTabBarBadge(app, unreadTotal);
wx.setStorageSync(app.globalData.messageManager.cacheKeys.unreadTotal, unreadTotal);
if (typeof app.emitEvent === 'function') {
app.emitEvent('conversationsUpdated', conversations);
}
};
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _globalPrivateHandler);
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _globalGroupHandler);
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _globalConvHandler);
}
function handleNewMessage(app, message) {
const messageId = message.messageId || message.id;
const fingerprint = `${message.senderId}_${message.timestamp}_${message.type}`;
const msgKey = messageId || fingerprint;
if (!app._processedMessages) app._processedMessages = new Set();
if (app._processedMessages.has(msgKey)) return;
app._processedMessages.add(msgKey);
if (app._processedMessages.size > 200) {
app._processedMessages.delete(app._processedMessages.values().next().value);
}
const now = Date.now();
const msgTime = message.timestamp || now;
if (now - msgTime > 60000) {
cacheMessage(app, message);
return;
}
cacheMessage(app, message);
if (shouldShowNotification(app)) {
let notificationType = 'private';
let extraData = {};
if (message.groupId) {
notificationType = 'group';
extraData = {
groupId: message.groupId,
orderId: message.orderId || '',
groupName: '',
groupAvatar: '',
isCross: message.isCross || 0
};
const groupInfo = app.globalData.groupInfoMap?.[message.groupId];
if (groupInfo) {
extraData.groupName = groupInfo.name || '';
extraData.groupAvatar = groupInfo.avatar || '';
}
} else if (message.teamId) {
notificationType = 'cs';
extraData = { teamId: message.teamId };
}
showNotificationDirect(app, {
senderId: message.senderId,
senderName: message.senderData?.name || '用户',
avatar: message.senderData?.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang),
content: formatMessageForNotification(message),
message,
notificationType,
...extraData
});
}
}
function shouldShowNotification(app) {
if (app.globalData.messageManager.notificationMuted) return false;
if (isDoNotDisturbTime(app)) return false;
if (app.globalData.pageState.isInChatPage) {
const pages = getCurrentPages();
if (pages.length > 0) {
const currentPage = pages[pages.length - 1];
if (currentPage.data && currentPage.data.currentChatId) {
if (isMessageFromCurrentChat(app, currentPage.data.currentChatId)) return false;
}
}
}
return true;
}
function isDoNotDisturbTime(app) {
const { doNotDisturb, doNotDisturbStart, doNotDisturbEnd } = app.globalData.messageManager;
if (!doNotDisturb) return false;
const now = new Date();
const currentTime = now.getHours() * 60 + now.getMinutes();
const [sH, sM] = doNotDisturbStart.split(':').map(Number);
const [eH, eM] = doNotDisturbEnd.split(':').map(Number);
const startTime = sH * 60 + sM;
const endTime = eH * 60 + eM;
if (startTime < endTime) {
return currentTime >= startTime && currentTime < endTime;
} else {
return currentTime >= startTime || currentTime < endTime;
}
}
function isMessageFromCurrentChat(app, currentChatId) {
return false;
}
function showNotificationDirect(app, data) {
if (app.globalData.globalNotification?.show) {
try {
app.globalData.globalNotification.show(data);
return;
} catch (error) {
console.warn('全局通知显示失败:', error);
}
}
app.emitEvent('showNotification', data);
}
async function updateUnreadCount(app, customCount) {
let unreadTotal = customCount;
if (unreadTotal === undefined) {
try {
const result = await getUnreadCount(app);
unreadTotal = result.unreadTotal || 0;
} catch (error) {
console.error('获取未读数失败:', error);
return;
}
}
app.globalData.messageManager.unreadTotal = unreadTotal;
updateTabBarBadge(app, unreadTotal);
wx.setStorageSync(app.globalData.messageManager.cacheKeys.unreadTotal, unreadTotal);
app.emitEvent('unreadCountChanged', { unreadTotal });
}
function getUnreadCount(app) {
return new Promise((resolve, reject) => {
if (!getCurrentGoEasyUserId()) {
resolve({ unreadTotal: 0 });
return;
}
wx.goEasy.im.latestConversations({
onSuccess: (result) => resolve({
unreadTotal: result.unreadTotal || 0,
conversations: result.content?.conversations || [],
}),
onFailed: reject,
});
});
}
function formatMessageForNotification(message) {
switch (message.type) {
case 'text': return message.payload.text || '[文本]';
case 'image': return '[图片]';
case 'audio': return '[语音]';
case 'video': return '[视频]';
case 'file': return '[文件]';
case 'order': return '[订单]';
default: return '[新消息]';
}
}
function cacheMessage(app, message) {
const { latestMessages } = app.globalData.messageManager;
latestMessages.unshift({
id: message.messageId,
type: message.type,
senderId: message.senderId,
senderName: message.senderData?.name,
content: formatMessageForNotification(message),
timestamp: message.timestamp || Date.now(),
conversationId: message.conversationId,
});
if (latestMessages.length > 50) latestMessages.length = 50;
wx.setStorageSync(app.globalData.messageManager.cacheKeys.latestMessages, latestMessages);
}
function loadConversations(app) {
if (!getCurrentGoEasyUserId()) return;
wx.goEasy.im.latestConversations({
onSuccess: (result) => {
if (result.unreadTotal !== undefined) {
updateUnreadCount(app, result.unreadTotal);
}
app.emitEvent('conversationsUpdated', result);
const conversations = result?.content?.conversations || result?.conversations || [];
if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {};
conversations.forEach(c => {
if (c.type === 'group' && c.groupId) {
app.globalData.groupInfoMap[c.groupId] = c.data || {};
}
});
_ensureGroupSubscriptions(app, conversations);
},
onFailed: (error) => console.error('加载会话列表失败:', error),
});
}
function _ensureGroupSubscriptions(app, conversations) {
const groupIds = conversations
.filter(c => c.type === 'group' && c.groupId)
.map(c => c.groupId);
if (groupIds.length === 0) return;
if (!wx.goEasy?.im || typeof wx.goEasy.im.subscribeGroup !== 'function') {
console.error('subscribeGroup 不可用');
return;
}
wx.goEasy.im.subscribeGroup({
groupIds: groupIds,
onSuccess: () => {},
onFailed: (error) => console.error('全局订阅群组失败:', error),
});
}
function updateCurrentPageState(app) {
try {
const pages = getCurrentPages();
if (pages.length > 0) {
const currentPage = pages[pages.length - 1];
app.globalData.pageState.currentPage = currentPage.route;
app.globalData.pageState.isInChatPage = currentPage.route === 'pages/liaotian/liaotian';
app.globalData.pageState.lastPageUpdate = Date.now();
}
} catch (error) {
console.warn('更新页面状态失败:', error);
}
}
function saveUserSettings(app) {
try {
wx.setStorageSync(app.globalData.messageManager.cacheKeys.messageSettings, {
soundEnabled: app.globalData.messageManager.soundEnabled,
vibrationEnabled: app.globalData.messageManager.vibrationEnabled,
doNotDisturb: app.globalData.messageManager.doNotDisturb,
doNotDisturbStart: app.globalData.messageManager.doNotDisturbStart,
doNotDisturbEnd: app.globalData.messageManager.doNotDisturbEnd,
notificationMuted: app.globalData.messageManager.notificationMuted,
notificationStyle: app.globalData.messageManager.notificationStyle,
});
} catch (error) {
console.warn('保存用户设置失败:', error);
}
}
function toggleDoNotDisturb(app, enabled) {
app.globalData.messageManager.doNotDisturb = enabled;
saveUserSettings(app);
wx.showToast({ title: `免打扰${enabled ? '开启' : '关闭'}`, icon: 'success', duration: 2000 });
}
function toggleNotificationMute(app, enabled) {
app.globalData.messageManager.notificationMuted = enabled;
wx.setStorageSync(app.globalData.messageManager.cacheKeys.notificationMuted, enabled);
saveUserSettings(app);
wx.showToast({ title: `通知${enabled ? '静音' : '开启'}`, icon: 'success', duration: 2000 });
}
function closeNotification(app) {
app.emitEvent('hideNotification');
}
function updateTabBarBadge(app, unreadCount) {
const { messageManager } = app.globalData;
if (!messageManager.showTabBarBadge) return;
let badgeText = '';
if (unreadCount > 0) {
badgeText = unreadCount > 99 ? '99+' : unreadCount.toString();
}
messageManager.tabBarBadgeText = badgeText;
app.emitEvent('tabBarBadgeChanged', {
index: messageManager.tabBarIndex,
badgeText,
showRedDot: unreadCount > 0,
forceUpdate: true,
});
updateTabBarDirectly(app, badgeText);
saveTabBarBadgeToStorage(app, badgeText);
}
function updateTabBarDirectly(app, badgeText) {
try {
const pages = getCurrentPages();
if (pages.length > 0) {
const tabBar = pages[pages.length - 1].selectComponent('#custom-tab-bar');
if (tabBar && tabBar.setData) tabBar.setData({ badgeText });
}
} catch (error) { console.warn('直接更新TabBar失败:', error); }
}
function saveTabBarBadgeToStorage(app, badgeText) {
try {
wx.setStorageSync('tabBarMessageBadge', badgeText);
} catch (error) { console.warn('保存TabBar徽章失败:', error); }
}
module.exports = { initGlobalMessageSystem };

374
utils/chat_utils.py Normal file
View File

@@ -0,0 +1,374 @@
# utils/chat_utils.py
import json
import logging
import requests
from django.conf import settings
from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia
from peizhi.models import ClubConfig
from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia
logger = logging.getLogger('chat_utils')
# ========== 辅助配置获取(从 ClubConfig ==========
def _get_self_club_config():
if not hasattr(_get_self_club_config, '_config'):
try:
_get_self_club_config._config = ClubConfig.objects.filter(is_self=1).first()
except Exception as e:
logger.warning(f"获取我方俱乐部配置失败: {e}")
_get_self_club_config._config = None
return _get_self_club_config._config
def _get_self_goeasy_appkey():
cfg = _get_self_club_config()
if cfg and cfg.chat_api_key:
return cfg.chat_api_key
return getattr(settings, 'GOEASY_APPKEY', '')
def _get_self_goeasy_secret():
cfg = _get_self_club_config()
if cfg and cfg.chat_api_id:
return cfg.chat_api_id
return getattr(settings, 'GOEASY_SECRET', '')
def _get_self_storage_domain():
cfg = _get_self_club_config()
if cfg and cfg.storage_bucket_domain:
return cfg.storage_bucket_domain
return ''
def _get_self_default_avatar():
cfg = _get_self_club_config()
if cfg and cfg.club_avatar:
return cfg.club_avatar
return ''
# ========== 头像拼接 ==========
def _full_local_avatar(relative_url):
storage_domain = _get_self_storage_domain()
default_avatar = _get_self_default_avatar()
if not relative_url:
return default_avatar or ''
if relative_url.startswith('http'):
return relative_url
if storage_domain.endswith('/'):
return storage_domain + relative_url.lstrip('/')
return storage_domain + '/' + relative_url if storage_domain else relative_url
def _full_remote_avatar(storage_bucket_domain, relative_url):
if not relative_url:
return ''
if relative_url.startswith('http'):
return relative_url
if storage_bucket_domain.endswith('/'):
return storage_bucket_domain + relative_url.lstrip('/')
return storage_bucket_domain + '/' + relative_url if storage_bucket_domain else relative_url
# ========== GoEasy 订阅 ==========
def _subscribe_users_to_group(user_ids, group_ids, appkey=None, secret=None):
if not appkey:
appkey = _get_self_goeasy_appkey()
if not secret:
secret = _get_self_goeasy_secret()
if not appkey:
logger.error("GoEasy AppKey 未配置")
return False
url = 'https://rest-hangzhou.goeasy.io/v2/im/subscribe-groups'
body = {"appkey": appkey, "userIds": user_ids, "groupIds": group_ids}
headers = {"Content-Type": "application/json"}
if secret:
headers["Authorization"] = f"Bearer {secret}"
try:
resp = requests.post(url, headers=headers, json=body, timeout=10)
if resp.status_code == 200:
return True
logger.error(f"订阅失败: {resp.status_code} {resp.text}")
return False
except Exception as e:
logger.error(f"订阅异常: {e}", exc_info=True)
return False
# ========== 发送群消息(完整参数) ==========
def _send_group_message(appkey, secret, group_id, sender_id, sender_name, sender_avatar,
message_text, custom_payload=None, group_name=None, order_id=None, is_cross=0):
if not appkey:
appkey = _get_self_goeasy_appkey()
if not secret:
secret = _get_self_goeasy_secret()
if not appkey:
logger.error("GoEasy AppKey 未配置")
return False
url = 'https://rest-hangzhou.goeasy.io/v2/im/message'
# to.data 包含群聊展示信息及业务字段
to_data = {
"name": group_name or group_id,
"avatar": sender_avatar or "",
}
if order_id:
to_data["orderId"] = order_id
to_data["isCross"] = is_cross
payload_str = custom_payload.get("text", message_text) if custom_payload else message_text
request_body = {
"appkey": appkey,
"senderId": sender_id,
"senderData": {"avatar": sender_avatar, "name": sender_name},
"to": {
"type": "group",
"id": group_id,
"data": to_data
},
"type": "text",
"payload": payload_str
}
headers = {"Content-Type": "application/json"}
if secret:
headers["Authorization"] = f"Bearer {secret}"
try:
resp = requests.post(url, headers=headers, json=request_body, timeout=10)
if resp.status_code == 200:
return True
logger.error(f"群聊消息发送失败,状态码:{resp.status_code},响应:{resp.text}")
return False
except Exception as e:
logger.error(f"发送群聊消息异常: {e}", exc_info=True)
return False
# ========== 核心入口 ==========
def establish_order_chat(dingdan_id):
try:
order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
logger.error(f"订单 {dingdan_id} 不存在")
return False
dashou_uid = order.jiedan_dashou_id
dashou_goeasy_id = f"Ds{dashou_uid}"
dashou_nickname = f'打手{dashou_uid[:6]}'
dashou_avatar_relative = ''
try:
dashou_user = UserMain.objects.get(yonghuid=dashou_uid)
dashou_avatar_relative = dashou_user.avatar or ''
dashou_profile = UserDashou.objects.get(user=dashou_user)
if dashou_profile.nicheng:
dashou_nickname = dashou_profile.nicheng
except Exception as e:
logger.warning(f"获取打手信息失败: {e}")
dashou_avatar = _full_local_avatar(dashou_avatar_relative)
appkey = _get_self_goeasy_appkey()
secret = _get_self_goeasy_secret()
if not appkey:
logger.error("GoEasy AppKey 未配置")
return False
# 判断是否跨平台
if order.is_cross != 1:
return _handle_local_order(order, dashou_goeasy_id, dashou_nickname, dashou_avatar, appkey, secret)
else:
if order.dispatch_type == 1:
# 我方派单,与普通订单处理一致
return _handle_local_order(order, dashou_goeasy_id, dashou_nickname, dashou_avatar, appkey, secret)
elif order.dispatch_type == 2:
return _handle_cross_partner_order(order, dashou_goeasy_id, dashou_nickname, dashou_avatar, appkey, secret)
else:
return False
# ========== 普通订单 / 我方派单 ==========
def _handle_local_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret):
group_id = f"group_{order.dingdan_id}"
group_name = (order.jieshao[:20] + '') if order.jieshao and len(order.jieshao) > 20 else (order.jieshao or f"订单{order.dingdan_id}")
group_avatar = _full_local_avatar(order.tupian) if order.tupian else _full_local_avatar('')
partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order)
if not partner_goeasy_id:
return False
# 订阅双方
if not _subscribe_users_to_group([dashou_goeasy_id, partner_goeasy_id], [group_id], appkey, secret):
return False
# 1. 打手发送初始化消息
init_msg_text = f"订单已接单,内容:{order.jieshao},备注:{order.beizhu}游戏ID{order.nicheng}"
success1 = _send_group_message(
appkey, secret, group_id, dashou_goeasy_id, dashou_name, group_avatar,
init_msg_text, None, group_name=group_name, order_id=order.dingdan_id,
is_cross=order.is_cross
)
# 2. 【新增】派单方也发一条消息,确保他也能看到群聊
partner_msg = f"订单已确认,请开始服务。"
success2 = _send_group_message(
appkey, secret, group_id, partner_goeasy_id, partner_name, partner_avatar,
partner_msg, None, group_name=group_name, order_id=order.dingdan_id,
is_cross=order.is_cross
)
return success1 and success2
def _get_local_partner_info(order):
"""本地订单下单方:老板或商家"""
if order.fadan_pingtai == 1: # 老板
try:
ext = order.pingtai_kuozhan
laoban_id = ext.laoban_id
avatar_full = _full_local_avatar('')
nickname = f'老板{laoban_id[:6]}'
try:
boss_user = UserMain.objects.get(yonghuid=laoban_id)
avatar_full = _full_local_avatar(boss_user.avatar or '')
boss_profile = UserBoss.objects.get(user=boss_user)
if boss_profile.nickname:
nickname = boss_profile.nickname
except Exception:
pass
return f"Boss{laoban_id}", nickname, avatar_full
except Exception as e:
logger.error(f"获取老板信息失败: {e}")
return None, None, None
elif order.fadan_pingtai == 2: # 商家
try:
ext = order.shangjia_kuozhan
shangjia_id = ext.shangjia_id
nickname = ext.sjnicheng or f'商家{shangjia_id[:6]}'
avatar_full = _full_local_avatar('')
try:
sj_user = UserMain.objects.get(yonghuid=shangjia_id)
avatar_full = _full_local_avatar(sj_user.avatar or '')
except Exception:
pass
return f"Sj{shangjia_id}", nickname, avatar_full
except Exception as e:
logger.error(f"获取商家信息失败: {e}")
return None, None, None
else:
if order.user1_id:
return order.user1_id, "用户", _full_local_avatar('')
return None, None, None
# ========== 跨平台 + 对方派单 ==========
def _handle_cross_partner_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret):
partner_club_id = order.partner_club_id
if not partner_club_id:
logger.warning("缺少 partner_club_id")
return _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret)
try:
partner_config = ClubConfig.objects.get(club_id=partner_club_id, is_self=0)
except ClubConfig.DoesNotExist:
logger.error(f"未找到对方俱乐部配置: {partner_club_id}")
return _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret)
partner_order_id = order.partner_order_id
if not partner_order_id:
logger.error("partner_order_id 为空")
return _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret)
# 推断对方下单方标识
target_partner_goeasy_id = _get_cross_partner_identity(order, partner_order_id)
if not target_partner_goeasy_id:
logger.error("无法推断对方下单方标识")
return _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret)
# 对方默认头像
partner_default_avatar = _full_remote_avatar(partner_config.storage_bucket_domain or '', partner_config.club_avatar or '') or _full_local_avatar('')
# ---- 1. 在我方建群 ----
local_group_id = f"group_{order.dingdan_id}"
local_group_name = (order.jieshao[:20] + '') if order.jieshao and len(order.jieshao) > 20 else f"跨平台订单{order.dingdan_id}"
local_group_avatar = _full_local_avatar(order.tupian) if order.tupian else _full_local_avatar('')
proxy_partner_id = f"{partner_order_id}_{target_partner_goeasy_id}"
proxy_partner_name = f"对方用户({partner_club_id})"
# 对方代理头像使用对方默认头像
proxy_partner_avatar = partner_default_avatar
if not _subscribe_users_to_group([dashou_goeasy_id, proxy_partner_id], [local_group_id], appkey, secret):
return False
# 1. 打手发送初始化消息
init_msg_text = f"跨平台订单已接单,内容:{order.jieshao},备注:{order.beizhu}"
_send_group_message(
appkey, secret, local_group_id, dashou_goeasy_id, dashou_name, local_group_avatar,
init_msg_text, None, group_name=local_group_name, order_id=order.dingdan_id,
is_cross=1
)
# 2. 【新增】对方代理也发一条消息,确保对方能看到群聊
partner_msg = f"订单已确认,请等待服务。"
_send_group_message(
appkey, secret, local_group_id, proxy_partner_id, proxy_partner_name, proxy_partner_avatar,
partner_msg, None, group_name=local_group_name, order_id=order.dingdan_id,
is_cross=1
)
# ---- 2. 在对方建群 ----
partner_appkey = partner_config.chat_api_key
partner_secret = partner_config.chat_api_id or ''
if not partner_appkey:
return True # 无对方 key仅完成我方建群
partner_group_id = f"group_{partner_order_id}"
partner_group_name = f"跨平台订单-我方打手已接单"
# 代理我方打手 ID
our_agent_id = f"{order.dingdan_id}_{dashou_goeasy_id}"
our_agent_name = f"[我方]{dashou_name}"
# 对方频道头像用对方俱乐部默认头像
partner_group_avatar = partner_default_avatar
if not _subscribe_users_to_group([our_agent_id, target_partner_goeasy_id], [partner_group_id], partner_appkey, partner_secret):
logger.error("向对方频道订阅失败")
return True
# 我方打手在对方频道发消息
cross_msg_text = f"我方打手已接单,订单:{order.jieshao},请确认"
_send_group_message(
partner_appkey, partner_secret, partner_group_id, our_agent_id, our_agent_name, partner_group_avatar,
cross_msg_text, None, group_name=partner_group_name, order_id=partner_order_id,
is_cross=1
)
# 【新增】对方用户在对方频道也发一条消息(以对方原始身份)
target_partner_name = f"用户{target_partner_goeasy_id}" # 简单命名,可按需优化
_send_group_message(
partner_appkey, partner_secret, partner_group_id, target_partner_goeasy_id, target_partner_name, partner_default_avatar,
"订单信息已收到。", None, group_name=partner_group_name, order_id=partner_order_id,
is_cross=1
)
return True
def _get_cross_partner_identity(order, partner_order_id):
"""
获取对方下单方的 GoEasy 标识。
优先从商家扩展表取 partner_yonghu_id否则按 partner_order_id 前缀推断。
"""
# 尝试从商家扩展表取(仅当发单平台为商家且存在 partner_yonghu_id
if order.fadan_pingtai == 2 and hasattr(order, 'shangjia_kuozhan'):
ext = order.shangjia_kuozhan
if ext and ext.partner_yonghu_id:
# 直接根据订单前缀构造标识
prefix = partner_order_id[:2].upper()
if prefix == 'SJ':
return f"Sj{ext.partner_yonghu_id}"
elif prefix == 'PT':
return f"Boss{ext.partner_yonghu_id}"
else:
return f"User{ext.partner_yonghu_id}"
# 后备:根据 partner_order_id 前缀推断
prefix = partner_order_id[:2].upper()
uid = partner_order_id[2:] if len(partner_order_id) > 2 else ''
if prefix == 'SJ':
return f"Sj{uid}"
elif prefix == 'PT':
return f"Boss{uid}"
else:
return f"User{uid}"
def _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret):
group_id = f"group_{order.dingdan_id}"
_subscribe_users_to_group([dashou_goeasy_id], [group_id], appkey, secret)
return False

13296
utils/cos-wx-sdk-v5.js Normal file

File diff suppressed because one or more lines are too long

140
utils/fadan_utils.py Normal file
View File

@@ -0,0 +1,140 @@
# utils/fadan_utils.py
from dingdan.models import Fadan
import logging
logger = logging.getLogger('xiaochengxu')
def check_fadan_qiangdan_eligible(yonghuid: str, action_type: int) -> tuple:
"""
通用罚单资格检查。
参数:
yonghuid : 被检查的用户 yonghuid
action_type : 1 = 抢单, 2 = 提现
返回:
(is_eligible: bool, message: str)
True 允许操作False 返回拦截原因。
"""
try:
if action_type == 1:
# 抢单:仅当存在“影响抢单”且“未解决”的罚单时拦截(待缴纳或申诉中)
blocked = Fadan.objects.filter(
beichufa_id=yonghuid,
yingxiang_qiangdan=1,
zhuangtai__in=[1, 3] # 1=待缴纳, 3=申诉中
).exists()
if blocked:
logger.info(f"用户 {yonghuid} 抢单被拦截:存在未解决的影响抢单罚单")
return (False, "您有未缴纳或申诉中的罚单,请先处理后再抢单")
return (True, "无罚单限制")
elif action_type == 2:
# 提现:只要存在任意未解决的罚单(待缴纳/申诉中),不论是否影响抢单,一律禁止提现
blocked = Fadan.objects.filter(
beichufa_id=yonghuid,
zhuangtai__in=[1, 3] # 1=待缴纳, 3=申诉中
).exists()
if blocked:
logger.info(f"用户 {yonghuid} 提现被拦截:存在未解决的罚单")
return (False, "您有未处理完的罚单,需全部完成或驳回后方可提现")
return (True, "罚单状态正常,可提现")
else:
logger.warning(f"未知的检查类型: {action_type}")
return (False, "罚单查询系统异常(无效的检查类型)")
except Exception as e:
logger.error(f"检查用户 {yonghuid} 罚单状态异常: {e}", exc_info=True)
# 安全起见,异常时禁止操作
return (False, "罚单查询系统繁忙,请稍后重试")
import logging
from django.db import transaction
from decimal import Decimal
from dingdan.models import Fadan, FadanFenhong, FadanFenhongLilv
logger = logging.getLogger('xiaochengxu')
def fadan_fenhong(fadan_id):
"""
罚款缴纳后,给申请处罚人累加分红。
自动映射身份,异常不抛出,不影响主流程。
"""
try:
# 1. 查询罚单
try:
fadan = Fadan.objects.get(id=fadan_id)
except Fadan.DoesNotExist:
logger.warning(f"罚单 {fadan_id} 不存在,分红跳过")
return
if fadan.zhuangtai != 2:
logger.info(f"罚单 {fadan_id} 状态非已缴纳,不分红")
return
fenhongzhe_id = fadan.shenqing_chufa or ''
shenfen_in_fadan = fadan.shenqingren_shenfen # 罚单中的申请人身份
jine = fadan.fakuanjine
if not fenhongzhe_id or jine <= 0:
logger.info(f"罚单 {fadan_id} 缺少申请处罚人或金额为0不分红")
return
# 2. 直接用罚单身份的原始值查询费率(费率表 shenfen 与之含义一致)
rate_record = FadanFenhongLilv.objects.filter(shenfen=shenfen_in_fadan).first()
if not rate_record or rate_record.lilv <= 0:
logger.info(f"身份 {shenfen_in_fadan} 无有效分红费率,不分红")
return
rate = rate_record.lilv
# 3. 计算分红金额
amount = (jine * rate).quantize(Decimal('0.01'))
if amount <= 0:
logger.info(f"罚单 {fadan_id} 分红金额为0不分红")
return
# 4. 硬编码映射:罚单身份 → 分红记录表身份
SHENFEN_MAP = {
1: 2, # 客服 → 客服
2: 5, # 售后 → 售后
3: 3, # 管理员 → 平台管理者
4: 4, # 店铺 → 店铺
5: 1, # 商家 → 商家
}
shenfen_in_fenhong = SHENFEN_MAP.get(shenfen_in_fadan)
if shenfen_in_fenhong is None:
logger.info(f"罚单 {fadan_id} 身份 {shenfen_in_fadan} 无对应的分红记录映射,不分红")
return
# 5. 事务累加分红记录(同一用户+身份只保留一条记录)
with transaction.atomic():
record = FadanFenhong.objects.select_for_update().filter(
fenhongzhe_id=fenhongzhe_id,
shenfen=shenfen_in_fenhong # 使用映射后的身份值
).first()
if record:
record.zonge += amount
record.ketixian_fenhong += amount
record.fenhong_cishu += 1
record.yingde_lilv = rate
record.save()
else:
FadanFenhong.objects.create(
fenhongzhe_id=fenhongzhe_id,
shenfen=shenfen_in_fenhong,
zonge=amount,
ketixian_fenhong=amount,
fenhong_cishu=1,
yingde_lilv=rate,
)
logger.info(f"罚单 {fadan_id} 分红完成:用户 {fenhongzhe_id} 身份 {shenfen_in_fenhong} 金额 {amount}")
except Exception as e:
logger.error(f"罚单 {fadan_id} 分红异常:{e}", exc_info=True)

103
utils/goeasy_service.py Normal file
View File

@@ -0,0 +1,103 @@
# apps/dingdan/utils/goeasy_service.py
import requests
import json
import logging
from django.conf import settings
logger = logging.getLogger(__name__)
class GoEasyService:
"""
GoEasy聊天服务工具类
用于建立用户间的私聊关系
"""
def __init__(self):
self.appkey = settings.GOEASY_APPKEY # 需要在settings.py中配置
self.secret = settings.GOEASY_SECRET # GoEasy控制台获取的Secret
self.rest_host = "https://rest-hz.goeasy.io" # 杭州区域
def create_private_chat(self, user1_id, user2_id, user1_info=None, user2_info=None):
"""
建立两个用户间的私聊关系
Args:
user1_id: 用户1在GoEasy的标识
user2_id: 用户2在GoEasy的标识
user1_info: 用户1的附加信息 (avatar, nickname等)
user2_info: 用户2的附加信息
Returns:
bool: 是否成功
"""
try:
# 构造消息数据
message_data = {
"appkey": self.appkey,
"senderId": "system_admin",
"senderData": {
"avatar": "/static/system_avatar.png",
"name": "系统通知"
},
"to": {
"type": "private",
"id": user1_id,
"data": user2_info or {
"avatar": "",
"name": f"用户{user2_id}"
}
},
"type": "system",
"payload": {
"action": "create_chat",
"partner_id": user2_id,
"message": "已为您建立聊天连接"
},
"notification": {
"title": "新聊天",
"body": "您有新的聊天连接"
}
}
# 发送请求
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.secret}"
}
response = requests.post(
f"{self.rest_host}/v2/im/message",
headers=headers,
json=message_data,
timeout=5 # 5秒超时
)
if response.status_code == 200:
logger.info(f"成功建立聊天关系: {user1_id}{user2_id}")
return True
else:
logger.error(f"建立聊天关系失败: {response.status_code}, {response.text}")
return False
except Exception as e:
logger.error(f"调用GoEasy API异常: {str(e)}")
return False
def establish_chat_for_order(self, order, dashou_yonghuid, dashou_info=None):
"""
为订单建立聊天关系
"""
# user1_id 是老板/商家标识 (从订单中获取)
# user2_id 是打手标识
user1_id = order.user1_id
user2_id = f"Ds{dashou_yonghuid}"
if not user1_id or not user2_id:
logger.warning(f"缺少用户标识,无法建立聊天: user1_id={user1_id}, user2_id={user2_id}")
return False
# 建立双向聊天关系
success1 = self.create_private_chat(user1_id, user2_id, user2_info=dashou_info)
success2 = self.create_private_chat(user2_id, user1_id)
return success1 or success2 # 只要有一个方向成功就认为成功

View File

@@ -0,0 +1,51 @@
// utils/imAuth/dashoujianquan.js
const imRequest = require('./imRequest.js');
/**
* 打手 IM 权限检测
* POST /yonghu/dshqltqx
* 后端返回 { code: 0, allow: 1 } 或 { code: 0, allow: 0 }
*/
async function dashoujianquan(app) {
const uid = wx.getStorageSync('uid');
if (!uid) return { allowed: false, reason: '未登录' };
try {
const res = await imRequest({
url: '/yonghu/dshqltqx',
method: 'POST',
data: { uid }
});
if (res && res.data && res.data.code === 0) {
const allow = res.data.allow;
if (allow === 1) {
return { allowed: true };
} else {
await app.disconnectGoEasy();
wx.showToast({
title: '当前身份已不支持消息聊天功能,请充值会员或联系管理员后尝试',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(r => setTimeout(r, 2100));
return { allowed: false, reason: '打手消息权限被限制' };
}
}
throw new Error(res.data?.msg || '鉴权接口异常');
} catch (err) {
console.error('打手鉴权失败:', err);
await app.disconnectGoEasy();
wx.showToast({
title: '消息服务暂不可用,请稍后重试',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(r => setTimeout(r, 2100));
return { allowed: false, reason: '鉴权请求失败' };
}
}
module.exports = { dashoujianquan };

67
utils/imAuth/imRequest.js Normal file
View File

@@ -0,0 +1,67 @@
// utils/imAuth/imRequest.js
// 专用于 IM 鉴权模块的请求封装
// 与 utils/request.js 功能完全一致,但不依赖 getApp()API 域名硬编码
const API_BASE_URL = 'https://6ccy.top/hqhd';
function imRequest(options) {
const token = wx.getStorageSync('token');
const header = {
'Content-Type': 'application/json',
...options.header,
};
if (token) {
header['Authorization'] = 'Bearer ' + token;
}
return new Promise((resolve, reject) => {
wx.request({
url: API_BASE_URL + options.url,
method: options.method,
data: options.data || {},
header,
success: async (res) => {
if (res.statusCode === 401 || res.statusCode === 403) {
// 如果全局有登录 Promise 在等待,则等待完成后重试
const app = getApp();
if (app && app.globalData && app.globalData.loginPromise) {
try {
await app.globalData.loginPromise;
const retryRes = await imRequest(options);
resolve(retryRes);
} catch (err) {
wx.showModal({
content: '操作失败,请先登录',
success: (modalRes) => {
if (modalRes.confirm) {
wx.removeStorageSync('token');
wx.switchTab({ url: '/pages/wode/wode' });
}
},
});
resolve(res);
}
return;
}
// 常规未登录提示
wx.showModal({
content: '操作失败,请先登录',
success: (modalRes) => {
if (modalRes.confirm) {
wx.removeStorageSync('token');
wx.switchTab({ url: '/pages/wode/wode' });
}
},
});
resolve(res);
return;
}
// 正常响应
resolve(res);
},
fail: reject,
});
});
}
module.exports = imRequest;

View File

@@ -0,0 +1,58 @@
// utils/imAuth/jianquanxian.js
// 原require('./laobanjianquan')
// 改:
const { laobanjianquan } = require('./laobanjianquan.js');
const { dashoujianquan } = require('./dashoujianquan.js');
/**
* IM 长连接权限统一入口
* 按顺序进行:
* 1. 检查本地 token/uid 是否存在
* 2. 若不存在,强制断开已有连接,弹窗提示未登录(强制阅读 2 秒)
* 3. 若存在,根据当前角色调用具体的鉴权模块
*
* @param {Object} app 全局 App 实例
* @returns {Promise<{ allowed: boolean, reason?: string }>}
*/
async function jianquanxian(app) {
// 1. 检测登录态(用 uid 或 token
const token = wx.getStorageSync('token') || wx.getStorageSync('uid');
if (!token) {
// 没登录:先强制断开(如果已连接)
await app.disconnectGoEasy();
// 弹窗提示,强制阅读 2 秒
wx.showToast({
title: '您当前处于未登录状态,消息功能暂不可用',
icon: 'none',
duration: 2000,
mask: true
});
// 等待提示结束,确保用户看到
await new Promise(resolve => setTimeout(resolve, 2100));
return { allowed: false, reason: '未登录' };
}
// 2. 获取当前选中的角色
const role = app.globalData.currentRole || 'normal';
// 3. 根据角色分发到具体鉴权模块
switch (role) {
case 'normal': // 点单老板
return laobanjianquan(app);
case 'dashou': // 打手
return dashoujianquan(app);
// 其他角色暂未细分,统一放行(后续可新增对应文件)
case 'shangjia':
case 'guanshi':
case 'zuzhang':
case 'kaoheguan':
return { allowed: true };
default:
return { allowed: false, reason: '未知角色' };
}
}
module.exports = { jianquanxian };

View File

@@ -0,0 +1,51 @@
// utils/imAuth/laobanjianquan.js
const imRequest = require('./imRequest.js');
/**
* 老板点单用户IM 权限检测
* POST /yonghu/lbhqltqx
* 后端返回 { code: 0, allow: 1 } 或 { code: 0, allow: 0 }
*/
async function laobanjianquan(app) {
const uid = wx.getStorageSync('uid');
if (!uid) return { allowed: false, reason: '未登录' };
try {
const res = await imRequest({
url: '/yonghu/lbhqltqx',
method: 'POST',
data: { uid }
});
if (res && res.data && res.data.code === 0) {
const allow = res.data.allow;
if (allow === 1) {
return { allowed: true };
} else {
await app.disconnectGoEasy();
wx.showToast({
title: '当前身份已不支持使用消息功能',
icon: 'none',
duration: 1000,
mask: true
});
await new Promise(r => setTimeout(r, 1100));
return { allowed: false, reason: '当前身份无消息权限' };
}
}
throw new Error(res.data?.msg || '鉴权接口异常');
} catch (err) {
console.error('老板鉴权失败:', err);
await app.disconnectGoEasy();
wx.showToast({
title: '消息服务暂不可用,请稍后重试',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(r => setTimeout(r, 2100));
return { allowed: false, reason: '鉴权请求失败' };
}
}
module.exports = { laobanjianquan };

74
utils/imageUrl.js Normal file
View File

@@ -0,0 +1,74 @@
// /utils/imageUrl.js - 统一的COS图片路径管理
const app = getApp();
/**
* 获取完整的COS图片URL
* @param {string} path - 图片相对路径,如 'a_long/images/common/arrow-down.png'
* @returns {string} 完整的URL
*/
export function getImageUrl(path) {
if (!app || !app.globalData || !app.globalData.ossImageUrl) {
console.warn('⚠️ App.globalData.ossImageUrl 未初始化');
return path; // 返回原路径作为fallback
}
// 如果已经是完整URL直接返回
if (path.startsWith('http')) {
return path;
}
// 拼接完整URL
return app.globalData.ossImageUrl + path;
}
/**
* 获取常用图片的URL
*/
export const imageUrls = {
// 身份相关
identityIcon: 'a_long/images/identity/identity-icon.png',
arrowDown: 'a_long/images/common/arrow-down.png',
bossIcon: 'a_long/images/identity/boss-icon.png',
dashouIcon: 'a_long/images/identity/dashou-icon.png',
shangjiaIcon: 'a_long/images/identity/shangjia-icon.png',
// 公共图标
actionIcon: 'a_long/images/common/action.png',
emptyMessage: 'a_long/images/common/empty-message.png',
// 通知相关
notificationOn: 'a_long/images/notifications/notification-on.png',
notificationOff: 'a_long/images/notifications/notification-off.png',
// TabBar图标
tabbarHome: 'a_long/images/tabbar/home.png',
tabbarHomeActive: 'a_long/images/tabbar/home-active.png',
tabbarCategory: 'a_long/images/tabbar/category.png',
tabbarCategoryActive: 'a_long/images/tabbar/category-active.png',
tabbarMessage: 'a_long/images/tabbar/message.png',
tabbarMessageActive: 'a_long/images/tabbar/message-active.png',
tabbarProfile: 'a_long/images/tabbar/profile.png',
tabbarProfileActive: 'a_long/images/tabbar/profile-active.png',
// 用户相关
defaultAvatar: 'a_long/images/common/default-avatar.png'
};
/**
* 预加载常用图片
*/
export function preloadCommonImages() {
const images = Object.values(imageUrls).map(path => getImageUrl(path));
images.forEach(src => {
wx.getImageInfo({
src: src,
success: () => {
console.log('✅ 图片预加载成功:', src);
},
fail: (err) => {
console.warn('⚠️ 图片预加载失败:', src, err);
}
});
});
}

152
utils/message-sender.js Normal file
View File

@@ -0,0 +1,152 @@
// utils/message-sender.js
const app = getApp();
import { formatDate } from '../static/lib/utils';
import request from './request.js';
function sendGroupMessage(options) {
const { msgData, onSuccess, onSendStart } = options;
const { type, currentUser, groupId, orderId, isCross, groupName, groupAvatar } = msgData;
const localMsg = buildLocalMessage(msgData);
if (onSendStart) onSendStart(localMsg);
// 只有 isCross 明确为 0 才走前端 SDK其余一律走后端
if (isCross === 0) {
sendViaSDK(localMsg, currentUser, groupId, onSuccess, orderId, isCross, groupName, groupAvatar);
return;
}
sendViaBackend(localMsg, currentUser, groupId, orderId, onSuccess);
}
function buildLocalMessage(data) {
const { type, text, filePath, orderPayload, currentUser, groupId } = data;
const now = Date.now();
const base = {
messageId: `${type}-${now}`,
timestamp: now,
senderId: currentUser.id,
groupId: groupId,
senderData: {
name: currentUser.name,
avatar: currentUser.avatar
},
status: 'sending',
formattedTime: formatDate(now)
};
if (type === 'text') return { ...base, type: 'text', payload: { text } };
if (type === 'image') return { ...base, type: 'image', payload: { url: filePath } };
if (type === 'order') return { ...base, type: 'order', payload: orderPayload };
return base;
}
function sendViaSDK(localMsg, currentUser, groupId, onSuccess, orderId, isCross, groupName, groupAvatar) {
const toData = {
name: groupName || '订单群聊',
avatar: groupAvatar || currentUser.avatar
};
if (orderId) toData.orderId = orderId;
toData.isCross = isCross || 0;
const to = {
type: wx.GoEasy.IM_SCENE.GROUP,
id: groupId,
data: toData
};
let message;
if (localMsg.type === 'text') {
message = wx.goEasy.im.createTextMessage({ text: localMsg.payload.text, to });
} else if (localMsg.type === 'image') {
message = wx.goEasy.im.createImageMessage({ file: localMsg.payload.url, to });
} else if (localMsg.type === 'order') {
message = wx.goEasy.im.createCustomMessage({ type: 'order', payload: localMsg.payload, to });
}
wx.goEasy.im.sendMessage({
message,
onSuccess: () => { if (onSuccess) onSuccess(localMsg.messageId, 'success'); },
onFailed: () => { if (onSuccess) onSuccess(localMsg.messageId, 'failed'); }
});
}
function sendViaBackend(localMsg, currentUser, groupId, orderId, onSuccess) {
const apiUrl = '/dingdan/kptxxfs';
// 🔥 实时获取当前身份,不再依赖外部传入的 currentUser.id
const uid = wx.getStorageSync('uid');
const role = app.globalData.currentRole || 'normal';
const prefixMap = { normal: 'Boss', dashou: 'Ds', shangjia: 'Sj', guanshi: 'Gs', zuzhang: 'Zz' };
const identityType = role === 'dashou' ? 'dashou' : (role === 'shangjia' ? 'shangjia' : 'boss');
const prefix = prefixMap[role] || 'Boss';
const senderId = prefix + uid;
const params = {
orderId: orderId,
groupId: groupId,
identityType: identityType, // 告诉后端当前是什么身份
senderId: senderId,
senderName: currentUser.name || ('用户' + uid),
senderAvatar: currentUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang),
messageType: localMsg.type,
messageId: localMsg.messageId,
timestamp: localMsg.timestamp
};
if (localMsg.type === 'text' || localMsg.type === 'order') {
if (localMsg.type === 'text') {
params.text = localMsg.payload.text;
} else {
params.orderPayload = localMsg.payload;
}
request({
url: apiUrl,
method: 'POST',
data: params
}).then((res) => {
if (res && res.data && res.data.code === 200) {
if (onSuccess) onSuccess(localMsg.messageId, 'success');
} else {
if (onSuccess) onSuccess(localMsg.messageId, 'failed');
}
}).catch(() => {
if (onSuccess) onSuccess(localMsg.messageId, 'failed');
});
return;
}
// 图片消息
if (localMsg.type === 'image') {
const token = wx.getStorageSync('token');
const formData = {
...params,
senderInfo: JSON.stringify({ id: senderId, name: params.senderName, avatar: params.senderAvatar })
};
wx.uploadFile({
url: app.globalData.apiBaseUrl + apiUrl,
filePath: localMsg.payload.url,
name: 'file',
formData: formData,
header: {
'Authorization': 'Bearer ' + (token || '')
},
success(res) {
try {
const data = JSON.parse(res.data);
if (data.code === 200) {
if (onSuccess) onSuccess(localMsg.messageId, 'success');
} else {
if (onSuccess) onSuccess(localMsg.messageId, 'failed');
}
} catch (e) {
if (onSuccess) onSuccess(localMsg.messageId, 'failed');
}
},
fail() {
if (onSuccess) onSuccess(localMsg.messageId, 'failed');
}
});
}
}
module.exports = { sendGroupMessage };

View File

Binary file not shown.

3
utils/models.py Normal file
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

512
utils/oss_utils.py Normal file
View File

@@ -0,0 +1,512 @@
# utils/oss_utils.py
import os
import uuid
from django.conf import settings
from PIL import Image, UnidentifiedImageError
import io
'''try:
from qcloud_cos import CosConfig, CosS3Client
TENCENT_COS_AVAILABLE = True
except ImportError:
TENCENT_COS_AVAILABLE = False
try:
import oss2
ALIYUN_OSS_AVAILABLE = True
except ImportError:
ALIYUN_OSS_AVAILABLE = False
def validate_image(file):
"""
验证图片文件
使用Pillow检查图片是否有效
"""
try:
# 打开图片但不保存到磁盘
img = Image.open(file)
img.verify() # 验证文件完整性
# 重置文件指针因为PIL读取后会移动指针
file.seek(0)
# 检查图片格式
allowed_formats = ['JPEG', 'PNG', 'GIF', 'WEBP', 'BMP']
if img.format not in allowed_formats:
return False, f"不支持{img.format}格式请使用JPEG、PNG、GIF、WEBP或BMP格式"
# 检查图片大小最大5MB
file_size = len(file.read())
file.seek(0)
if file_size > 5 * 1024 * 1024:
return False, "图片大小不能超过5MB"
# 检查图片尺寸最大2000x2000
img = Image.open(file)
file.seek(0)
if img.width > 3000 or img.height > 3000:
return False, "图片尺寸不能超过2000x2000像素"
return True, "图片验证通过"
except UnidentifiedImageError:
return False, "文件不是有效的图片"
except Exception as e:
return False, f"图片验证失败: {str(e)}"
def get_oss_client():
"""
获取OSS客户端
根据配置自动选择云厂商
"""
oss_type = getattr(settings, 'OSS_TYPE', 'tencent') # 默认为腾讯云
if oss_type == 'tencent' and TENCENT_COS_AVAILABLE:
# 腾讯云COS
config = CosConfig(
Region=getattr(settings, 'COS_REGION', 'ap-shanghai'),
SecretId=getattr(settings, 'COS_SECRET_ID', ''),
SecretKey=getattr(settings, 'COS_SECRET_KEY', ''),
Scheme=getattr(settings, 'COS_SCHEME', 'https')
)
return CosS3Client(config)
elif oss_type == 'aliyun' and ALIYUN_OSS_AVAILABLE:
# 阿里云OSS
auth = oss2.Auth(
getattr(settings, 'ALIYUN_ACCESS_KEY_ID', ''),
getattr(settings, 'ALIYUN_ACCESS_KEY_SECRET', '')
)
endpoint = getattr(settings, 'ALIYUN_OSS_ENDPOINT', '')
bucket_name = getattr(settings, 'ALIYUN_OSS_BUCKET', '')
return oss2.Bucket(auth, endpoint, bucket_name)
else:
raise ImportError(f"不支持{oss_type}云存储或相关SDK未安装")
def upload_to_oss(file_obj, file_path):
"""
通用上传文件到OSS
file_obj: Django的UploadedFile对象或类文件对象
file_path: 在OSS中的相对路径'avatar/123/abc.jpg'
返回: 文件访问URL
"""
try:
oss_type = getattr(settings, 'OSS_TYPE', 'tencent')
if oss_type == 'tencent':
return _upload_to_tencent_cos(file_obj, file_path)
elif oss_type == 'aliyun':
return _upload_to_aliyun_oss(file_obj, file_path)
else:
raise ValueError(f"不支持的OSS类型: {oss_type}")
except Exception as e:
print(f"OSS上传失败: {e}")
return None
def _upload_to_tencent_cos(file_obj, file_path):
"""
上传到腾讯云COS
"""
client = get_oss_client()
# 获取文件扩展名
ext = file_path.split('.')[-1].lower() if '.' in file_path else ''
content_type_map = {
'jpg': 'image/jpeg', 'jpeg': 'image/jpeg',
'png': 'image/png', 'gif': 'image/gif',
'webp': 'image/webp', 'bmp': 'image/bmp'
}
content_type = content_type_map.get(ext, 'image/jpeg')
# 如果是Django的UploadedFile需要读取内容
if hasattr(file_obj, 'read'):
file_content = file_obj.read()
else:
file_content = file_obj
# 上传文件
response = client.put_object(
Bucket=getattr(settings, 'COS_BUCKET', ''),
Body=file_content,
Key=file_path,
ContentType=content_type,
ContentDisposition='inline'
)
# 返回完整URL
domain = getattr(settings, 'COS_DOMAIN', '')
return f"{domain}/{file_path}"
def _upload_to_aliyun_oss(file_obj, file_path):
"""
上传到阿里云OSS
"""
bucket = get_oss_client()
# 如果是Django的UploadedFile需要读取内容
if hasattr(file_obj, 'read'):
file_content = file_obj.read()
else:
file_content = file_obj
# 上传文件
result = bucket.put_object(file_path, file_content)
if result.status == 200:
# 阿里云OSS返回的URL
domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '')
return f"{domain}/{file_path}"
else:
return None
def delete_from_oss(file_url):
"""
从OSS删除文件
file_url: 可以是完整URL或相对路径
"""
try:
if not file_url:
return False
oss_type = getattr(settings, 'OSS_TYPE', 'tencent')
# 从URL中提取文件路径
if oss_type == 'tencent':
domain = getattr(settings, 'COS_DOMAIN', '')
# 判断是完整URL还是相对路径
if file_url.startswith('http'):
# 完整URL
if domain not in file_url:
return False # URL不属于我们的域名
file_key = file_url.replace(f"{domain}/", "")
else:
# 相对路径,直接使用
file_key = file_url
return _delete_from_tencent_cos(file_key)
elif oss_type == 'aliyun':
domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '')
if file_url.startswith('http'):
if domain not in file_url:
return False
file_key = file_url.replace(f"{domain}/", "")
else:
file_key = file_url
return _delete_from_aliyun_oss(file_key)
else:
return False
except Exception as e:
print(f"OSS删除失败: {e}")
return False
def _delete_from_tencent_cos(file_key):
"""
从腾讯云COS删除文件
"""
client = get_oss_client()
try:
client.delete_object(
Bucket=getattr(settings, 'COS_BUCKET', ''),
Key=file_key
)
return True
except Exception as e:
print(f"腾讯云COS删除失败: {e}")
return False
def _delete_from_aliyun_oss(file_key):
"""
从阿里云OSS删除文件
"""
bucket = get_oss_client()
try:
bucket.delete_object(file_key)
return True
except Exception as e:
print(f"阿里云OSS删除失败: {e}")
return False'''
# utils/oss_utils.py
import os
import uuid
from django.conf import settings
from PIL import Image, UnidentifiedImageError
import io
try:
from qcloud_cos import CosConfig, CosS3Client
TENCENT_COS_AVAILABLE = True
except ImportError:
TENCENT_COS_AVAILABLE = False
try:
import oss2
ALIYUN_OSS_AVAILABLE = True
except ImportError:
ALIYUN_OSS_AVAILABLE = False
# ========== 通用 MIME 类型映射(扩展支持非图片文件) ==========
MIME_TYPES = {
# 图片
'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png',
'gif': 'image/gif', 'webp': 'image/webp', 'bmp': 'image/bmp',
# JSON / 文本
'json': 'application/json', 'txt': 'text/plain',
# 音频(小程序录音常见格式)
'mp3': 'audio/mpeg', 'aac': 'audio/aac', 'wav': 'audio/wav',
'm4a': 'audio/mp4', 'ogg': 'audio/ogg',
# 视频
'mp4': 'video/mp4', 'webm': 'video/webm',
# 二进制
'bin': 'application/octet-stream',
}
# ====================================================================
def validate_image(file):
"""
验证图片文件
使用Pillow检查图片是否有效
"""
try:
# 打开图片但不保存到磁盘
img = Image.open(file)
img.verify() # 验证文件完整性
# 重置文件指针因为PIL读取后会移动指针
file.seek(0)
# 检查图片格式
allowed_formats = ['JPEG', 'PNG', 'GIF', 'WEBP', 'BMP']
if img.format not in allowed_formats:
return False, f"不支持{img.format}格式请使用JPEG、PNG、GIF、WEBP或BMP格式"
# 检查图片大小最大5MB
file_size = len(file.read())
file.seek(0)
if file_size > 5 * 1024 * 1024:
return False, "图片大小不能超过5MB"
# 检查图片尺寸最大2000x2000
img = Image.open(file)
file.seek(0)
if img.width > 3000 or img.height > 3000:
return False, "图片尺寸不能超过2000x2000像素"
return True, "图片验证通过"
except UnidentifiedImageError:
return False, "文件不是有效的图片"
except Exception as e:
return False, f"图片验证失败: {str(e)}"
def get_oss_client():
"""
获取OSS客户端
根据配置自动选择云厂商
"""
oss_type = getattr(settings, 'OSS_TYPE', 'tencent') # 默认为腾讯云
if oss_type == 'tencent' and TENCENT_COS_AVAILABLE:
# 腾讯云COS
config = CosConfig(
Region=getattr(settings, 'COS_REGION', 'ap-shanghai'),
SecretId=getattr(settings, 'COS_SECRET_ID', ''),
SecretKey=getattr(settings, 'COS_SECRET_KEY', ''),
Scheme=getattr(settings, 'COS_SCHEME', 'https')
)
return CosS3Client(config)
elif oss_type == 'aliyun' and ALIYUN_OSS_AVAILABLE:
# 阿里云OSS
auth = oss2.Auth(
getattr(settings, 'ALIYUN_ACCESS_KEY_ID', ''),
getattr(settings, 'ALIYUN_ACCESS_KEY_SECRET', '')
)
endpoint = getattr(settings, 'ALIYUN_OSS_ENDPOINT', '')
bucket_name = getattr(settings, 'ALIYUN_OSS_BUCKET', '')
return oss2.Bucket(auth, endpoint, bucket_name)
else:
raise ImportError(f"不支持{oss_type}云存储或相关SDK未安装")
def upload_to_oss(file_obj, file_path, content_type=None):
"""
通用上传文件到OSS
file_obj: Django的UploadedFile对象或类文件对象
file_path: 在OSS中的相对路径'avatar/123/abc.jpg'
content_type: 可选MIME类型若不传则根据文件扩展名自动推断
返回: 文件访问URL
"""
try:
oss_type = getattr(settings, 'OSS_TYPE', 'tencent')
if oss_type == 'tencent':
return _upload_to_tencent_cos(file_obj, file_path, content_type)
elif oss_type == 'aliyun':
return _upload_to_aliyun_oss(file_obj, file_path)
else:
raise ValueError(f"不支持的OSS类型: {oss_type}")
except Exception as e:
print(f"OSS上传失败: {e}")
return None
def _upload_to_tencent_cos(file_obj, file_path, content_type=None):
"""
上传到腾讯云COS扩展支持任意文件类型
"""
client = get_oss_client()
# 若调用方未指定 Content-Type则根据扩展名自动推断
if not content_type:
ext = file_path.rsplit('.', 1)[-1].lower() if '.' in file_path else ''
content_type = MIME_TYPES.get(ext, 'application/octet-stream')
# 读取文件内容(兼容 Django UploadedFile 和普通 bytes
if hasattr(file_obj, 'read'):
file_content = file_obj.read()
else:
file_content = file_obj
# 上传文件
response = client.put_object(
Bucket=getattr(settings, 'COS_BUCKET', ''),
Body=file_content,
Key=file_path,
ContentType=content_type,
ContentDisposition='inline'
)
# 返回完整URL
domain = getattr(settings, 'COS_DOMAIN', '')
return f"{domain.rstrip('/')}/{file_path.lstrip('/')}"
def _upload_to_aliyun_oss(file_obj, file_path):
"""
上传到阿里云OSS保持原样未修改
"""
bucket = get_oss_client()
# 如果是Django的UploadedFile需要读取内容
if hasattr(file_obj, 'read'):
file_content = file_obj.read()
else:
file_content = file_obj
# 上传文件
result = bucket.put_object(file_path, file_content)
if result.status == 200:
# 阿里云OSS返回的URL
domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '')
return f"{domain}/{file_path}"
else:
return None
def delete_from_oss(file_url):
"""
从OSS删除文件
file_url: 可以是完整URL或相对路径
"""
try:
if not file_url:
return False
oss_type = getattr(settings, 'OSS_TYPE', 'tencent')
# 从URL中提取文件路径
if oss_type == 'tencent':
domain = getattr(settings, 'COS_DOMAIN', '')
# 判断是完整URL还是相对路径
if file_url.startswith('http'):
# 完整URL
if domain not in file_url:
return False # URL不属于我们的域名
file_key = file_url.replace(f"{domain}/", "")
else:
# 相对路径,直接使用
file_key = file_url
return _delete_from_tencent_cos(file_key)
elif oss_type == 'aliyun':
domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '')
if file_url.startswith('http'):
if domain not in file_url:
return False
file_key = file_url.replace(f"{domain}/", "")
else:
file_key = file_url
return _delete_from_aliyun_oss(file_key)
else:
return False
except Exception as e:
print(f"OSS删除失败: {e}")
return False
def _delete_from_tencent_cos(file_key):
"""
从腾讯云COS删除文件
"""
client = get_oss_client()
try:
client.delete_object(
Bucket=getattr(settings, 'COS_BUCKET', ''),
Key=file_key
)
return True
except Exception as e:
print(f"腾讯云COS删除失败: {e}")
return False
def _delete_from_aliyun_oss(file_key):
"""
从阿里云OSS删除文件
"""
bucket = get_oss_client()
try:
bucket.delete_object(file_key)
return True
except Exception as e:
print(f"阿里云OSS删除失败: {e}")
return False

165
utils/phone-auth.js Normal file
View File

@@ -0,0 +1,165 @@
/**
* 手机号强制认证 - 完全自包含逻辑模块
*
* 特点:
* 1. 不引用项目中的 request.js / upload.js避免其顶层 getApp() 带来的加载顺序错误
* 2. 所有请求直接使用 wx.request / wx.uploadFile基础 URL 写死(与 app.globalData.apiBaseUrl 保持一致)
* 3. check() 内部捕获所有异常,永远返回 false 或 true不会抛出错误
* 4. submit() 抛出错误由认证页面自己处理,不影响主流程
*
* 用法:
* app.js 中:
* try {
* const { check } = require('./utils/phone-auth');
* if (await check()) { wx.reLaunch(...); return; }
* } catch(e) {}
*
* 认证页面中:
* const { submit } = require('../../utils/phone-auth');
* await submit(phoneCode, avatarPath);
*/
// ==================== 基础配置 ====================
const API_BASE = 'https://6ccy.top/hqhd'; // 与你 app.js 里的 apiBaseUrl 一致
const AUTH_CHECK_URL = '/peizhi/yhbdsjh'; // 检查是否需要认证
const AUTH_SUBMIT_URL = '/yonghu/xiugai'; // 提交认证(复用修改信息接口)
// ==================== 内部封装wx.request ====================
function innerRequest(url, data = {}) {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token'); // 运行时获取 token不提前取
const header = {
'Content-Type': 'application/json'
};
if (token) {
header['Authorization'] = 'Bearer ' + token;
}
wx.request({
url: API_BASE + url,
method: 'POST',
data,
header,
success: (res) => {
// 正常响应直接 resolve 整个响应对象,让上层自己解析
resolve(res);
},
fail: (err) => {
reject(err);
}
});
});
}
// ==================== 内部封装wx.uploadFile ====================
function innerUpload(url, filePath, formData = {}, fileName = 'avatar') {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token');
if (!token) {
reject(new Error('未登录'));
return;
}
// 先验证文件是否存在
wx.getFileInfo({
filePath,
success: () => {
wx.uploadFile({
url: API_BASE + url,
filePath,
name: fileName,
formData,
header: {
'Authorization': 'Bearer ' + token
},
success: (res) => {
// ✅ 修复wx.uploadFile 成功时 statusCode 为 200
if (res.statusCode === 200) {
try {
const data = JSON.parse(res.data);
resolve(data); // 直接返回解析后的对象,方便下游使用
} catch (e) {
reject(new Error('解析响应数据失败'));
}
} else if (res.statusCode === 401) {
wx.removeStorageSync('token');
reject(new Error('认证失败'));
} else {
reject(new Error(`上传失败,状态码:${res.statusCode}`));
}
},
fail: (err) => {
reject(new Error('网络请求失败'));
}
});
},
fail: () => {
reject(new Error('文件不存在或无法访问'));
}
});
});
}
// ==================== 公开方法 ====================
/**
* 检查是否需要手机号认证
* - 无 token 或 任何异常 → 返回 false
* - 后端明确 need_auth: true → 返回 true
*/
async function check() {
const token = wx.getStorageSync('token');
if (!token) return false;
try {
const res = await innerRequest(AUTH_CHECK_URL, {});
// 假设后端返回格式:{ data: { need_auth: true/false } } 或直接在顶层
const body = res.data;
return body && body.need_auth === true;
} catch (e) {
// 任何错误(网络、后端异常)都吞掉,不影响主流程
return false;
}
}
/**
* 提交认证(手机号 code + 头像文件)
* @param {string} phoneCode 微信 getPhoneNumber 返回的 code
* @param {string} avatarPath 头像临时路径
* @returns {Promise<object>} 后端返回的完整数据
*/
async function submit(phoneCode, avatarPath) {
if (!phoneCode || !avatarPath) {
throw new Error('缺少参数');
}
// 将手机号 code 放入 formData
const formData = {
shoujihao_code: phoneCode
};
// 调用上传
const data = await innerUpload(AUTH_SUBMIT_URL, avatarPath, formData);
// ✅ 修复:正确判断后端返回的 code 字段,后端成功时 code === 0
if (data && data.code === 0) {
// data.data 包含 { touxiang: '相对路径', phone: '...' }
const respData = data.data || data;
// 存储手机号
if (respData.phone) {
wx.setStorageSync('phone', respData.phone);
}
// ✅ 修复:存储头像相对路径,与 xiugai 页面处理方式一致
if (respData.touxiang) {
wx.setStorageSync('touxiang', respData.touxiang);
}
return respData;
} else {
throw new Error((data && data.msg) || '认证失败');
}
}
module.exports = { check, submit };

18
utils/redis_lock.py Normal file
View File

@@ -0,0 +1,18 @@
import redis
from django.conf import settings
redis_client = redis.Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
password=settings.REDIS_PASSWORD,
db=2,
decode_responses=True
)
def acquire_lock(lock_key, timeout=10):
"""获取锁,返回是否成功"""
return redis_client.set(lock_key, '1', nx=True, ex=timeout)
def release_lock(lock_key):
"""释放锁"""
redis_client.delete(lock_key)

66
utils/request.js Normal file
View File

@@ -0,0 +1,66 @@
// utils/request.js
const app = getApp();
function request(options) {
const token = wx.getStorageSync('token');
const header = {
'Content-Type': 'application/json',
...options.header,
};
if (token) {
header['Authorization'] = 'Bearer ' + token;
}
return new Promise((resolve, reject) => {
wx.request({
url: app.globalData.apiBaseUrl + options.url,
method: options.method,
data: options.data || {},
header,
success: async (res) => {
if (res.statusCode === 401 || res.statusCode === 403) {
// 如果当前正在执行静默登录,等待登录完成后重试一次
if (app.globalData.loginPromise) {
try {
await app.globalData.loginPromise; // 等待登录成功
// 登录成功后重新发起原请求,并将结果返回给调用方
const retryRes = await request(options);
resolve(retryRes);
} catch (err) {
// 登录失败,按原逻辑弹窗
wx.showModal({
content: '操作失败,请先登录',
success: (modalRes) => {
if (modalRes.confirm) {
wx.removeStorageSync('token');
wx.reLaunch({ url: '/pages/wode/wode' });
}
},
});
resolve(res); // 返回原始响应让调用方知晓
}
return;
}
// 无登录流程进行中,直接弹窗(原逻辑)
wx.showModal({
content: '操作失败,请先登录',
success: (modalRes) => {
if (modalRes.confirm) {
wx.removeStorageSync('token');
wx.reLaunch({ url: '/pages/wode/wode' });
}
},
});
resolve(res);
return;
}
// 正常响应
resolve(res);
},
fail: reject,
});
});
}
export default request;

0
utils/role-manager.js Normal file
View File

3
utils/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

95
utils/upload.js Normal file
View File

@@ -0,0 +1,95 @@
// utils/upload.js
import request from './request.js'
const app = getApp()
/**
* 统一的异步上传方法
* @param {object} options 配置项
* @returns {Promise} Promise对象
*/
const upload = async (options) => {
const {
url,
formData = {},
filePath = null,
fileName = 'file'
} = options
// 获取token
const token = wx.getStorageSync('token')
if (!token) {
throw new Error('未登录')
}
// 拼接完整URL
const fullUrl = `${app.globalData.apiBaseUrl}${url}`
if (filePath) {
// 有文件上传使用wx.uploadFile
return new Promise((resolve, reject) => {
// 先验证文件是否存在
wx.getFileInfo({
filePath,
success: () => {
// 文件存在,开始上传
wx.uploadFile({
url: fullUrl,
filePath,
name: fileName,
formData,
header: {
'Authorization': `Bearer ${token}`
},
success: (res) => {
if (res.statusCode === 200) {
try {
const data = JSON.parse(res.data)
resolve({
statusCode: res.statusCode,
data
})
} catch (e) {
console.error('解析JSON失败:', e, '原始数据:', res.data)
reject(new Error('解析响应数据失败'))
}
} else if (res.statusCode === 401) {
// token过期或无效
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
reject(new Error('认证失败'))
} else {
console.error('上传失败,状态码:', res.statusCode, '响应:', res)
reject(new Error(`上传失败,状态码:${res.statusCode}`))
}
},
fail: (err) => {
console.error('wx.uploadFile失败:', err)
reject(new Error('网络请求失败'))
}
})
},
fail: (err) => {
console.error('文件不存在或无法访问:', err)
reject(new Error('文件不存在或无法访问'))
}
})
})
} else {
// 没有文件使用封装的request
// 注意这里应该使用传入的url而不是硬编码
return request({
url, // 使用传入的url
method: 'POST',
data: formData,
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
})
}
}
export default upload

3
utils/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

207
utils/wechat_v3.py Normal file
View File

@@ -0,0 +1,207 @@
# utils/wechat_v3.py
'''import time
import hashlib
import base64
import json
from urllib.parse import urlparse
import rsa
from Crypto.Cipher import AES
from django.conf import settings
def load_private_key():
"""加载商户私钥PEM格式"""
with open(settings.WECHAT_PAY_V3_CONFIG['PRIVATE_KEY_PATH'], 'rb') as f:
key_data = f.read()
return rsa.PrivateKey.load_pkcs1(key_data)
def build_authorization(method, url, body):
"""
生成V3接口的Authorization头
:param method: GET/POST
:param url: 完整URL含域名和路径
:param body: 请求体JSON字符串
:return: Authorization字符串
"""
private_key = load_private_key()
mchid = settings.WECHAT_PAY_V3_CONFIG['MCHID']
serial_no = settings.WECHAT_PAY_V3_CONFIG['CERT_SERIAL_NO']
# 提取URL路径不含query
parsed = urlparse(url)
path = parsed.path
if parsed.query:
path += '?' + parsed.query
timestamp = str(int(time.time()))
nonce = hashlib.md5((timestamp + 'wechat').encode()).hexdigest()[:32]
# 构造签名串
message = '\n'.join([method, path, timestamp, nonce, body]) + '\n'
# 使用私钥签名SHA256
signature = rsa.sign(message.encode('utf-8'), private_key, 'SHA-256')
signature_b64 = base64.b64encode(signature).decode('utf-8')
# 组装Authorization
auth = f'WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature_b64}"'
return auth
def verify_wechat_sign(headers, body):
"""
验证微信回调的签名
需要提前从微信平台下载证书公钥,存放在 PLATFORM_CERT_DIR 下,文件名 = 序列号.pem
:param headers: 请求头
:param body: 原始请求体字符串
:return: bool
"""
serial = headers.get('Wechatpay-Serial')
signature = headers.get('Wechatpay-Signature')
timestamp = headers.get('Wechatpay-Timestamp')
nonce = headers.get('Wechatpay-Nonce')
if not all([serial, signature, timestamp, nonce]):
return False
# 构造验签名串
message = '\n'.join([timestamp, nonce, body]) + '\n'
# 读取对应序列号的平台证书公钥
cert_path = f"{settings.WECHAT_PAY_V3_CONFIG['PLATFORM_CERT_DIR']}/{serial}.pem"
try:
with open(cert_path, 'rb') as f:
pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(f.read())
except FileNotFoundError:
# 如果证书不存在,可尝试下载(生产环境建议定时下载)
return False
signature_bytes = base64.b64decode(signature)
try:
rsa.verify(message.encode('utf-8'), signature_bytes, pub_key)
return True
except rsa.VerificationError:
return False
def decrypt_callback_ciphertext(associated_data, nonce, ciphertext):
"""
使用APIv3密钥解密回调中的加密数据
:param associated_data: 附加数据
:param nonce: 随机串
:param ciphertext: 密文Base64
:return: 解密后的JSON字符串
"""
api_v3_key = settings.WECHAT_PAY_V3_CONFIG['API_V3_KEY'].encode('utf-8')
nonce_bytes = nonce.encode('utf-8')
ciphertext_bytes = base64.b64decode(ciphertext)
# AES-GCM解密
cipher = AES.new(api_v3_key, AES.MODE_GCM, nonce=nonce_bytes)
if associated_data:
cipher.update(associated_data.encode('utf-8'))
plaintext = cipher.decrypt_and_verify(ciphertext_bytes[:-16], ciphertext_bytes[-16:])
return plaintext.decode('utf-8')'''
# utils/wechat_v3.py
import time
import hashlib
import base64
import json
from urllib.parse import urlparse
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
from Crypto.Cipher import AES
from django.conf import settings
import os
def load_private_key():
"""加载私钥(支持 PKCS#1 和 PKCS#8"""
key_path = settings.WECHAT_PAY_V3_CONFIG['PRIVATE_KEY_PATH']
with open(key_path, 'rb') as f:
key_data = f.read()
# cryptography 自动识别格式
private_key = serialization.load_pem_private_key(
key_data,
password=None,
backend=default_backend()
)
return private_key
def build_authorization(method, url, body):
"""生成V3接口的Authorization头"""
private_key = load_private_key()
mchid = settings.WECHAT_PAY_V3_CONFIG['MCHID']
serial_no = settings.WECHAT_PAY_V3_CONFIG['CERT_SERIAL_NO']
parsed = urlparse(url)
path = parsed.path
if parsed.query:
path += '?' + parsed.query
timestamp = str(int(time.time()))
nonce = hashlib.md5((timestamp + 'wechat').encode()).hexdigest()[:32]
# 构造签名串
message = '\n'.join([method, path, timestamp, nonce, body]) + '\n'
# 使用 cryptography 签名
signature = private_key.sign(
message.encode('utf-8'),
padding.PKCS1v15(),
hashes.SHA256()
)
signature_b64 = base64.b64encode(signature).decode('utf-8')
auth = f'WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature_b64}"'
return auth
def verify_wechat_sign(headers, body):
"""验证微信回调签名(需提前下载平台证书)"""
# 这部分使用 rsa 库验签,也可以改用 cryptography但 rsa 已足够
import rsa
serial = headers.get('Wechatpay-Serial')
signature = headers.get('Wechatpay-Signature')
timestamp = headers.get('Wechatpay-Timestamp')
nonce = headers.get('Wechatpay-Nonce')
if not all([serial, signature, timestamp, nonce]):
return False
message = '\n'.join([timestamp, nonce, body]) + '\n'
cert_path = f"{settings.WECHAT_PAY_V3_CONFIG['PLATFORM_CERT_DIR']}/{serial}.pem"
if not os.path.exists(cert_path):
return False
with open(cert_path, 'rb') as f:
pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(f.read())
signature_bytes = base64.b64decode(signature)
try:
rsa.verify(message.encode('utf-8'), signature_bytes, pub_key)
return True
except rsa.VerificationError:
return False
def decrypt_callback_ciphertext(associated_data, nonce, ciphertext):
"""解密回调数据(使用 AES-GCM"""
api_v3_key = settings.WECHAT_PAY_V3_CONFIG['API_V3_KEY'].encode('utf-8')
nonce_bytes = nonce.encode('utf-8')
ciphertext_bytes = base64.b64decode(ciphertext)
cipher = AES.new(api_v3_key, AES.MODE_GCM, nonce=nonce_bytes)
if associated_data:
cipher.update(associated_data.encode('utf-8'))
plaintext = cipher.decrypt_and_verify(ciphertext_bytes[:-16], ciphertext_bytes[-16:])
return plaintext.decode('utf-8')

201
utils/weixin_broadcast.py Normal file
View File

@@ -0,0 +1,201 @@
# utils/weixin_broadcast.py - 强调试版本
import time
import logging
import json # 新增,用于格式化打印
import requests
from django.core.cache import cache
from django.conf import settings
from yonghu.models import OfficialAccountUser
logger = logging.getLogger('weixin_broadcast')
class WeixinBroadcastSender:
"""
微信模板消息广播发送器 - 调试版
"""
def __init__(self):
self.appid = getattr(settings, 'WEIXIN_OFFICIAL_APPID', '')
self.secret = getattr(settings, 'WEIXIN_OFFICIAL_SECRET', '')
self.template_id = getattr(settings, 'WEIXIN_TEMPLATE_ID', '')
# 发送控制参数
self.batch_size = 100
self.delay_between_batches = 1.5
def get_access_token(self):
"""获取服务号access_token"""
cache_key = f'weixin_official_token_{self.appid}'
token = cache.get(cache_key)
if token:
return token
url = 'https://api.weixin.qq.com/cgi-bin/token'
params = {'grant_type': 'client_credential', 'appid': self.appid, 'secret': self.secret}
try:
response = requests.get(url, params=params, timeout=10)
result = response.json()
if 'access_token' in result:
token = result['access_token']
expires_in = result.get('expires_in', 7200) - 300
cache.set(cache_key, token, expires_in)
logger.info("✅ 获取access_token成功")
return token
else:
logger.error(f"❌ 获取access_token失败: {result}")
return None
except Exception as e:
logger.error(f"❌ 获取access_token异常: {str(e)}")
return None
def build_order_message_data(self, order_info):
"""
构建订单消息数据 - 【调试关键点1】
打印传入的 order_info 和构建出的最终数据
"""
# 🟡 调试点1打印传入的参数
logger.info(
f"🟡【调试-输入】build_order_message_data 收到的 order_info: {json.dumps(order_info, ensure_ascii=False, indent=2)}")
# 这是你现在的构建逻辑,我们原样保留但加上调试
# 注意这里保持你目前的字段名short_thing5, short_thing4, thing15, amount8
data = {
"short_thing5": {
"value": order_info.get('order_type', '平台发单'),
"color": "#173177"
},
"short_thing4": {
"value": order_info.get('game_type', '游戏订单'),
"color": "#173177"
},
"thing15": {
"value": str(order_info.get('order_desc', '新订单'))[:20],
"color": "#173177"
},
"amount8": {
"value": f"{order_info.get('amount', '0')}",
"color": "#173177"
}
}
# 🟡 调试点2打印构建出的最终数据结构
logger.info(f"🟡【调试-输出】构建完成的模板数据 data 字段:")
for key, content in data.items():
logger.info(f" 字段名: '{key}' -> 值: '{content.get('value', '')}'")
logger.info(f"🟡【调试-输出】完整 data 结构: {json.dumps(data, ensure_ascii=False)}")
# 🔴 关键检查:确认 short_thing5 是否存在,值是否为空
if 'short_thing5' not in data:
logger.error("❌【致命错误】构建的数据中根本没有 'short_thing5' 这个字段!")
else:
st5_value = data['short_thing5'].get('value', '')
if not st5_value:
logger.error(f"❌【致命错误】'short_thing5' 字段的值为空!")
else:
logger.info(f"'short_thing5' 字段存在,值为: '{st5_value}'")
return data
def send_to_user(self, official_openid, template_data, order_id=None):
"""发送模板消息给单个用户 - 【调试关键点2】"""
# 🟡 调试点3打印每次发送前的数据
logger.info(f"🟡【调试-发送前】准备发送给 {official_openid[:10]}... 的数据:")
logger.info(f" 模板ID: {self.template_id}")
logger.info(
f" pagepath: pages/dingdan-xiangqing/dingdan-xiangqing?id={order_id}" if order_id else " 无pagepath")
for key, content in template_data.items():
logger.info(f" 字段 '{key}': '{content.get('value', '')}'")
access_token = self.get_access_token()
if not access_token:
return {'success': False, 'msg': '获取access_token失败', 'retry': True}
request_data = {
"touser": official_openid,
"template_id": self.template_id,
"data": template_data
}
if order_id and hasattr(settings, 'WEIXIN_APPID'):
request_data["miniprogram"] = {
"appid": settings.WEIXIN_APPID,
"pagepath": f"pages/jiedan/jiedan?id={order_id}"
}
send_url = f'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={access_token}'
try:
response = requests.post(send_url, json=request_data, timeout=10)
result = response.json()
logger.info(f"🟡【调试-微信响应】{official_openid[:10]}... 的发送结果: {result}")
if result.get('errcode') == 0:
return {'success': True, 'msg': '发送成功', 'msgid': result.get('msgid')}
else:
# 如果是未关注或拒收,更新本地状态
if result.get('errcode') in [43004, 43101]:
try:
OfficialAccountUser.objects.filter(official_openid=official_openid).update(is_subscribed=False)
except:
pass
return {'success': False, 'msg': f"[{result.get('errcode')}]{result.get('errmsg')}", 'retry': False}
except Exception as e:
logger.error(f"❌ 发送给用户异常: {official_openid[:10]}..., 错误: {str(e)}")
return {'success': False, 'msg': f'请求异常: {str(e)}', 'retry': True}
def broadcast_order(self, order_info):
"""广播订单消息给所有关注用户 - 【调试入口】"""
logger.info(f"🚀【开始广播】订单: {order_info.get('dingdan_id')}")
# 1. 获取所有关注用户
try:
subscribed_users = OfficialAccountUser.objects.filter(is_subscribed=True).values_list('official_openid',
flat=True)
total_users = subscribed_users.count()
logger.info(f"📊【广播统计】将从数据库查询到 {total_users} 个订阅用户")
if total_users == 0:
return {'success': True, 'msg': '无订阅用户', 'total': 0}
except Exception as e:
logger.error(f"❌ 获取订阅用户列表失败: {str(e)}")
return {'success': False, 'msg': f'获取用户失败: {str(e)}'}
# 2. 构建模板数据(这里会触发上面的调试打印)
template_data = self.build_order_message_data(order_info)
order_id = order_info.get('dingdan_id')
# 3. 分批发送
success_count = 0
fail_count = 0
current_batch = 0
openid_list = list(subscribed_users)
logger.info(f"🟡【调试-广播循环】开始遍历 {len(openid_list)} 个用户进行发送...")
for i, openid in enumerate(openid_list):
try:
result = self.send_to_user(openid, template_data, order_id)
if result['success']:
success_count += 1
else:
fail_count += 1
if fail_count <= 5: # 只打印前几个失败原因
logger.warning(f"⚠️ 发送失败 {openid[:10]}...: {result.get('msg')}")
# 分批控制
current_batch += 1
if current_batch >= self.batch_size:
time.sleep(self.delay_between_batches)
current_batch = 0
except Exception as e:
fail_count += 1
logger.error(f"❌ 处理用户 {openid[:10]}... 异常: {str(e)}")
logger.info(f"🏁【广播完成】订单 {order_id},总计: {total_users},成功: {success_count},失败: {fail_count}")
return {
'success': True,
'msg': f'广播完成: 成功{success_count}, 失败{fail_count}',
'total': total_users,
'success_count': success_count,
'fail_count': fail_count
}

214
utils/xiaoxilj.js Normal file
View File

@@ -0,0 +1,214 @@
/**
* 连接管理模块 - xiaoxilj.js (根治发送失败:跳转前确保群组已订阅)
* 从详情页跳转订单群聊时,提前订阅群组,避免发送消息时订阅未完成而失败
*/
const app = getApp();
function waitForConnection(expectedUserId, timeout = 10000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
app.off('connectionChanged', handler);
reject(new Error('连接超时'));
}, timeout);
const handler = (event) => {
if (event.status === 'connected') {
if (event.userId === expectedUserId) {
clearTimeout(timer);
app.off('connectionChanged', handler);
resolve();
} else {
console.error(`连接身份不匹配: 期望 ${expectedUserId}, 实际 ${event.userId}`);
wx.removeStorageSync('savedGoEasyConnection');
wx.removeStorageSync('goEasyUserId');
wx.removeStorageSync('currentGoEasyIdentity');
if (app.clearSavedConnection) app.clearSavedConnection();
clearTimeout(timer);
app.off('connectionChanged', handler);
reject(new Error(`连接身份不匹配: 期望 ${expectedUserId}, 实际 ${event.userId}`));
}
} else if (event.status === 'disconnected' && !event.manual) {
wx.removeStorageSync('savedGoEasyConnection');
wx.removeStorageSync('goEasyUserId');
wx.removeStorageSync('currentGoEasyIdentity');
if (app.clearSavedConnection) app.clearSavedConnection();
clearTimeout(timer);
app.off('connectionChanged', handler);
reject(new Error('连接失败'));
}
};
app.on('connectionChanged', handler);
});
}
class ConnectionManager {
// 原有的私聊方法保持不变
async connectAndChat(params) {
const { identityType, userId, targetUser } = params;
if (!identityType || !userId || !targetUser || !targetUser.id) {
throw new Error('参数不完整');
}
if (identityType === 'dashou' && !userId.startsWith('Ds')) {
throw new Error(`打手身份的用户ID必须以Ds开头实际为 ${userId}`);
}
if (identityType === 'shangjia' && !userId.startsWith('Sj')) {
throw new Error(`商家身份的用户ID必须以Sj开头实际为 ${userId}`);
}
if (identityType === 'boss' && !userId.startsWith('Boss')) {
throw new Error(`老板身份的用户ID必须以Boss开头实际为 ${userId}`);
}
wx.removeStorageSync('savedGoEasyConnection');
wx.removeStorageSync('goEasyUserId');
wx.removeStorageSync('currentGoEasyIdentity');
if (app.clearSavedConnection) app.clearSavedConnection();
if (wx.goEasy && wx.goEasy.getConnectionStatus() === 'connected') {
wx.goEasy.disconnect();
}
const waitPromise = waitForConnection(userId);
app.connectWithIdentity(identityType, userId, false);
try {
await waitPromise;
const currentUser = {
id: userId,
name: app.globalData.currentUser?.name || '用户',
avatar: app.globalData.currentUser?.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang)
};
const to = {
id: targetUser.id,
name: targetUser.name || '用户',
avatar: targetUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang)
};
const param = { to, currentUser };
const path = '/pages/liaotian/liaotian?data=' + encodeURIComponent(JSON.stringify(param));
wx.navigateTo({ url: path });
} catch (err) {
console.error('连接失败:', err);
wx.showToast({ title: err.message || '连接失败', icon: 'none' });
throw err;
}
}
async connectToGroupChat(params) {
const { identityType, userId, orderId, groupName, groupAvatar, isCross } = params;
if (!identityType || !userId || !orderId) {
throw new Error('参数不完整identityType, userId, orderId 必填');
}
// 校验 userId 前缀
if (identityType === 'dashou' && !userId.startsWith('Ds')) {
throw new Error(`打手身份的用户ID必须以Ds开头实际为 ${userId}`);
}
if (identityType === 'shangjia' && !userId.startsWith('Sj')) {
throw new Error(`商家身份的用户ID必须以Sj开头实际为 ${userId}`);
}
if (identityType === 'boss' && !userId.startsWith('Boss')) {
throw new Error(`老板身份的用户ID必须以Boss开头实际为 ${userId}`);
}
// 1. 同步全局角色,确保群聊页初始化正确
app.globalData.currentRole = identityType;
wx.setStorageSync('currentRole', identityType);
const uid = userId.replace(/^(Ds|Sj|Boss|Gs|Zz)/, '');
const avatar = wx.getStorageSync('touxiang') ||
(app.globalData.ossImageUrl + app.globalData.morentouxiang);
app.globalData.currentUser = {
id: userId,
name: app.globalData.currentUser?.name || `用户${uid}`,
avatar: avatar
};
// 2. 确保 GoEasy 已使用目标身份连接
let needConnect = false;
if (!wx.goEasy || !wx.goEasy.getConnectionStatus || wx.goEasy.getConnectionStatus() !== 'connected') {
needConnect = true;
} else {
const currentUserId = wx.goEasy.im?.userId;
if (currentUserId !== userId) {
needConnect = true;
}
}
if (needConnect) {
wx.removeStorageSync('savedGoEasyConnection');
wx.removeStorageSync('goEasyUserId');
if (app.clearSavedConnection) app.clearSavedConnection();
if (wx.goEasy && wx.goEasy.getConnectionStatus() === 'connected') {
wx.goEasy.disconnect();
}
const waitPromise = waitForConnection(userId);
app.connectWithIdentity(identityType, userId, false);
try {
await waitPromise;
} catch (err) {
wx.showToast({ title: '连接失败', icon: 'none' });
throw err;
}
}
// 3. 从会话列表查找真实群组ID
let realGroupId = null;
try {
const result = await new Promise((resolve, reject) => {
wx.goEasy.im.latestConversations({
onSuccess: (res) => {
const conversations = res.content?.conversations || [];
const found = conversations.find(c => c.type === 'group' && c.data && c.data.orderId === orderId);
resolve(found ? found.groupId : null);
},
onFailed: reject
});
});
realGroupId = result;
} catch (err) {
console.error('获取会话列表失败:', err);
wx.showToast({ title: '获取群聊信息失败', icon: 'none' });
throw err;
}
if (!realGroupId) {
wx.showToast({ title: '暂未创建群聊', icon: 'none' });
return;
}
// 4. 🔥 关键:主动订阅群组并等待成功,确保发送消息时订阅已完成
try {
await new Promise((resolve, reject) => {
wx.goEasy.im.subscribeGroup({
groupIds: [realGroupId],
onSuccess: () => {
resolve();
},
onFailed: (error) => {
console.error('订阅群组失败:', error);
reject(error);
}
});
});
} catch (err) {
wx.showToast({ title: '订阅群组失败', icon: 'none' });
throw err;
}
// 5. 跳转群聊页(参数与消息列表完全一致)
const param = {
groupId: realGroupId,
orderId: orderId,
groupName: groupName || '订单群聊',
groupAvatar: groupAvatar || '',
isCross: isCross || 0
};
const path = '/pages/qunliaotian/qunliaotian?data=' + encodeURIComponent(JSON.stringify(param));
wx.navigateTo({ url: path });
}
}
export default new ConnectionManager();

89
utils/yaoqingma_utils.py Normal file
View File

@@ -0,0 +1,89 @@
"""
阿龙电竞陪玩平台 - 邀请码生成工具
生成规则:毫秒时间戳 + 用户ID + 随机数 -> Base62编码 -> 固定20位邀请码
特点:唯一性高、生成快、无序不可读
"""
import time
import random
# Base62 编码字符集 (数字+小写字母+大写字母)
_BASE62_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
_BASE62_LENGTH = 62
def base62_encode(num):
"""将数字转换为Base62字符串"""
if num == 0:
return _BASE62_CHARS[0]
result = []
while num > 0:
num, remainder = divmod(num, _BASE62_LENGTH)
result.append(_BASE62_CHARS[remainder])
# 反转结果字符串
return ''.join(reversed(result))
def chuangjianYaoqingma(yonghuid_str):
"""
创建唯一邀请码 (20位固定长度)
参数:
yonghuid_str: 用户ID字符串"123456"
返回:
20位的Base62编码邀请码字符串
"""
# 1. 获取当前毫秒时间戳 (13位)
timestamp_ms = int(time.time() * 1000)
# 2. 将用户ID字符串转换为数字 (确保处理字符串)
try:
# 先尝试直接转换整数
yonghuid_num = int(yonghuid_str)
except ValueError:
# 如果用户ID不是纯数字使用哈希值
yonghuid_num = abs(hash(yonghuid_str)) % (10 ** 8) # 取8位数字
# 3. 生成一个随机数 (0-9999)
random_salt = random.randint(0, 9999)
# 4. 组合数字: 时间戳(13位) + 用户ID数字(最多8位) + 随机数(4位)
# 示例: 1647850123456 + 123456 + 7890 = 16478501234561234567890
combined_num = int(f"{timestamp_ms}{yonghuid_num:08d}{random_salt:04d}")
# 5. Base62编码
encoded_str = base62_encode(combined_num)
# 6. 确保固定20位长度: 不足补0超过取后20位
# 理论上组合数字非常大编码后通常会超过20位
if len(encoded_str) < 20:
# 左补0 (理论上不会发生,但保持安全)
encoded_str = encoded_str.rjust(20, _BASE62_CHARS[0])
else:
# 取后20位 (更随机)
encoded_str = encoded_str[-20:]
return encoded_str
def yanzhengYaoqingmaWeiyixing(yaoqingma):
"""
验证邀请码是否有效 (简单的格式验证)
参数:
yaoqingma: 待验证的邀请码
返回:
bool: 邀请码格式是否有效
"""
if not yaoqingma or len(yaoqingma) != 20:
return False
# 检查是否只包含Base62字符
for char in yaoqingma:
if char not in _BASE62_CHARS:
return False
return True