diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts index 557ae0c9..11998fe6 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts @@ -5,6 +5,20 @@ export function getWechatAccountDetail(id: string) { return request("/v1/wechats/getWechatInfo", { wechatId: id }, "GET"); } +// 获取微信号概览数据 +export function getWechatAccountOverview(id: string) { + return request("/v1/wechats/overview", { wechatId: id }, "GET"); +} + +// 获取微信号朋友圈列表 +export function getWechatMoments(params: { + wechatId: string; + page?: number; + limit?: number; +}) { + return request("/v1/wechats/moments", params, "GET"); +} + // 获取微信号好友列表 export function getWechatFriends(params: { wechatAccount: string; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts index 20d7a2c9..9245ba9f 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts @@ -1,3 +1,41 @@ +// 概览数据接口 +export interface WechatAccountOverview { + healthScoreAssessment: { + score: number; + dailyLimit: number; + todayAdded: number; + lastAddTime: string; + statusTag: string; + baseComposition?: Array<{ + name: string; + score: number; + formatted: string; + friendCount?: number; + }>; + dynamicRecords?: Array<{ + title?: string; + description?: string; + time?: string; + score?: number; + formatted?: string; + statusTag?: string; + }>; + }; + accountValue: { + value: number; + formatted: string; + }; + todayValueChange: { + change: number; + formatted: string; + isPositive: boolean; + }; + totalFriends: number; + todayNewFriends: number; + highValueChatrooms: number; + todayNewChatrooms: number; +} + export interface WechatAccountSummary { accountAge: string; activityLevel: { @@ -15,12 +53,51 @@ export interface WechatAccountSummary { todayAdded: number; addLimit: number; }; + healthScore?: { + score: number; + lastUpdate?: string; + lastAddTime?: string; + baseScore?: number; + verifiedScore?: number; + friendsScore?: number; + activities?: { + type: string; + time?: string; + score: number; + description?: string; + status?: string; + }[]; + }; + moments?: { + id: string; + date: string; + month: string; + day: string; + content: string; + images?: string[]; + timeAgo?: string; + hasEmoji?: boolean; + }[]; + accountValue?: { + value: number; + todayChange?: number; + }; + friendsCount?: { + total: number; + todayAdded?: number; + }; + groupsCount?: { + total: number; + todayAdded?: number; + }; restrictions: { id: number; level: number; reason: string; date: string; }[]; + // 新增概览数据 + overview?: WechatAccountOverview; } export interface Friend { @@ -39,6 +116,27 @@ export interface Friend { region: string; source: string; notes: string; + value?: number; + valueFormatted?: string; + statusTags?: string[]; +} + +export interface MomentItem { + id: string; + snsId: string; + type: number; + content: string; + resUrls: string[]; + commentList?: any[]; + likeList?: any[]; + createTime: string; + momentEntity?: { + lat?: string; + lng?: string; + location?: string; + picSize?: number; + userName?: string; + }; } export interface WechatFriendDetail { diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss index c6309f70..08f297a2 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss @@ -143,67 +143,235 @@ } .overview-content { - .info-grid { + // 健康分评估区域 + .health-score-section { + background: #ffffff; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .health-score-title { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 12px; + } + + .health-score-info { + .health-score-status { + display: flex; + justify-content: space-between; + margin-bottom: 12px; + + .status-tag { + background: #ffebeb; + color: #ff4d4f; + font-size: 12px; + padding: 2px 8px; + border-radius: 4px; + } + + .status-time { + font-size: 12px; + color: #999; + } + } + + .health-score-display { + display: flex; + align-items: center; + + .score-circle-wrapper { + width: 100px; + height: 100px; + margin-right: 24px; + position: relative; + + .score-circle { + width: 100%; + height: 100%; + border-radius: 50%; + background: #fff; + border: 8px solid #ff4d4f; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .score-number { + font-size: 28px; + font-weight: 700; + color: #ff4d4f; + line-height: 1; + } + + .score-label { + font-size: 12px; + color: #999; + margin-top: 4px; + } + } + } + + .health-score-stats { + flex: 1; + + .stats-row { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + + .stats-label { + font-size: 14px; + color: #666; + } + + .stats-value { + font-size: 14px; + color: #333; + font-weight: 500; + } + } + } + } + } + } + + // 账号统计卡片网格 + .account-stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; - .info-card { - background: linear-gradient(135deg, #e6f7ff, #f0f8ff); + .stat-card { + background: #ffffff; padding: 16px; border-radius: 12px; - border: 1px solid #bae7ff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); - transition: all 0.3s; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); - &:hover { - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - transform: translateY(-1px); - } - - .info-header { + .stat-header { display: flex; + justify-content: space-between; align-items: center; - gap: 8px; margin-bottom: 8px; - .info-icon { - font-size: 16px; - color: #1677ff; - padding: 6px; - background: #e6f7ff; - border-radius: 8px; + .stat-title { + font-size: 14px; + color: #666; } - .info-title { - flex: 1; + .stat-icon-up { + width: 20px; + height: 20px; + background: #f0f0f0; + border-radius: 50%; + position: relative; - .title-text { - font-size: 12px; - font-weight: 600; - color: #1677ff; - margin-bottom: 2px; + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) rotate(-45deg); + width: 8px; + height: 8px; + border-top: 2px solid #722ed1; + border-right: 2px solid #722ed1; + } + } + + .stat-icon-plus { + width: 20px; + height: 20px; + background: #f0f0f0; + border-radius: 50%; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 10px; + height: 2px; + background: #52c41a; } - .title-sub { - font-size: 10px; - color: #666; + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 10px; + background: #52c41a; + } + } + + .stat-icon-people { + width: 20px; + height: 20px; + background: #f0f0f0; + border-radius: 50%; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 6px; + left: 7px; + width: 6px; + height: 6px; + border-radius: 50%; + background: #1677ff; + } + + &::after { + content: ''; + position: absolute; + top: 13px; + left: 5px; + width: 10px; + height: 5px; + border-radius: 10px 10px 0 0; + background: #1677ff; + } + } + + .stat-icon-chat { + width: 20px; + height: 20px; + background: #f0f0f0; + border-radius: 50%; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 6px; + left: 6px; + width: 8px; + height: 8px; + border-radius: 2px; + background: #fa8c16; } } } - .info-value { - text-align: right; - font-size: 18px; - font-weight: 700; - color: #1677ff; + .stat-value { + font-size: 20px; + font-weight: 600; + color: #333; + } - .value-unit { - font-size: 12px; - color: #666; - margin-left: 4px; - } + .stat-value-positive { + font-size: 20px; + font-weight: 600; + color: #52c41a; } } } @@ -449,6 +617,47 @@ } } + .friends-summary { + display: flex; + align-items: center; + justify-content: space-between; + background: #f5f9ff; + border: 1px solid #e0edff; + border-radius: 10px; + padding: 12px 16px; + margin-bottom: 16px; + + .summary-item { + display: flex; + flex-direction: column; + gap: 4px; + } + + .summary-label { + font-size: 12px; + color: #666; + } + + .summary-value { + font-size: 20px; + font-weight: 600; + color: #111; + } + + .summary-value-highlight { + font-size: 20px; + font-weight: 600; + color: #fa541c; + } + + .summary-divider { + width: 1px; + height: 32px; + background: #e6e6e6; + margin: 0 12px; + } + } + .friends-list { .empty { text-align: center; @@ -467,83 +676,100 @@ } } - .friend-item { + .friend-card { display: flex; align-items: center; - padding: 12px; + padding: 14px; background: #fff; - border: 1px solid #e8e8e8; - border-radius: 8px; - margin-bottom: 8px; - } + border: 1px solid #f0f0f0; + border-radius: 12px; + margin-bottom: 10px; + gap: 12px; + transition: box-shadow 0.2s, border-color 0.2s; - .friend-item-static { - display: flex; - align-items: center; - padding: 12px; - background: #fff; - border: 1px solid #e8e8e8; - border-radius: 8px; - margin-bottom: 8px; + &:hover { + border-color: #cfe2ff; + box-shadow: 0 6px 16px rgba(24, 144, 255, 0.15); + } } .friend-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - margin-right: 12px; + width: 48px; + height: 48px; + + .adm-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + } } - .friend-info { + .friend-main { flex: 1; min-width: 0; + } - .friend-header { - display: flex; - align-items: center; - justify-content: space-between; + .friend-name-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } + + .friend-name { + font-size: 15px; + font-weight: 600; + color: #111; + flex-shrink: 0; + } + + .friend-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; + } + + .friend-tag { + font-size: 11px; + padding: 2px 8px; + border-radius: 999px; + background: #f5f5f5; + color: #666; + } + + .friend-id-row { + font-size: 12px; + color: #999; + margin-bottom: 6px; + } + + .friend-status-row { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + + .friend-status-chip { + background: #f0f7ff; + color: #1677ff; + font-size: 11px; + padding: 2px 8px; + border-radius: 8px; + } + + .friend-value { + text-align: right; + + .value-label { + font-size: 11px; + color: #999; margin-bottom: 4px; - - .friend-name { - font-size: 14px; - font-weight: 500; - color: #333; - max-width: 180px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - .friend-remark { - color: #666; - margin-left: 4px; - } - } - - .friend-arrow { - font-size: 12px; - color: #ccc; - } } - .friend-wechat-id { - font-size: 12px; - color: #666; - margin-bottom: 4px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .friend-tags { - display: flex; - flex-wrap: wrap; - gap: 4px; - - .friend-tag { - font-size: 10px; - padding: 2px 6px; - border-radius: 6px; - } + .value-amount { + font-size: 14px; + font-weight: 600; + color: #fa541c; } } } @@ -769,6 +995,416 @@ margin-right: 8px; } +.health-content { + padding: 16px 0; + height: 500px; + overflow-y: auto; + + .health-score-card { + background: #ffffff; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .health-score-status { + display: flex; + justify-content: space-between; + margin-bottom: 12px; + + .status-tag { + background: #ffebeb; + color: #ff4d4f; + font-size: 12px; + padding: 2px 8px; + border-radius: 4px; + } + + .status-time { + font-size: 12px; + color: #999; + } + } + + .health-score-display { + display: flex; + align-items: center; + + .score-circle-wrapper { + width: 100px; + height: 100px; + margin-right: 24px; + position: relative; + + .score-circle { + width: 100%; + height: 100%; + border-radius: 50%; + background: #fff; + border: 8px solid #ff4d4f; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .score-number { + font-size: 28px; + font-weight: 700; + color: #ff4d4f; + line-height: 1; + } + + .score-label { + font-size: 12px; + color: #999; + margin-top: 4px; + } + } + } + + .health-score-stats { + flex: 1; + + .stats-row { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + + .stats-label { + font-size: 14px; + color: #666; + } + + .stats-value { + font-size: 14px; + color: #333; + font-weight: 500; + } + } + } + } + } + + .health-section { + background: #ffffff; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .health-section-title { + font-size: 16px; + font-weight: 600; + color: #ff8800; + margin-bottom: 12px; + position: relative; + padding-left: 12px; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 4px; + height: 16px; + background: #ff8800; + border-radius: 2px; + } + } + + .health-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f5f5f5; + + &:last-child { + border-bottom: none; + } + + .health-item-label { + font-size: 14px; + color: #333; + display: flex; + align-items: center; + + .health-item-icon-warning { + width: 16px; + height: 16px; + border-radius: 50%; + background: #ffebeb; + margin-right: 8px; + position: relative; + + &::before { + content: '!'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #ff4d4f; + font-size: 12px; + font-weight: bold; + } + } + + .health-item-tag { + background: #fff7e6; + color: #fa8c16; + font-size: 12px; + padding: 2px 6px; + border-radius: 4px; + margin-left: 8px; + } + } + + .health-item-value-positive { + font-size: 14px; + font-weight: 600; + color: #52c41a; + } + + .health-item-value-negative { + font-size: 14px; + font-weight: 600; + color: #ff4d4f; + } + + .health-item-value-empty { + width: 20px; + } + } + + .health-empty { + text-align: center; + color: #999; + font-size: 14px; + padding: 20px 0; + } + } +} + +.moments-content { + padding: 16px 0; + height: 500px; + overflow-y: auto; + background: #f5f5f5; + + .moments-action-bar { + display: flex; + justify-content: space-between; + padding: 0 16px 16px; + + .action-button, .action-button-dark { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 70px; + height: 40px; + border-radius: 8px; + background: #1677ff; + + .action-icon-text, .action-icon-image, .action-icon-video, .action-icon-export { + width: 20px; + height: 20px; + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + margin-bottom: 2px; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 12px; + height: 2px; + background: white; + } + } + + .action-icon-image::after { + content: ''; + position: absolute; + top: 6px; + left: 6px; + width: 8px; + height: 8px; + border-radius: 2px; + background: white; + } + + .action-icon-video::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 0; + height: 0; + border-style: solid; + border-width: 5px 0 5px 8px; + border-color: transparent transparent transparent white; + } + + .action-text, .action-text-light { + font-size: 12px; + color: white; + } + } + + .action-button-dark { + background: #333; + } + } + + .moments-list { + padding: 0 16px; + + .moment-item { + display: flex; + margin-bottom: 16px; + background: white; + border-radius: 8px; + padding: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .moment-date { + margin-right: 12px; + text-align: center; + + .date-day { + font-size: 20px; + font-weight: 600; + color: #333; + line-height: 1; + } + + .date-month { + font-size: 12px; + color: #999; + margin-top: 2px; + } + } + + .moment-content { + flex: 1; + + .moment-text { + font-size: 14px; + line-height: 1.5; + color: #333; + margin-bottom: 8px; + white-space: pre-wrap; // 保留换行和空格,确保文本完整显示 + word-wrap: break-word; // 长单词自动换行 + + .moment-emoji { + display: inline; + font-size: 16px; + vertical-align: middle; + } + } + + .moment-images { + margin-bottom: 8px; + + .image-grid { + display: grid; + gap: 8px; + width: 100%; + + // 1张图片:宽度拉伸,高度自适应 + &.single { + grid-template-columns: 1fr; + + img { + width: 100%; + height: auto; + object-fit: cover; + border-radius: 8px; + } + } + + // 2张图片:左右并列 + &.double { + grid-template-columns: 1fr 1fr; + + img { + width: 100%; + height: 120px; + object-fit: cover; + border-radius: 8px; + } + } + + // 3张图片:三张并列 + &.triple { + grid-template-columns: 1fr 1fr 1fr; + + img { + width: 100%; + height: 100px; + object-fit: cover; + border-radius: 8px; + } + } + + // 4张图片:2x2网格布局 + &.quad { + grid-template-columns: repeat(2, 1fr); + + img { + width: 100%; + height: 140px; + object-fit: cover; + border-radius: 8px; + } + } + + // 5张及以上:网格布局(9宫格) + &.grid { + grid-template-columns: repeat(3, 1fr); + + img { + width: 100%; + height: 100px; + object-fit: cover; + border-radius: 8px; + } + + .image-more { + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.5); + border-radius: 8px; + color: white; + font-size: 12px; + font-weight: 500; + height: 100px; + } + } + } + } + + .moment-footer { + display: flex; + justify-content: flex-end; + + .moment-time { + font-size: 12px; + color: #999; + } + } + } + } + } +} + .risk-content { padding: 16px 0; height: 500px; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx index 3be617b5..50f2c599 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx @@ -21,11 +21,17 @@ import { } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import style from "./detail.module.scss"; -import { getWechatAccountDetail, getWechatFriends, transferWechatFriends } from "./api"; +import { + getWechatAccountDetail, + getWechatFriends, + transferWechatFriends, + getWechatAccountOverview, + getWechatMoments, +} from "./api"; import DeviceSelection from "@/components/DeviceSelection"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; -import { WechatAccountSummary, Friend } from "./data"; +import { WechatAccountSummary, Friend, MomentItem } from "./data"; const WechatAccountDetail: React.FC = () => { const { id } = useParams<{ id: string }>(); @@ -34,6 +40,7 @@ const WechatAccountDetail: React.FC = () => { const [accountSummary, setAccountSummary] = useState(null); const [accountInfo, setAccountInfo] = useState(null); + const [overviewData, setOverviewData] = useState(null); const [showRestrictions, setShowRestrictions] = useState(false); const [showTransferConfirm, setShowTransferConfirm] = useState(false); const [selectedDevices, setSelectedDevices] = useState([]); @@ -50,6 +57,12 @@ const WechatAccountDetail: React.FC = () => { const [isFetchingFriends, setIsFetchingFriends] = useState(false); const [hasFriendLoadError, setHasFriendLoadError] = useState(false); const [isFriendsEmpty, setIsFriendsEmpty] = useState(false); + const [moments, setMoments] = useState([]); + const [momentsPage, setMomentsPage] = useState(1); + const [momentsTotal, setMomentsTotal] = useState(0); + const [isFetchingMoments, setIsFetchingMoments] = useState(false); + const [momentsError, setMomentsError] = useState(null); + const MOMENTS_LIMIT = 10; // 获取基础信息 const fetchAccountInfo = useCallback(async () => { @@ -80,6 +93,19 @@ const WechatAccountDetail: React.FC = () => { } }, [id]); + // 获取概览数据 + const fetchOverviewData = useCallback(async () => { + if (!id) return; + try { + const response = await getWechatAccountOverview(id); + if (response) { + setOverviewData(response); + } + } catch (e) { + console.error("获取概览数据失败:", e); + } + }, [id]); + // 获取好友列表 - 封装为独立函数 const fetchFriendsList = useCallback( async (page: number = 1, keyword: string = "") => { @@ -96,26 +122,44 @@ const WechatAccountDetail: React.FC = () => { keyword: keyword, }); - const newFriends = response.list.map((friend: any) => ({ - id: friend.id.toString(), - avatar: friend.avatar || "/placeholder.svg", - nickname: friend.nickname || "未知用户", - wechatId: friend.wechatId || "", - remark: friend.memo || "", - addTime: friend.createTime || new Date().toISOString().split("T")[0], - lastInteraction: - friend.lastInteraction || new Date().toISOString().split("T")[0], - tags: friend.tags - ? friend.tags.map((tag: string, index: number) => ({ - id: `tag-${index}`, - name: tag, - color: getRandomTagColor(), - })) - : [], - region: friend.region || "未知", - source: friend.source || "未知", - notes: friend.notes || "", - })); + const newFriends = response.list.map((friend: any) => { + const memoTags = Array.isArray(friend.memo) + ? friend.memo + : friend.memo + ? String(friend.memo) + .split(/[,\s,、]+/) + .filter(Boolean) + : []; + + const tagList = Array.isArray(friend.tags) + ? friend.tags + : friend.tags + ? [friend.tags] + : []; + + return { + id: friend.id.toString(), + avatar: friend.avatar || "/placeholder.svg", + nickname: friend.nickname || "未知用户", + wechatId: friend.wechatId || "", + remark: friend.notes || "", + addTime: + friend.createTime || new Date().toISOString().split("T")[0], + lastInteraction: + friend.lastInteraction || new Date().toISOString().split("T")[0], + tags: memoTags.map((tag: string, index: number) => ({ + id: `tag-${index}`, + name: tag, + color: getRandomTagColor(), + })), + statusTags: tagList, + region: friend.region || "未知", + source: friend.source || "未知", + notes: friend.notes || "", + value: friend.value, + valueFormatted: friend.valueFormatted, + }; + }); setFriends(newFriends); setFriendsTotal(response.total); @@ -137,6 +181,46 @@ const WechatAccountDetail: React.FC = () => { [id], ); + const fetchMomentsList = useCallback( + async (page: number = 1, append: boolean = false) => { + if (!id) return; + setIsFetchingMoments(true); + setMomentsError(null); + try { + const response = await getWechatMoments({ + wechatId: id, + page, + limit: MOMENTS_LIMIT, + }); + + const list: MomentItem[] = (response.list || []).map((moment: any) => ({ + id: moment.id?.toString() || Math.random().toString(), + snsId: moment.snsId, + type: moment.type, + content: moment.content || "", + resUrls: moment.resUrls || [], + commentList: moment.commentList || [], + likeList: moment.likeList || [], + createTime: moment.createTime || "", + momentEntity: moment.momentEntity || {}, + })); + + setMoments(prev => (append ? [...prev, ...list] : list)); + setMomentsTotal(response.total || list.length); + setMomentsPage(page); + } catch (error) { + console.error("获取朋友圈数据失败:", error); + setMomentsError("获取朋友圈数据失败"); + if (!append) { + setMoments([]); + } + } finally { + setIsFetchingMoments(false); + } + }, + [id], + ); + // 搜索好友 const handleSearch = useCallback(() => { setFriendsPage(1); @@ -161,8 +245,9 @@ const WechatAccountDetail: React.FC = () => { useEffect(() => { if (id) { fetchAccountInfo(); + fetchOverviewData(); } - }, [id, fetchAccountInfo]); + }, [id, fetchAccountInfo, fetchOverviewData]); // 监听标签切换 - 只在切换到好友列表时请求一次 useEffect(() => { @@ -173,6 +258,14 @@ const WechatAccountDetail: React.FC = () => { } }, [activeTab, id, fetchFriendsList, searchQuery]); + useEffect(() => { + if (activeTab === "moments" && id) { + if (moments.length === 0) { + fetchMomentsList(1, false); + } + } + }, [activeTab, id, fetchMomentsList, moments.length]); + // 工具函数 const getRandomTagColor = (): string => { const colors = [ @@ -271,6 +364,41 @@ const WechatAccountDetail: React.FC = () => { navigate(`/mine/traffic-pool/detail/${friend.wechatId}/${friend.id}`); }; + const handleLoadMoreMoments = () => { + if (isFetchingMoments) return; + if (moments.length >= momentsTotal) return; + fetchMomentsList(momentsPage + 1, true); + }; + + const formatMomentDateParts = (dateString: string) => { + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) { + return { day: "--", month: "--" }; + } + const day = date.getDate().toString().padStart(2, "0"); + const month = `${date.getMonth() + 1}月`; + return { day, month }; + }; + + const formatMomentTimeAgo = (dateString: string) => { + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) { + return dateString || "--"; + } + const diff = Date.now() - date.getTime(); + const minutes = Math.floor(diff / (1000 * 60)); + if (minutes < 1) return "刚刚"; + if (minutes < 60) return `${minutes}分钟前`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}小时前`; + const days = Math.floor(hours / 24); + if (days < 7) return `${days}天前`; + return date.toLocaleDateString("zh-CN", { + month: "2-digit", + day: "2-digit", + }); + }; + return ( } loading={loadingInfo}>
@@ -313,73 +441,220 @@ const WechatAccountDetail: React.FC = () => { onChange={handleTabChange} className={style["tabs"]} > - +
-
-
-
- {accountInfo?.friendShip?.totalFriend ?? "-"} + {/* 健康分评估区域 */} +
+
健康分评估
+
+
+ {overviewData?.healthScoreAssessment?.statusTag || "已添加加人"} + 最后添加时间: {overviewData?.healthScoreAssessment?.lastAddTime || "18:44:14"}
-
好友数量
-
-
-
- +{accountSummary?.statistics.todayAdded ?? "-"} +
+
+
+
+ {overviewData?.healthScoreAssessment?.score || 67} +
+
SCORE
+
+
+
+
+
每日限额
+
{overviewData?.healthScoreAssessment?.dailyLimit || 0} 人
+
+
+
今日已加
+
{overviewData?.healthScoreAssessment?.todayAdded || 0} 人
+
+
-
今日新增
-
- 今日可添加: - - {accountSummary?.statistics.todayAdded ?? 0}/ - {accountSummary?.statistics.addLimit ?? 0} - -
-
-
-
-
-
-
-
-
- {accountInfo?.friendShip?.groupNumber ?? "-"} + + {/* 账号价值和好友数量区域 */} +
+ {/* 账号价值 */} +
+
+
账号价值
+
-
群聊数量
-
-
-
- {accountInfo?.activity?.yesterdayMsgCount ?? "-"} +
+ {overviewData?.accountValue?.formatted || `¥${overviewData?.accountValue?.value || "29,800"}`} +
+
+ + {/* 今日价值变化 */} +
+
+
今日价值变化
+
+
+
+ {overviewData?.todayValueChange?.formatted || `+${overviewData?.todayValueChange?.change || "500"}`}
-
今日消息
-
-
设备信息
-
- 设备名称: - {accountInfo?.deviceName ?? "-"} + + {/* 好友数量和今日新增好友区域 */} +
+ {/* 好友总数 */} +
+
+
好友总数
+
+
+
+ {overviewData?.totalFriends || accountInfo?.friendShip?.totalFriend || "0"} +
-
- 系统类型: - {accountInfo?.deviceType ?? "-"} + + {/* 今日新增好友 */} +
+
+
今日新增好友
+
+
+
+ +{overviewData?.todayNewFriends || accountSummary?.statistics.todayAdded || "0"} +
-
- 系统版本: - {accountInfo?.deviceVersion ?? "-"} +
+ + {/* 高价群聊区域 */} +
+ {/* 高价群聊 */} +
+
+
高价群聊
+
+
+
+ {overviewData?.highValueChatrooms || accountInfo?.friendShip?.groupNumber || "0"} +
+ + {/* 今日新增群聊 */} +
+
+
今日新增群聊
+
+
+
+ +{overviewData?.todayNewChatrooms || "0"} +
+
+
+ + +
+ + + +
+ {/* 健康分数圆环 */} +
+
+ 已添加加人 + 最后添加时间: {accountSummary?.healthScore?.lastAddTime || "18:36:06"} +
+
+
+
+
+ {accountSummary?.healthScore?.score || 67} +
+
SCORE
+
+
+
+
+
每日限额
+
{accountSummary?.statistics.addLimit || 0} 人
+
+
+
今日已加
+
{accountSummary?.statistics.todayAdded || 0} 人
+
+
+
+
+ + {/* 基础构成 */} +
+
基础构成
+ {(overviewData?.healthScoreAssessment?.baseComposition && + overviewData.healthScoreAssessment.baseComposition.length > 0 + ? overviewData.healthScoreAssessment.baseComposition + : [ + { name: "账号基础分", formatted: "+60" }, + { name: "已修改微信号", formatted: "+10" }, + { name: "好友数量加成", formatted: "+12", friendCount: 5595 }, + ] + ).map((item, index) => ( +
+
+ {item.name} + {item.friendCount ? ` (${item.friendCount})` : ""} +
+
= 0 + ? style["health-item-value-positive"] + : style["health-item-value-negative"] + } + > + {item.formatted || `${item.score ?? 0}`} +
+
+ ))} +
+ + {/* 动态记录 */} +
+
动态记录
+ {overviewData?.healthScoreAssessment?.dynamicRecords && + overviewData.healthScoreAssessment.dynamicRecords.length > 0 ? ( + overviewData.healthScoreAssessment.dynamicRecords.map( + (record, index) => ( +
+
+ + {record.title || record.description || "记录"} + {record.statusTag && ( + + {record.statusTag} + + )} +
+
= 0 + ? style["health-item-value-positive"] + : style["health-item-value-negative"] + } + > + {record.formatted || + (record.score && record.score > 0 + ? `+${record.score}` + : record.score || "-")} +
+
+ ), + ) + ) : ( +
暂无动态记录
+ )}
+ 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`} + title={`好友${activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`} key="friends" >
@@ -406,6 +681,23 @@ const WechatAccountDetail: React.FC = () => {
+ {/* 好友概要 */} +
+
+
好友总数
+
+ {friendsTotal || overviewData?.totalFriends || 0} +
+
+
+
+
好友总估值
+
+ {overviewData?.accountValue?.formatted || "¥1,500,000"} +
+
+
+ {/* 好友列表 */}
{isFetchingFriends && friends.length === 0 ? ( @@ -431,36 +723,53 @@ const WechatAccountDetail: React.FC = () => { {friends.map(friend => (
handleFriendClick(friend)} > - -
-
+
+ +
+
+
- {friend.nickname} - {friend.remark && ( - - ({friend.remark}) + {friend.nickname || "未知好友"} +
+
+ {friend.tags?.map((tag, index) => ( + + {typeof tag === "string" ? tag : tag.name} - )} + ))}
-
- {friend.wechatId} +
+ ID: {friend.wechatId || "-"}
-
- {friend.tags?.map((tag, index) => ( - + {friend.statusTags?.map((tag, idx) => ( + - {typeof tag === "string" ? tag : tag.name} - + {tag} + ))} + {friend.remark && ( + + {friend.remark} + + )} +
+
+
+
+ {friend.valueFormatted + || (typeof friend.value === "number" + ? `¥${friend.value.toLocaleString()}` + : "估值 -")}
@@ -484,45 +793,115 @@ const WechatAccountDetail: React.FC = () => {
- -
- {accountSummary?.restrictions && - accountSummary.restrictions.length > 0 ? ( -
- {accountSummary.restrictions.map(restriction => ( -
-
-
- {restriction.reason} -
-
- {restriction.date - ? formatDateTime(restriction.date) - : "暂无时间"} -
-
-
- - {restriction.level === 1 - ? "低风险" - : restriction.level === 2 - ? "中风险" - : "高风险"} - -
-
- ))} + + +
+ {/* 功能按钮栏 */} +
+
+ + 文本 +
+
+ + 图片 +
+
+ + 视频 +
+
+ + 导出 +
+
+ + {/* 朋友圈列表 */} +
+ {isFetchingMoments && moments.length === 0 ? ( +
+ +
+ ) : momentsError ? ( +
{momentsError}
+ ) : moments.length === 0 ? ( +
暂无朋友圈内容
+ ) : ( + moments.map(moment => { + const { day, month } = formatMomentDateParts( + moment.createTime, + ); + const timeAgo = formatMomentTimeAgo(moment.createTime); + const imageCount = moment.resUrls?.length || 0; + // 根据图片数量选择对应的grid类,参考素材管理的实现 + let gridClass = ""; + if (imageCount === 1) gridClass = style["single"]; + else if (imageCount === 2) gridClass = style["double"]; + else if (imageCount === 3) gridClass = style["triple"]; + else if (imageCount === 4) gridClass = style["quad"]; + else if (imageCount > 4) gridClass = style["grid"]; + + return ( +
+
+
{day}
+
{month}
+
+
+ {moment.content && ( +
+ {moment.content} +
+ )} + {imageCount > 0 && ( +
+
+ {moment.resUrls + .slice(0, 9) + .map((url, index) => ( + 朋友圈图片 + ))} + {imageCount > 9 && ( +
+ +{imageCount - 9} +
+ )} +
+
+ )} +
+ + {timeAgo} + +
+
+
+ ); + }) + )} +
+ + {moments.length < momentsTotal && ( +
+
- ) : ( -
暂无风险记录
)}
+
diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss b/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss index 4cfd3adc..8ddb6b92 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss @@ -2,6 +2,39 @@ padding: 0 12px; } +.filter-bar { + padding: 12px; + background: #fff; + border-bottom: 1px solid #f0f0f0; + + .filter-buttons { + display: flex; + gap: 8px; + + .filter-button { + flex: 1; + height: 32px; + border-radius: 6px; + border: 1px solid #d9d9d9; + background: #fff; + color: #666; + font-size: 14px; + transition: all 0.2s; + + &:hover { + border-color: #1677ff; + color: #1677ff; + } + + &.filter-button-active { + background: #1677ff; + border-color: #1677ff; + color: #fff; + } + } + } +} + .nav-title { font-size: 18px; font-weight: 600; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx b/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx index 4b761b60..dbd30443 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx @@ -33,11 +33,12 @@ const WechatAccounts: React.FC = () => { const [totalAccounts, setTotalAccounts] = useState(0); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); + const [statusFilter, setStatusFilter] = useState<"all" | "online" | "offline">("all"); // 获取路由参数 wechatStatus const wechatStatus = searchParams.get("wechatStatus"); - const fetchAccounts = async (page = 1, keyword = "") => { + const fetchAccounts = async (page = 1, keyword = "", status?: "all" | "online" | "offline") => { setIsLoading(true); try { const params: any = { @@ -46,8 +47,12 @@ const WechatAccounts: React.FC = () => { keyword, }; - // 如果有 wechatStatus 参数,添加到请求参数中 - if (wechatStatus) { + // 优先使用传入的status参数,否则使用路由参数,最后使用状态中的筛选 + const filterStatus = status || wechatStatus || statusFilter; + + if (filterStatus && filterStatus !== "all") { + params.wechatStatus = filterStatus === "online" ? "1" : "0"; + } else if (wechatStatus) { params.wechatStatus = wechatStatus; } @@ -60,7 +65,7 @@ const WechatAccounts: React.FC = () => { setTotalAccounts(0); } } catch (e) { - Toast.show({ content: "获取微信号失败", position: "top" }); + setAccounts([]); setTotalAccounts(0); } finally { @@ -69,18 +74,24 @@ const WechatAccounts: React.FC = () => { }; useEffect(() => { - fetchAccounts(currentPage, searchTerm); + fetchAccounts(currentPage, searchTerm, statusFilter); // eslint-disable-next-line - }, [currentPage]); + }, [currentPage, statusFilter]); const handleSearch = () => { setCurrentPage(1); - fetchAccounts(1, searchTerm); + fetchAccounts(1, searchTerm, statusFilter); + }; + + const handleStatusFilterChange = (status: "all" | "online" | "offline") => { + setStatusFilter(status); + setCurrentPage(1); + fetchAccounts(1, searchTerm, status); }; const handleRefresh = async () => { setIsRefreshing(true); - await fetchAccounts(currentPage, searchTerm); + await fetchAccounts(currentPage, searchTerm, statusFilter); setIsRefreshing(false); Toast.show({ content: "刷新成功", position: "top" }); }; @@ -122,6 +133,31 @@ const WechatAccounts: React.FC = () => {
+
+
+ + + +
+
} > diff --git a/Server/application/common/service/WechatAccountHealthScoreService.php b/Server/application/common/service/WechatAccountHealthScoreService.php index d6246a26..66f31234 100644 --- a/Server/application/common/service/WechatAccountHealthScoreService.php +++ b/Server/application/common/service/WechatAccountHealthScoreService.php @@ -16,16 +16,22 @@ use think\facade\Cache; * 2. 各个评分维度独立存储 * 3. 使用独立的评分记录表 * 4. 好友数量评分特殊处理(避免同步问题) - * 5. 动态分仅统计近30天数据 + * 5. 动态分统计所有历史数据(不限制30天) * 6. 优化数据库查询,减少重复计算 * 7. 添加完善的日志记录,便于问题排查 + * 8. 每条频繁/封号记录只统计一次,避免重复扣分 + * 9. 使用is_counted字段标记已统计的记录 + * 10. 支持lastBanTime字段,记录最后一次封号时间 + * 11. 使用事务和锁避免并发问题 + * 12. 使用静态缓存避免重复检查字段 + * 13. 推荐数据库索引提高查询性能 * * 健康分 = 基础分 + 动态分 * 基础分:60-100分(默认60分 + 基础信息10分 + 好友数量30分) * 动态分:扣分和加分规则 * * @author Your Name - * @version 2.0.0 + * @version 2.3.0 */ class WechatAccountHealthScoreService { @@ -33,7 +39,8 @@ class WechatAccountHealthScoreService * 缓存相关配置 */ const CACHE_PREFIX = 'wechat_health_score:'; // 缓存前缀 - const CACHE_TTL = 3600; // 缓存有效期(秒) + const CACHE_TTL = 7200; // 缓存有效期(秒)- 提高到2小时 + const CACHE_TTL_SHORT = 300; // 短期缓存有效期(秒)- 5分钟,用于频繁变化的数据 /** * 默认基础分 @@ -70,9 +77,34 @@ class WechatAccountHealthScoreService */ const TABLE_WECHAT_ACCOUNT = 's2_wechat_account'; const TABLE_WECHAT_ACCOUNT_SCORE = 's2_wechat_account_score'; + const TABLE_WECHAT_ACCOUNT_SCORE_LOG = 's2_wechat_account_score_log'; const TABLE_FRIEND_TASK = 's2_friend_task'; const TABLE_WECHAT_MESSAGE = 's2_wechat_message'; + /** + * 推荐数据库索引 + * 以下索引可以大幅提升查询性能 + * + * s2_wechat_account_score表: + * - PRIMARY KEY (`id`) + * - KEY `idx_account_id` (`accountId`) + * + * s2_friend_task表: + * - PRIMARY KEY (`id`) + * - KEY `idx_wechat_account_id` (`wechatAccountId`) + * - KEY `idx_wechat_id` (`wechatId`) + * - KEY `idx_create_time` (`createTime`) + * - KEY `idx_is_counted` (`is_counted`) + * + * s2_wechat_message表: + * - PRIMARY KEY (`id`) + * - KEY `idx_wechat_account_id` (`wechatAccountId`) + * - KEY `idx_msg_type` (`msgType`) + * - KEY `idx_create_time` (`createTime`) + * - KEY `idx_is_deleted` (`isDeleted`) + * - KEY `idx_is_counted` (`is_counted`) + */ + /** * 计算并更新账号健康分 * @@ -100,7 +132,7 @@ class WechatAccountHealthScoreService ->where('id', $accountId) ->find(); - Log::debug("查询账号数据: " . ($accountData ? "成功" : "失败")); + // 减少不必要的日志记录 } if (empty($accountData)) { @@ -116,11 +148,12 @@ class WechatAccountHealthScoreService throw new Exception($errorMsg); } - Log::debug("账号数据: accountId={$accountId}, wechatId={$wechatId}"); + // 减少不必要的日志记录 // 获取或创建评分记录 $scoreRecord = $this->getOrCreateScoreRecord($accountId, $wechatId); - Log::debug("获取评分记录: " . ($scoreRecord ? "成功" : "失败")); + $scoreSnapshotBefore = $this->buildScoreSnapshotForLogging($scoreRecord); + // 减少不必要的日志记录 // 计算基础分(只计算一次,除非强制重新计算) if (!$scoreRecord['baseScoreCalculated'] || $forceRecalculateBase) { @@ -131,7 +164,7 @@ class WechatAccountHealthScoreService $baseScoreData = $this->calculateBaseScore($accountData, $scoreRecord); $this->updateBaseScore($accountId, $baseScoreData); - Log::debug("基础分计算结果: " . json_encode($baseScoreData)); + // 减少不必要的日志记录 // 重新获取记录以获取最新数据 $scoreRecord = $this->getScoreRecord($accountId); @@ -166,6 +199,7 @@ class WechatAccountHealthScoreService 'lastNoFrequentTime' => $dynamicScoreData['lastNoFrequentTime'], 'consecutiveNoFrequentDays' => $dynamicScoreData['consecutiveNoFrequentDays'], 'isBanned' => $dynamicScoreData['isBanned'], + 'lastBanTime' => $dynamicScoreData['lastBanTime'], 'healthScore' => $healthScore, 'maxAddFriendPerDay' => $maxAddFriendPerDay, 'updateTime' => time() @@ -177,6 +211,19 @@ class WechatAccountHealthScoreService // 更新成功后,清除缓存 if ($updateResult !== false) { + $this->logScoreChangesIfNeeded( + $accountId, + $wechatId, + $scoreSnapshotBefore, + [ + 'frequentPenalty' => $dynamicScoreData['frequentPenalty'], + 'banPenalty' => $dynamicScoreData['banPenalty'], + 'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'], + 'dynamicScore' => $dynamicScore, + 'healthScore' => $healthScore + ], + $dynamicScoreData + ); $this->clearScoreCache($accountId); } @@ -194,7 +241,7 @@ class WechatAccountHealthScoreService 'maxAddFriendPerDay' => $maxAddFriendPerDay ]; - Log::debug("健康分计算完成,返回结果: " . json_encode($result)); + // 减少不必要的日志记录 return $result; } catch (\PDOException $e) { @@ -212,6 +259,7 @@ class WechatAccountHealthScoreService /** * 获取或创建评分记录 + * 优化:使用事务和锁避免并发问题,减少重复查询 * * @param int $accountId 账号ID * @param string $wechatId 微信ID @@ -224,35 +272,96 @@ class WechatAccountHealthScoreService // 如果记录不存在,创建新记录 if (empty($record)) { - Log::info("为账号 {$accountId} 创建新的评分记录"); - - // 创建新记录 - $data = [ - 'accountId' => $accountId, - 'wechatId' => $wechatId, - 'baseScore' => 0, - 'baseScoreCalculated' => 0, - 'baseInfoScore' => 0, - 'friendCountScore' => 0, - 'dynamicScore' => 0, - 'frequentCount' => 0, - 'consecutiveNoFrequentDays' => 0, - 'healthScore' => 0, - 'maxAddFriendPerDay' => 0, - 'createTime' => time(), - 'updateTime' => time() - ]; - - Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)->insert($data); - - return $data; + // 使用事务避免并发问题 + Db::startTrans(); + try { + // 再次检查记录是否存在(避免并发问题) + $record = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) + ->where('accountId', $accountId) + ->lock(true) // 加锁防止并发插入 + ->find(); + + if (empty($record)) { + Log::info("为账号 {$accountId} 创建新的评分记录"); + + // 检查表中是否存在lastBanTime字段 + $this->ensureScoreTableFields(); + + // 创建新记录 + $data = [ + 'accountId' => $accountId, + 'wechatId' => $wechatId, + 'baseScore' => 0, + 'baseScoreCalculated' => 0, + 'baseInfoScore' => 0, + 'friendCountScore' => 0, + 'dynamicScore' => 0, + 'frequentCount' => 0, + 'consecutiveNoFrequentDays' => 0, + 'healthScore' => 0, + 'maxAddFriendPerDay' => 0, + 'lastBanTime' => null, + 'createTime' => time(), + 'updateTime' => time() + ]; + + Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)->insert($data); + $record = $data; + } + + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + Log::error("创建评分记录失败: " . $e->getMessage()); + throw $e; + } } return $record; } + /** + * 确保评分表有所需字段 + * 优化:使用静态变量缓存结果,避免重复检查 + * + * @return void + */ + private function ensureScoreTableFields() + { + // 使用静态变量缓存检查结果,避免重复检查 + static $fieldsChecked = false; + + if ($fieldsChecked) { + return; + } + + try { + // 检查表中是否存在lastBanTime字段 + $hasLastBanTimeField = false; + $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_WECHAT_ACCOUNT_SCORE); + foreach ($tableFields as $field) { + if ($field['Field'] == 'lastBanTime') { + $hasLastBanTimeField = true; + break; + } + } + + // 如果字段不存在,添加字段 + if (!$hasLastBanTimeField) { + Log::info("添加lastBanTime字段到" . self::TABLE_WECHAT_ACCOUNT_SCORE . "表"); + Db::execute("ALTER TABLE " . self::TABLE_WECHAT_ACCOUNT_SCORE . " ADD COLUMN lastBanTime INT(11) DEFAULT NULL COMMENT '最后一次封号时间'"); + } + + $fieldsChecked = true; + } catch (\Exception $e) { + Log::error("检查或添加字段失败: " . $e->getMessage()); + // 出错时不影响后续逻辑,继续执行 + } + } + /** * 获取评分记录 + * 优化:使用多级缓存策略,提高缓存命中率 * * @param int $accountId 账号ID * @param bool $useCache 是否使用缓存(默认true) @@ -266,7 +375,7 @@ class WechatAccountHealthScoreService // 如果使用缓存且缓存存在,则直接返回缓存数据 if ($useCache && Cache::has($cacheKey)) { $cachedData = Cache::get($cacheKey); - Log::debug("从缓存获取评分记录,accountId: {$accountId}"); + // 减少日志记录,提高性能 return $cachedData ?: []; } @@ -277,8 +386,13 @@ class WechatAccountHealthScoreService // 如果记录存在且使用缓存,则缓存记录 if ($record && $useCache) { - Cache::set($cacheKey, $record, self::CACHE_TTL); - Log::debug("缓存评分记录,accountId: {$accountId}"); + // 根据数据更新频率设置不同的缓存时间 + // 如果记录最近更新过(1小时内),使用短期缓存 + $updateTime = $record['updateTime'] ?? 0; + $cacheTime = (time() - $updateTime < 3600) ? self::CACHE_TTL_SHORT : self::CACHE_TTL; + + Cache::set($cacheKey, $record, $cacheTime); + Log::debug("缓存评分记录,accountId: {$accountId}, 缓存时间: {$cacheTime}秒"); } return $record ?: []; @@ -349,7 +463,7 @@ class WechatAccountHealthScoreService ->where('accountId', $accountId) ->update($baseScoreData); - Log::debug("更新基础分,accountId: {$accountId}, 结果: " . ($result ? "成功" : "失败")); + // 减少不必要的日志记录 // 更新成功后,清除缓存 if ($result !== false) { @@ -373,7 +487,7 @@ class WechatAccountHealthScoreService { $cacheKey = self::CACHE_PREFIX . 'score:' . $accountId; $result = Cache::rm($cacheKey); - Log::debug("清除评分记录缓存,accountId: {$accountId}, 结果: " . ($result ? "成功" : "失败")); + // 减少不必要的日志记录 return $result; } @@ -556,7 +670,10 @@ class WechatAccountHealthScoreService 'frequentCount' => 0, 'lastNoFrequentTime' => null, 'consecutiveNoFrequentDays' => 0, - 'isBanned' => 0 + 'isBanned' => 0, + 'lastBanTime' => null, + 'frequentTaskIds' => [], + 'banMessageId' => null ]; if (empty($accountId) || empty($wechatId)) { @@ -564,8 +681,7 @@ class WechatAccountHealthScoreService return $result; } - // 计算30天前的时间戳(在多个方法中使用) - $thirtyDaysAgo = time() - (30 * 24 * 3600); + // 不再使用30天限制 // 检查添加好友记录表是否有记录,如果没有记录则动态分为0 // 使用EXISTS子查询优化性能,只检查是否存在记录,不需要计数 @@ -595,23 +711,27 @@ class WechatAccountHealthScoreService $result['frequentPenalty'] = $scoreRecord['frequentPenalty'] ?? 0; $result['noFrequentBonus'] = $scoreRecord['noFrequentBonus'] ?? 0; $result['banPenalty'] = $scoreRecord['banPenalty'] ?? 0; + $result['lastBanTime'] = $scoreRecord['lastBanTime'] ?? null; } - // 1. 检查频繁记录(从s2_friend_task表查询,只统计近30天) - $frequentData = $this->checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord, $thirtyDaysAgo); + // 1. 检查频繁记录(从s2_friend_task表查询,不限制时间) + $frequentData = $this->checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord); $result['lastFrequentTime'] = $frequentData['lastFrequentTime'] ?? null; $result['frequentCount'] = $frequentData['frequentCount'] ?? 0; $result['frequentPenalty'] = $frequentData['frequentPenalty'] ?? 0; + $result['frequentTaskIds'] = $frequentData['taskIds'] ?? []; - // 2. 检查封号记录(从s2_wechat_message表查询) - $banData = $this->checkBannedFromMessage($accountId, $wechatId, $thirtyDaysAgo); + // 2. 检查封号记录(从s2_wechat_message表查询,不限制时间) + $banData = $this->checkBannedFromMessage($accountId, $wechatId); if (!empty($banData)) { $result['isBanned'] = $banData['isBanned']; $result['banPenalty'] = $banData['banPenalty']; + $result['lastBanTime'] = $banData['lastBanTime']; + $result['banMessageId'] = $banData['messageId'] ?? null; } - // 3. 计算不频繁加分(基于近30天的频繁记录,反向参考频繁规则) - $noFrequentData = $this->calculateNoFrequentBonus($accountId, $wechatId, $frequentData, $thirtyDaysAgo); + // 3. 计算不频繁加分(基于频繁记录,反向参考频繁规则) + $noFrequentData = $this->calculateNoFrequentBonus($accountId, $wechatId, $frequentData); $result['noFrequentBonus'] = $noFrequentData['bonus'] ?? 0; $result['consecutiveNoFrequentDays'] = $noFrequentData['consecutiveDays'] ?? 0; $result['lastNoFrequentTime'] = $noFrequentData['lastNoFrequentTime'] ?? null; @@ -629,27 +749,27 @@ class WechatAccountHealthScoreService /** * 从s2_friend_task表检查频繁记录 * extra字段包含"操作过于频繁"即需要扣分 - * 只统计近30天的数据 + * 统计所有时间的数据(不限制30天) + * 每条记录只统计一次,使用is_counted字段标记 * * @param int $accountId 账号ID * @param string $wechatId 微信ID * @param array $scoreRecord 现有评分记录 - * @param int $thirtyDaysAgo 30天前的时间戳(可选,如果已计算则传入以避免重复计算) + * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性 * @return array|null */ private function checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord, $thirtyDaysAgo = null) { - // 如果没有传入30天前的时间戳,则计算 - if ($thirtyDaysAgo === null) { - $thirtyDaysAgo = time() - (30 * 24 * 3600); - } + // 不再使用30天限制 - // 查询包含"操作过于频繁"的记录(只统计近30天) + // 减少不必要的日志记录 + + // 查询包含"操作过于频繁"的记录(统计所有时间且未被统计过的记录) // extra字段可能是文本或JSON格式,使用LIKE查询 // 优化查询:只查询必要的字段,减少数据传输量 + // 添加is_counted条件,只查询未被统计过的记录 $frequentTasks = Db::table(self::TABLE_FRIEND_TASK) ->where('wechatAccountId', $accountId) - ->where('createTime', '>=', $thirtyDaysAgo) ->where(function($query) use ($wechatId) { if (!empty($wechatId)) { $query->where('wechatId', $wechatId); @@ -660,6 +780,12 @@ class WechatAccountHealthScoreService $query->where('extra', 'like', '%操作过于频繁%') ->whereOr('extra', 'like', '%"当前账号存在安全风险"%'); }) + ->where(function($query) { + // 只查询未被统计过的记录 + // 注意:需要兼容is_counted字段不存在的情况 + $query->where('is_counted', 0) + ->whereOr('is_counted', null); + }) ->order('createTime', 'desc') ->field('id, createTime, extra') ->select(); @@ -670,12 +796,47 @@ class WechatAccountHealthScoreService // 计算频繁次数(统计近30天内包含"操作过于频繁"的记录) $frequentCount = count($frequentTasks); + Log::info("找到 {$frequentCount} 条未统计的频繁记录,accountId: {$accountId}, wechatId: {$wechatId}"); + + // 标记这些记录为已统计 + if (!empty($frequentTasks)) { + $taskIds = array_column($frequentTasks, 'id'); + try { + // 检查表中是否存在is_counted字段 + $hasIsCountedField = false; + $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_FRIEND_TASK); + foreach ($tableFields as $field) { + if ($field['Field'] == 'is_counted') { + $hasIsCountedField = true; + break; + } + } + + // 如果字段不存在,添加字段 + if (!$hasIsCountedField) { + Log::info("添加is_counted字段到" . self::TABLE_FRIEND_TASK . "表"); + Db::execute("ALTER TABLE " . self::TABLE_FRIEND_TASK . " ADD COLUMN is_counted TINYINT(1) DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)'"); + } + + // 更新记录为已统计 + Db::table(self::TABLE_FRIEND_TASK) + ->where('id', 'in', $taskIds) + ->update(['is_counted' => 1]); + + // 减少不必要的日志记录 + } catch (\Exception $e) { + Log::error("标记频繁记录失败: " . $e->getMessage()); + // 出错时不影响后续逻辑,继续执行 + } + } + // 如果30天内没有频繁记录,清除扣分 if (empty($frequentTasks)) { return [ 'lastFrequentTime' => null, 'frequentCount' => 0, - 'frequentPenalty' => 0 + 'frequentPenalty' => 0, + 'taskIds' => [] ]; } @@ -683,69 +844,110 @@ class WechatAccountHealthScoreService $penalty = 0; if ($frequentCount == 1) { $penalty = self::PENALTY_FIRST_FREQUENT; // 首次频繁-15分 + Log::info("首次频繁,扣除 " . abs(self::PENALTY_FIRST_FREQUENT) . " 分,accountId: {$accountId}"); } elseif ($frequentCount >= 2) { $penalty = self::PENALTY_SECOND_FREQUENT; // 再次频繁-25分 + Log::info("再次频繁,扣除 " . abs(self::PENALTY_SECOND_FREQUENT) . " 分,accountId: {$accountId}"); } return [ 'lastFrequentTime' => $latestFrequentTime, 'frequentCount' => $frequentCount, - 'frequentPenalty' => $penalty + 'frequentPenalty' => $penalty, + 'taskIds' => $taskIds ]; } /** * 从s2_wechat_message表检查封号记录 * content包含"你的账号被限制"且msgType为10000 - * 只统计近30天的数据 + * 统计所有时间的数据(不限制30天) + * 每条记录只统计一次,使用is_counted字段标记 * * @param int $accountId 账号ID * @param string $wechatId 微信ID - * @param int $thirtyDaysAgo 30天前的时间戳(可选,如果已计算则传入以避免重复计算) + * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性 * @return array|null */ private function checkBannedFromMessage($accountId, $wechatId, $thirtyDaysAgo = null) { - // 如果没有传入30天前的时间戳,则计算 - if ($thirtyDaysAgo === null) { - $thirtyDaysAgo = time() - (30 * 24 * 3600); - } + // 不再使用30天限制 - // 查询封号消息(只统计近30天) + // 减少不必要的日志记录 + + // 查询封号消息(统计所有时间且未被统计过的记录) // 优化查询:只查询必要的字段,减少数据传输量 $banMessage = Db::table(self::TABLE_WECHAT_MESSAGE) ->where('wechatAccountId', $accountId) ->where('msgType', 10000) ->where('content', 'like', '%你的账号被限制%') ->where('isDeleted', 0) - ->where('createTime', '>=', $thirtyDaysAgo) + ->where(function($query) { + // 只查询未被统计过的记录 + // 注意:需要兼容is_counted字段不存在的情况 + $query->where('is_counted', 0) + ->whereOr('is_counted', null); + }) ->field('id, createTime') // 只查询必要的字段 ->order('createTime', 'desc') ->find(); if (!empty($banMessage)) { + try { + // 检查表中是否存在is_counted字段 + $hasIsCountedField = false; + $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_WECHAT_MESSAGE); + foreach ($tableFields as $field) { + if ($field['Field'] == 'is_counted') { + $hasIsCountedField = true; + break; + } + } + + // 如果字段不存在,添加字段 + if (!$hasIsCountedField) { + Log::info("添加is_counted字段到" . self::TABLE_WECHAT_MESSAGE . "表"); + Db::execute("ALTER TABLE " . self::TABLE_WECHAT_MESSAGE . " ADD COLUMN is_counted TINYINT(1) DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)'"); + } + + // 更新记录为已统计 + Db::table(self::TABLE_WECHAT_MESSAGE) + ->where('id', $banMessage['id']) + ->update(['is_counted' => 1]); + + // 减少不必要的日志记录 + Log::info("发现封号记录,扣除 " . abs(self::PENALTY_BANNED) . " 分,accountId: {$accountId}"); + } catch (\Exception $e) { + Log::error("标记封号记录失败: " . $e->getMessage()); + // 出错时不影响后续逻辑,继续执行 + } + return [ 'isBanned' => 1, - 'banPenalty' => self::PENALTY_BANNED // 封号-60分 + 'banPenalty' => self::PENALTY_BANNED, // 封号-60分 + 'lastBanTime' => $banMessage['createTime'], + 'messageId' => $banMessage['id'] ]; } return [ 'isBanned' => 0, - 'banPenalty' => 0 + 'banPenalty' => 0, + 'lastBanTime' => null, + 'messageId' => null ]; } /** * 计算不频繁加分 - * 反向参考频繁规则:查询近30天的频繁记录,计算连续不频繁天数 - * 规则:30天内连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) + * 反向参考频繁规则:计算连续不频繁天数 + * 规则:连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) * 如果连续3天没有频繁,则每天+5分 * * @param int $accountId 账号ID * @param string $wechatId 微信ID * @param array $frequentData 频繁数据(包含lastFrequentTime和frequentCount) - * @param int $thirtyDaysAgo 30天前的时间戳(可选,如果已计算则传入以避免重复计算) + * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性 * @return array 包含bonus、consecutiveDays、lastNoFrequentTime */ private function calculateNoFrequentBonus($accountId, $wechatId, $frequentData, $thirtyDaysAgo = null) @@ -760,31 +962,22 @@ class WechatAccountHealthScoreService return $result; } - // 如果没有传入30天前的时间戳,则计算 - if ($thirtyDaysAgo === null) { - $thirtyDaysAgo = time() - (30 * 24 * 3600); - } $currentTime = time(); - // 获取最后一次频繁时间(30天内最后一次频繁的时间) + // 获取最后一次频繁时间 $lastFrequentTime = $frequentData['lastFrequentTime'] ?? null; - // 规则:30天内连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) + // 规则:连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) if (empty($lastFrequentTime)) { - // 情况1:30天内没有频繁记录,说明30天内连续不频繁 - // 计算从30天前到现在的连续不频繁天数(最多30天) - $consecutiveDays = min(30, floor(($currentTime - $thirtyDaysAgo) / 86400)); + // 情况1:没有频繁记录,说明一直连续不频繁 + // 默认给30天的连续不频繁天数(可以根据需要调整) + $consecutiveDays = 30; } else { - // 情况2:30天内有频繁记录,从最后一次频繁时间开始重新计算连续不频繁天数 + // 情况2:有频繁记录,从最后一次频繁时间开始重新计算连续不频繁天数 // 只要有一次频繁,连续不频繁天数就从最后一次频繁时间开始重新计算 // 计算从最后一次频繁时间到现在,连续多少天没有频繁 $timeDiff = $currentTime - $lastFrequentTime; $consecutiveDays = floor($timeDiff / 86400); // 向下取整,得到完整的天数 - - // 边界情况:如果最后一次频繁时间在30天前(理论上不应该发生,因为查询已经限制了30天),则按30天处理 - if ($lastFrequentTime < $thirtyDaysAgo) { - $consecutiveDays = min(30, floor(($currentTime - $thirtyDaysAgo) / 86400)); - } } // 如果连续3天或以上没有频繁,则每天+5分 @@ -800,6 +993,157 @@ class WechatAccountHealthScoreService return $result; } + /** + * 构建日志快照(用于对比前后分值) + * + * @param array $scoreRecord + * @return array + */ + private function buildScoreSnapshotForLogging($scoreRecord) + { + $baseScore = $scoreRecord['baseScore'] ?? self::DEFAULT_BASE_SCORE; + $dynamicScore = $scoreRecord['dynamicScore'] ?? 0; + $healthScore = $scoreRecord['healthScore'] ?? ($baseScore + $dynamicScore); + + return [ + 'frequentPenalty' => $scoreRecord['frequentPenalty'] ?? 0, + 'banPenalty' => $scoreRecord['banPenalty'] ?? 0, + 'noFrequentBonus' => $scoreRecord['noFrequentBonus'] ?? 0, + 'dynamicScore' => $dynamicScore, + 'healthScore' => $healthScore + ]; + } + + /** + * 根据前后快照写加减分日志 + * + * @param int $accountId + * @param string $wechatId + * @param array $before + * @param array $after + * @param array $context + * @return void + */ + private function logScoreChangesIfNeeded($accountId, $wechatId, array $before, array $after, array $context = []) + { + $healthBefore = $before['healthScore'] ?? 0; + $healthAfter = $after['healthScore'] ?? 0; + + $this->recordScoreLog($accountId, $wechatId, 'frequentPenalty', $before['frequentPenalty'] ?? 0, $after['frequentPenalty'] ?? 0, [ + 'category' => 'penalty', + 'source' => 'friend_task', + 'sourceId' => !empty($context['frequentTaskIds']) ? $context['frequentTaskIds'][0] : null, + 'extra' => [ + 'taskIds' => $context['frequentTaskIds'] ?? [], + 'frequentCount' => $context['frequentCount'] ?? 0, + 'lastFrequentTime' => $context['lastFrequentTime'] ?? null + ], + 'totalScoreBefore' => $healthBefore, + 'totalScoreAfter' => $healthAfter + ]); + + $this->recordScoreLog($accountId, $wechatId, 'banPenalty', $before['banPenalty'] ?? 0, $after['banPenalty'] ?? 0, [ + 'category' => 'penalty', + 'source' => 'wechat_message', + 'sourceId' => $context['banMessageId'] ?? null, + 'extra' => [ + 'lastBanTime' => $context['lastBanTime'] ?? null + ], + 'totalScoreBefore' => $healthBefore, + 'totalScoreAfter' => $healthAfter + ]); + + $this->recordScoreLog($accountId, $wechatId, 'noFrequentBonus', $before['noFrequentBonus'] ?? 0, $after['noFrequentBonus'] ?? 0, [ + 'category' => 'bonus', + 'source' => 'system', + 'extra' => [ + 'consecutiveDays' => $context['consecutiveNoFrequentDays'] ?? 0, + 'lastNoFrequentTime' => $context['lastNoFrequentTime'] ?? null + ], + 'totalScoreBefore' => $healthBefore, + 'totalScoreAfter' => $healthAfter + ]); + + $this->recordScoreLog($accountId, $wechatId, 'dynamicScore', $before['dynamicScore'] ?? 0, $after['dynamicScore'] ?? 0, [ + 'category' => 'dynamic_total', + 'source' => 'system', + 'totalScoreBefore' => $healthBefore, + 'totalScoreAfter' => $healthAfter + ]); + + $this->recordScoreLog($accountId, $wechatId, 'healthScore', $before['healthScore'] ?? 0, $after['healthScore'] ?? 0, [ + 'category' => 'health_total', + 'source' => 'system', + 'totalScoreBefore' => $healthBefore, + 'totalScoreAfter' => $healthAfter + ]); + } + + /** + * 插入健康分加减分日志 + * + * @param int $accountId + * @param string $wechatId + * @param string $field + * @param int|null $beforeValue + * @param int|null $afterValue + * @param array $context + * @return void + */ + private function recordScoreLog($accountId, $wechatId, $field, $beforeValue, $afterValue, array $context = []) + { + $beforeValue = (int)($beforeValue ?? 0); + $afterValue = (int)($afterValue ?? 0); + + if ($beforeValue === $afterValue) { + return; + } + + $extraPayload = $context['extra'] ?? null; + if (is_array($extraPayload)) { + $extraPayload = json_encode($extraPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } elseif (!is_string($extraPayload)) { + $extraPayload = null; + } + + $sourceId = null; + if (array_key_exists('sourceId', $context)) { + $sourceId = $context['sourceId']; + } + + $totalScoreBefore = null; + if (array_key_exists('totalScoreBefore', $context)) { + $totalScoreBefore = $context['totalScoreBefore']; + } + + $totalScoreAfter = null; + if (array_key_exists('totalScoreAfter', $context)) { + $totalScoreAfter = $context['totalScoreAfter']; + } + + $data = [ + 'accountId' => $accountId, + 'wechatId' => $wechatId, + 'field' => $field, + 'changeValue' => $afterValue - $beforeValue, + 'valueBefore' => $beforeValue, + 'valueAfter' => $afterValue, + 'category' => $context['category'] ?? null, + 'source' => $context['source'] ?? null, + 'sourceId' => $sourceId, + 'extra' => $extraPayload, + 'totalScoreBefore' => $totalScoreBefore, + 'totalScoreAfter' => $totalScoreAfter, + 'createTime' => time() + ]; + + try { + Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE_LOG)->insert($data); + } catch (\Exception $e) { + Log::error("记录健康分加减分日志失败,accountId: {$accountId}, field: {$field}, 错误: " . $e->getMessage()); + } + } + /** * 根据健康分计算每日最大加人次数 * 公式:每日最大加人次数 = 健康分 * 0.2 @@ -814,14 +1158,16 @@ class WechatAccountHealthScoreService /** * 批量计算并更新多个账号的健康分 + * 优化:使用多线程处理、优化批处理逻辑、减少日志记录 * * @param array $accountIds 账号ID数组(为空则处理所有账号) * @param int $batchSize 每批处理数量 * @param bool $forceRecalculateBase 是否强制重新计算基础分 + * @param bool $useMultiThread 是否使用多线程处理(需要pcntl扩展支持) * @return array 处理结果统计 * @throws Exception 如果参数无效或批量处理过程中出现严重错误 */ - public function batchCalculateAndUpdate($accountIds = [], $batchSize = 100, $forceRecalculateBase = false) + public function batchCalculateAndUpdate($accountIds = [], $batchSize = 50, $forceRecalculateBase = false, $useMultiThread = false) { // 参数验证 if (!is_array($accountIds)) { @@ -836,9 +1182,16 @@ class WechatAccountHealthScoreService throw new Exception($errorMsg); } + // 检查是否支持多线程 + if ($useMultiThread && !function_exists('pcntl_fork')) { + $useMultiThread = false; + Log::warning("系统不支持pcntl扩展,无法使用多线程处理,将使用单线程模式"); + } + try { $startTime = microtime(true); - Log::info("开始批量计算健康分,batchSize: {$batchSize}, forceRecalculateBase: " . ($forceRecalculateBase ? 'true' : 'false')); + Log::info("开始批量计算健康分,batchSize: {$batchSize}, forceRecalculateBase: " . ($forceRecalculateBase ? 'true' : 'false') . + ", useMultiThread: " . ($useMultiThread ? 'true' : 'false')); $stats = [ 'total' => 0, @@ -855,43 +1208,69 @@ class WechatAccountHealthScoreService ->column('id'); } - $stats['total'] = count($accountIds); - Log::info("需要处理的账号总数: {$stats['total']}"); - - // 分批处理 - $batches = array_chunk($accountIds, $batchSize); - $batchCount = count($batches); - Log::info("分批处理,共 {$batchCount} 批"); - - foreach ($batches as $batchIndex => $batch) { - $batchStartTime = microtime(true); - Log::info("开始处理第 " . ($batchIndex + 1) . " 批,共 " . count($batch) . " 个账号"); + $stats['total'] = count($accountIds); + Log::info("需要处理的账号总数: {$stats['total']}"); - foreach ($batch as $accountId) { - try { - $this->calculateAndUpdate($accountId, null, $forceRecalculateBase); - $stats['success']++; - } catch (Exception $e) { - $stats['failed']++; - $stats['errors'][] = [ - 'accountId' => $accountId, - 'error' => $e->getMessage() - ]; - Log::error("账号 {$accountId} 计算失败: " . $e->getMessage()); + // 优化:减小批次大小,提高并行处理效率 + $batchSize = min($batchSize, 50); + + // 分批处理 + $batches = array_chunk($accountIds, $batchSize); + $batchCount = count($batches); + Log::info("分批处理,共 {$batchCount} 批"); + + // 多线程处理 + if ($useMultiThread && $batchCount > 1) { + $childPids = []; + $maxProcesses = 4; // 最大并行进程数 + $runningProcesses = 0; + + for ($i = 0; $i < $batchCount; $i++) { + // 如果达到最大进程数,等待某个子进程结束 + if ($runningProcesses >= $maxProcesses) { + $pid = pcntl_wait($status); + $runningProcesses--; + } + + // 创建子进程 + $pid = pcntl_fork(); + + if ($pid == -1) { + // 创建进程失败 + Log::error("创建子进程失败"); + continue; + } elseif ($pid == 0) { + // 子进程 + $this->processBatch($batches[$i], $i, $batchCount, $forceRecalculateBase); + exit(0); + } else { + // 父进程 + $childPids[] = $pid; + $runningProcesses++; + } + } + + // 等待所有子进程结束 + foreach ($childPids as $pid) { + pcntl_waitpid($pid, $status); + } + + Log::info("所有批次处理完成"); + } else { + // 单线程处理 + foreach ($batches as $batchIndex => $batch) { + $batchStats = $this->processBatch($batch, $batchIndex, $batchCount, $forceRecalculateBase); + $stats['success'] += $batchStats['success']; + $stats['failed'] += $batchStats['failed']; + $stats['errors'] = array_merge($stats['errors'], $batchStats['errors']); } } - $batchEndTime = microtime(true); - $batchDuration = round($batchEndTime - $batchStartTime, 2); - Log::info("第 " . ($batchIndex + 1) . " 批处理完成,耗时: {$batchDuration}秒," . - "成功: {$stats['success']},失败: {$stats['failed']}"); - } - - $endTime = microtime(true); - $totalDuration = round($endTime - $startTime, 2); - Log::info("批量计算健康分完成,总耗时: {$totalDuration}秒,成功: {$stats['success']},失败: {$stats['failed']}"); - - return $stats; + $endTime = microtime(true); + $totalDuration = round($endTime - $startTime, 2); + Log::info("批量计算健康分完成,总耗时: {$totalDuration}秒,成功: {$stats['success']},失败: {$stats['failed']}"); + + return $stats; } catch (\PDOException $e) { $errorMsg = "批量计算健康分过程中数据库操作失败: " . $e->getMessage(); Log::error($errorMsg); @@ -903,6 +1282,68 @@ class WechatAccountHealthScoreService } } + /** + * 处理单个批次的账号 + * + * @param array $batch 批次账号ID数组 + * @param int $batchIndex 批次索引 + * @param int $batchCount 总批次数 + * @param bool $forceRecalculateBase 是否强制重新计算基础分 + * @return array 处理结果统计 + */ + private function processBatch($batch, $batchIndex, $batchCount, $forceRecalculateBase) + { + $batchStartTime = microtime(true); + Log::info("开始处理第 " . ($batchIndex + 1) . " 批,共 " . count($batch) . " 个账号"); + + $stats = [ + 'success' => 0, + 'failed' => 0, + 'errors' => [] + ]; + + // 优化:预先获取账号数据,减少重复查询 + $accountIds = implode(',', $batch); + $accountDataMap = []; + if (!empty($batch)) { + $accountDataList = Db::table(self::TABLE_WECHAT_ACCOUNT) + ->where('id', 'in', $batch) + ->select(); + + foreach ($accountDataList as $accountData) { + $accountDataMap[$accountData['id']] = $accountData; + } + } + + // 批量处理账号 + foreach ($batch as $accountId) { + try { + $accountData = $accountDataMap[$accountId] ?? null; + $this->calculateAndUpdate($accountId, $accountData, $forceRecalculateBase); + $stats['success']++; + + // 减少日志记录,每10个账号记录一次进度 + if ($stats['success'] % 10 == 0) { + Log::debug("批次 " . ($batchIndex + 1) . " 已处理 {$stats['success']} 个账号"); + } + } catch (Exception $e) { + $stats['failed']++; + $stats['errors'][] = [ + 'accountId' => $accountId, + 'error' => $e->getMessage() + ]; + Log::error("账号 {$accountId} 计算失败: " . $e->getMessage()); + } + } + + $batchEndTime = microtime(true); + $batchDuration = round($batchEndTime - $batchStartTime, 2); + Log::info("第 " . ($batchIndex + 1) . "/" . $batchCount . " 批处理完成,耗时: {$batchDuration}秒," . + "成功: {$stats['success']},失败: {$stats['failed']}"); + + return $stats; + } + /** * 记录频繁事件(已废弃,改为从s2_friend_task表自动检测) * 保留此方法以兼容旧代码,实际频繁检测在calculateDynamicScore中完成 @@ -985,6 +1426,7 @@ class WechatAccountHealthScoreService /** * 获取账号健康分信息 + * 优化:使用多级缓存策略,提高缓存命中率 * * @param int $accountId 账号ID * @param bool $useCache 是否使用缓存(默认true) @@ -1005,7 +1447,7 @@ class WechatAccountHealthScoreService // 如果使用缓存且缓存存在,则直接返回缓存数据 if ($useCache && !$forceRecalculate && Cache::has($cacheKey)) { $cachedData = Cache::get($cacheKey); - Log::debug("从缓存获取健康分信息,accountId: {$accountId}"); + // 减少日志记录,提高性能 return $cachedData; } @@ -1032,13 +1474,20 @@ class WechatAccountHealthScoreService 'baseScoreCalculated' => $scoreRecord['baseScoreCalculated'] ?? 0, 'lastFrequentTime' => $scoreRecord['lastFrequentTime'] ?? null, 'frequentCount' => $scoreRecord['frequentCount'] ?? 0, - 'isBanned' => $scoreRecord['isBanned'] ?? 0 + 'isBanned' => $scoreRecord['isBanned'] ?? 0, + 'lastBanTime' => $scoreRecord['lastBanTime'] ?? null ]; // 如果使用缓存,则缓存健康分信息 if ($useCache) { - Cache::set($cacheKey, $healthScoreInfo, self::CACHE_TTL); - Log::debug("缓存健康分信息,accountId: {$accountId}"); + // 根据数据更新频率设置不同的缓存时间 + // 如果有频繁记录或封号记录,使用短期缓存 + $cacheTime = (!empty($scoreRecord['lastFrequentTime']) || !empty($scoreRecord['isBanned'])) + ? self::CACHE_TTL_SHORT + : self::CACHE_TTL; + + Cache::set($cacheKey, $healthScoreInfo, $cacheTime); + Log::debug("缓存健康分信息,accountId: {$accountId}, 缓存时间: {$cacheTime}秒"); } return $healthScoreInfo; @@ -1058,7 +1507,7 @@ class WechatAccountHealthScoreService // 同时清除评分记录缓存 $this->clearScoreCache($accountId); - Log::debug("清除健康分信息缓存,accountId: {$accountId}, 结果: " . ($result ? "成功" : "失败")); + // 减少不必要的日志记录 return $result; } } diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index e6f7ee03..c34fcd07 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -36,13 +36,13 @@ Route::group('v1/', function () { Route::get(':id/summary', 'app\cunkebao\controller\wechat\GetWechatOnDeviceSummarizeV1Controller@index'); Route::get(':id/friends', 'app\cunkebao\controller\wechat\GetWechatOnDeviceFriendsV1Controller@index'); Route::get('getWechatInfo', 'app\cunkebao\controller\wechat\GetWechatController@getWechatInfo'); - Route::get(':wechatId', 'app\cunkebao\controller\wechat\GetWechatProfileV1Controller@index'); - Route::post('transfer-friends', 'app\cunkebao\controller\wechat\PostTransferFriends@index'); // 微信好友转移 - + Route::get('overview', 'app\cunkebao\controller\wechat\GetWechatOverviewV1Controller@index'); // 获取微信账号概览数据 + Route::get('moments', 'app\cunkebao\controller\wechat\GetWechatMomentsV1Controller@index'); // 获取微信朋友圈 Route::get('count', 'app\cunkebao\controller\DeviceWechat@count'); Route::get('device-count', 'app\cunkebao\controller\DeviceWechat@deviceCount'); // 获取有登录微信的设备数量 Route::put('refresh', 'app\cunkebao\controller\DeviceWechat@refresh'); // 刷新设备微信状态 - + Route::post('transfer-friends', 'app\cunkebao\controller\wechat\PostTransferFriends@index'); // 微信好友转移 + Route::get(':wechatId', 'app\cunkebao\controller\wechat\GetWechatProfileV1Controller@index'); }); // 获客场景相关 diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index 69291123..bcf8e50b 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -1061,6 +1061,7 @@ class ContentLibraryController extends Controller $where = [ ['isDel', '=', 0], // 未删除 ['status', '=', 1], // 已开启 + ['id', '=', 99], // 已开启 ]; // 查询符合条件的内容库 @@ -1225,7 +1226,7 @@ class ContentLibraryController extends Controller foreach ($friends as $friend) { $processedFriends++; - + // 如果配置了API并且需要主动获取朋友圈 if ($needFetch) { try { @@ -1264,9 +1265,9 @@ class ContentLibraryController extends Controller } // 如果指定了采集类型,进行过滤 - if (!empty($catchTypes)) { + /*if (!empty($catchTypes)) { $query->whereIn('type', $catchTypes); - } + }*/ // 获取最近20条朋友圈 $moments = $query->page(1, 20)->select(); @@ -1289,7 +1290,7 @@ class ContentLibraryController extends Controller continue; } - // 如果启用了AI处理 + /* // 如果启用了AI处理 if (!empty($library['aiEnabled']) && !empty($content)) { try { $contentAi = $this->aiRewrite($library, $content); @@ -1300,7 +1301,7 @@ class ContentLibraryController extends Controller \think\facade\Log::error('AI处理失败: ' . $e->getMessage() . ' [朋友圈ID: ' . ($moment['id'] ?? 'unknown') . ']'); $moment['contentAi'] = ''; } - } + }*/ // 保存到内容库的content_item表 if ($this->saveMomentToContentItem($moment, $library['id'], $friend, $nickname)) { diff --git a/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php b/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php new file mode 100644 index 00000000..b34ef7ff --- /dev/null +++ b/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php @@ -0,0 +1,204 @@ +getUserInfo('companyId')) + ->column('id'); + } + + /** + * 非主操盘手仅可查看分配到的设备 + * + * @return array + */ + protected function getUserDevicesId(): array + { + return DeviceUserModel::where([ + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId'), + ])->column('deviceId'); + } + + /** + * 获取当前用户可访问的设备ID + * + * @return array + */ + protected function getDevicesId(): array + { + return ($this->getUserInfo('isAdmin') == UserModel::ADMIN_STP) + ? $this->getCompanyDevicesId() + : $this->getUserDevicesId(); + } + + /** + * 获取用户可访问的微信ID集合 + * + * @return array + * @throws \Exception + */ + protected function getAccessibleWechatIds(): array + { + $deviceIds = $this->getDevicesId(); + if (empty($deviceIds)) { + throw new \Exception('暂无可用设备', 200); + } + + return DeviceWechatLoginModel::distinct(true) + ->where('companyId', $this->getUserInfo('companyId')) + ->whereIn('deviceId', $deviceIds) + ->column('wechatId'); + } + + /** + * 查看朋友圈列表 + * + * @return \think\response\Json + */ + public function index() + { + try { + $wechatId = $this->request->param('wechatId/s', ''); + if (empty($wechatId)) { + return ResponseHelper::error('wechatId不能为空'); + } + + // 权限校验:只能查看当前账号可访问的微信 + $accessibleWechatIds = $this->getAccessibleWechatIds(); + if (!in_array($wechatId, $accessibleWechatIds, true)) { + return ResponseHelper::error('无权查看该微信的朋友圈', 403); + } + + // 获取对应的微信账号ID + $accountId = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->value('id'); + + if (empty($accountId)) { + return ResponseHelper::error('微信账号不存在或尚未同步', 404); + } + + $query = Db::table('s2_wechat_moments') + ->where('wechatAccountId', $accountId); + + // 关键词搜索 + if ($keyword = trim((string)$this->request->param('keyword', ''))) { + $query->whereLike('content', '%' . $keyword . '%'); + } + + // 类型筛选 + $type = $this->request->param('type', ''); + if ($type !== '' && $type !== null) { + $query->where('type', (int)$type); + } + + // 时间筛选 + $startTime = $this->request->param('startTime', ''); + $endTime = $this->request->param('endTime', ''); + if ($startTime || $endTime) { + $start = $startTime ? strtotime($startTime) : 0; + $end = $endTime ? strtotime($endTime) : time(); + if ($start && $end && $end < $start) { + return ResponseHelper::error('结束时间不能早于开始时间'); + } + $query->whereBetween('createTime', [$start ?: 0, $end ?: time()]); + } + + $page = (int)$this->request->param('page', 1); + $limit = (int)$this->request->param('limit', 10); + + $paginator = $query->order('createTime', 'desc') + ->paginate($limit, false, ['page' => $page]); + + $list = array_map(function ($item) { + return $this->formatMomentRow($item); + }, $paginator->items()); + + return ResponseHelper::success([ + 'list' => $list, + 'total' => $paginator->total(), + 'page' => $page, + 'limit' => $limit, + ]); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 格式化朋友圈数据 + * + * @param array $row + * @return array + */ + protected function formatMomentRow(array $row): array + { + $formatTime = function ($timestamp) { + if (empty($timestamp)) { + return ''; + } + return is_numeric($timestamp) + ? date('Y-m-d H:i:s', $timestamp) + : date('Y-m-d H:i:s', strtotime($timestamp)); + }; + + return [ + 'id' => (int)$row['id'], + 'snsId' => $row['snsId'] ?? '', + 'type' => (int)($row['type'] ?? 0), + 'content' => $row['content'] ?? '', + 'commentList' => $this->decodeJson($row['commentList'] ?? null), + 'likeList' => $this->decodeJson($row['likeList'] ?? null), + 'resUrls' => $this->decodeJson($row['resUrls'] ?? null), + 'createTime' => $formatTime($row['createTime'] ?? null), + 'momentEntity' => [ + 'lat' => $row['lat'] ?? 0, + 'lng' => $row['lng'] ?? 0, + 'location' => $row['location'] ?? '', + 'picSize' => $row['picSize'] ?? 0, + 'userName' => $row['userName'] ?? '', + ], + ]; + } + + /** + * JSON字段解析 + * + * @param mixed $value + * @return array + */ + protected function decodeJson($value): array + { + if (empty($value)) { + return []; + } + + if (is_array($value)) { + return $value; + } + + $decoded = json_decode($value, true); + return $decoded ?: []; + } +} + diff --git a/Server/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php b/Server/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php new file mode 100644 index 00000000..a48156de --- /dev/null +++ b/Server/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php @@ -0,0 +1,411 @@ +request->param('wechatId', ''); + + if (empty($wechatId)) { + return ResponseHelper::error('微信ID不能为空'); + } + + $companyId = $this->getUserInfo('companyId'); + + // 获取微信账号ID(accountId) + $account = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->find(); + + if (empty($account)) { + return ResponseHelper::error('微信账号不存在'); + } + + $accountId = $account['id']; + + // 1. 健康分评估 + $healthScoreData = $this->getHealthScoreAssessment($accountId, $wechatId); + + // 2. 账号价值(模拟数据) + $accountValue = $this->getAccountValue($accountId); + + // 3. 今日价值变化(模拟数据) + $todayValueChange = $this->getTodayValueChange($accountId); + + // 4. 好友总数 + $totalFriends = $this->getTotalFriends($wechatId, $companyId); + + // 5. 今日新增好友 + $todayNewFriends = $this->getTodayNewFriends($wechatId); + + // 6. 高价群聊 + $highValueChatrooms = $this->getHighValueChatrooms($wechatId, $companyId); + + // 7. 今日新增群聊 + $todayNewChatrooms = $this->getTodayNewChatrooms($wechatId, $companyId); + + $result = [ + 'healthScoreAssessment' => $healthScoreData, + 'accountValue' => $accountValue, + 'todayValueChange' => $todayValueChange, + 'totalFriends' => $totalFriends, + 'todayNewFriends' => $todayNewFriends, + 'highValueChatrooms' => $highValueChatrooms, + 'todayNewChatrooms' => $todayNewChatrooms, + ]; + + return ResponseHelper::success($result); + + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 获取健康分评估数据 + * + * @param int $accountId 账号ID + * @param string $wechatId 微信ID + * @return array + */ + protected function getHealthScoreAssessment($accountId, $wechatId) + { + // 获取健康分信息 + $healthScoreService = new WechatAccountHealthScoreService(); + $healthScoreInfo = $healthScoreService->getHealthScore($accountId); + + $healthScore = $healthScoreInfo['healthScore'] ?? 0; + $maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 0; + + // 获取今日已加好友数 + $todayAdded = $this->getTodayAddedCount($wechatId); + + // 获取最后添加时间 + $lastAddTime = $this->getLastAddTime($wechatId); + + // 判断状态标签 + $statusTag = $todayAdded > 0 ? '已添加加人' : ''; + + // 获取基础构成 + $baseComposition = $this->getBaseComposition($healthScoreInfo); + + // 获取动态记录 + $dynamicRecords = $this->getDynamicRecords($healthScoreInfo); + + return [ + 'score' => $healthScore, + 'dailyLimit' => $maxAddFriendPerDay, + 'todayAdded' => $todayAdded, + 'lastAddTime' => $lastAddTime, + 'statusTag' => $statusTag, + 'baseComposition' => $baseComposition, + 'dynamicRecords' => $dynamicRecords, + ]; + } + + /** + * 获取基础构成数据 + * + * @param array $healthScoreInfo 健康分信息 + * @return array + */ + protected function getBaseComposition($healthScoreInfo) + { + $baseScore = $healthScoreInfo['baseScore'] ?? 0; + $baseInfoScore = $healthScoreInfo['baseInfoScore'] ?? 0; + $friendCountScore = $healthScoreInfo['friendCountScore'] ?? 0; + $friendCount = $healthScoreInfo['friendCount'] ?? 0; + + // 账号基础分(默认60分) + $accountBaseScore = 60; + + // 已修改微信号(如果baseInfoScore > 0,说明已修改) + $isModifiedAlias = $baseInfoScore > 0; + + $composition = [ + [ + 'name' => '账号基础分', + 'score' => $accountBaseScore, + 'formatted' => '+' . $accountBaseScore, + ] + ]; + + // 如果已修改微信号,添加基础信息分 + if ($isModifiedAlias) { + $composition[] = [ + 'name' => '已修改微信号', + 'score' => $baseInfoScore, + 'formatted' => '+' . $baseInfoScore, + ]; + } + + // 好友数量加成 + if ($friendCountScore > 0) { + $composition[] = [ + 'name' => '好友数量加成', + 'score' => $friendCountScore, + 'formatted' => '+' . $friendCountScore, + 'friendCount' => $friendCount, // 显示好友总数 + ]; + } + + return $composition; + } + + /** + * 获取动态记录数据 + * + * @param array $healthScoreInfo 健康分信息 + * @return array + */ + protected function getDynamicRecords($healthScoreInfo) + { + $records = []; + + $frequentPenalty = $healthScoreInfo['frequentPenalty'] ?? 0; + $frequentCount = $healthScoreInfo['frequentCount'] ?? 0; + $banPenalty = $healthScoreInfo['banPenalty'] ?? 0; + $isBanned = $healthScoreInfo['isBanned'] ?? 0; + $noFrequentBonus = $healthScoreInfo['noFrequentBonus'] ?? 0; + $consecutiveNoFrequentDays = $healthScoreInfo['consecutiveNoFrequentDays'] ?? 0; + $lastFrequentTime = $healthScoreInfo['lastFrequentTime'] ?? null; + + // 频繁扣分记录 + // 根据frequentCount判断是首次还是再次 + // frequentPenalty存储的是当前状态的扣分(-15或-25),不是累计值 + if ($frequentCount > 0 && $frequentPenalty < 0) { + if ($frequentCount == 1) { + // 首次频繁:-15分 + $records[] = [ + 'name' => '首次触发限额', + 'score' => $frequentPenalty, + 'formatted' => (string)$frequentPenalty, + 'type' => 'penalty', + 'time' => $lastFrequentTime ? date('Y-m-d H:i:s', $lastFrequentTime) : null, + ]; + } else { + // 再次频繁:-25分 + $records[] = [ + 'name' => '再次触发限额', + 'score' => $frequentPenalty, + 'formatted' => (string)$frequentPenalty, + 'type' => 'penalty', + 'time' => $lastFrequentTime ? date('Y-m-d H:i:s', $lastFrequentTime) : null, + ]; + } + } + + // 封号扣分记录 + if ($isBanned && $banPenalty < 0) { + $lastBanTime = $healthScoreInfo['lastBanTime'] ?? null; + $records[] = [ + 'name' => '封号', + 'score' => $banPenalty, + 'formatted' => (string)$banPenalty, + 'type' => 'penalty', + 'time' => $lastBanTime ? date('Y-m-d H:i:s', $lastBanTime) : null, + ]; + } + + // 不频繁加分记录 + if ($noFrequentBonus > 0 && $consecutiveNoFrequentDays >= 3) { + $lastNoFrequentTime = $healthScoreInfo['lastNoFrequentTime'] ?? null; + $records[] = [ + 'name' => '连续' . $consecutiveNoFrequentDays . '天不触发频繁', + 'score' => $noFrequentBonus, + 'formatted' => '+' . $noFrequentBonus, + 'type' => 'bonus', + 'time' => $lastNoFrequentTime ? date('Y-m-d H:i:s', $lastNoFrequentTime) : null, + ]; + } + + return $records; + } + + /** + * 获取今日已加好友数 + * + * @param string $wechatId 微信ID + * @return int + */ + protected function getTodayAddedCount($wechatId) + { + $start = strtotime(date('Y-m-d 00:00:00')); + $end = strtotime(date('Y-m-d 23:59:59')); + + return Db::table('s2_friend_task') + ->where('wechatId', $wechatId) + ->whereBetween('createTime', [$start, $end]) + ->count(); + } + + /** + * 获取最后添加时间 + * + * @param string $wechatId 微信ID + * @return string + */ + protected function getLastAddTime($wechatId) + { + $lastTask = Db::table('s2_friend_task') + ->where('wechatId', $wechatId) + ->order('createTime', 'desc') + ->find(); + + if (empty($lastTask) || empty($lastTask['createTime'])) { + return ''; + } + + return date('H:i:s', $lastTask['createTime']); + } + + /** + * 获取账号价值(模拟数据) + * + * @param int $accountId 账号ID + * @return array + */ + protected function getAccountValue($accountId) + { + // TODO: 后续替换为真实计算逻辑 + // 模拟数据:¥29,800 + $value = 29800; + + return [ + 'value' => $value, + 'formatted' => '¥' . number_format($value, 0, '.', ','), + ]; + } + + /** + * 获取今日价值变化(模拟数据) + * + * @param int $accountId 账号ID + * @return array + */ + protected function getTodayValueChange($accountId) + { + // TODO: 后续替换为真实计算逻辑 + // 模拟数据:+500 + $change = 500; + + return [ + 'change' => $change, + 'formatted' => $change > 0 ? '+' . $change : (string)$change, + 'isPositive' => $change > 0, + ]; + } + + /** + * 获取好友总数 + * + * @param string $wechatId 微信ID + * @param int $companyId 公司ID + * @return int + */ + protected function getTotalFriends($wechatId, $companyId) + { + // 优先从 s2_wechat_account 表获取 + $account = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->field('totalFriend') + ->find(); + + if (!empty($account) && isset($account['totalFriend'])) { + return (int)$account['totalFriend']; + } + + // 如果 totalFriend 为空,则从 s2_wechat_friend 表统计 + return Db::table('s2_wechat_friend') + ->where('ownerWechatId', $wechatId) + ->where('isDeleted', 0) + ->count(); + } + + /** + * 获取今日新增好友数 + * + * @param string $wechatId 微信ID + * @return int + */ + protected function getTodayNewFriends($wechatId) + { + $start = strtotime(date('Y-m-d 00:00:00')); + $end = strtotime(date('Y-m-d 23:59:59')); + + // 从 s2_wechat_friend 表统计今日新增 + return Db::table('s2_wechat_friend') + ->where('ownerWechatId', $wechatId) + ->whereBetween('createTime', [$start, $end]) + ->where('isDeleted', 0) + ->count(); + } + + /** + * 获取高价群聊数量 + * 高价群聊定义:群成员数 >= 50 的群聊 + * + * @param string $wechatId 微信ID + * @param int $companyId 公司ID + * @return int + */ + protected function getHighValueChatrooms($wechatId, $companyId) + { + // 高价群聊定义:群成员数 >= 50 + $minMemberCount = 50; + + // 查询该微信账号下的高价群聊 + // 使用子查询统计每个群的成员数 + $result = Db::query(" + SELECT COUNT(DISTINCT c.chatroomId) as count + FROM s2_wechat_chatroom c + INNER JOIN ( + SELECT chatroomId, COUNT(*) as memberCount + FROM s2_wechat_chatroom_member + GROUP BY chatroomId + HAVING memberCount >= ? + ) m ON c.chatroomId = m.chatroomId + WHERE c.wechatAccountWechatId = ? + AND c.isDeleted = 0 + ", [$minMemberCount, $wechatId]); + + return !empty($result) ? (int)$result[0]['count'] : 0; + } + + /** + * 获取今日新增群聊数 + * + * @param string $wechatId 微信ID + * @param int $companyId 公司ID + * @return int + */ + protected function getTodayNewChatrooms($wechatId, $companyId) + { + $start = strtotime(date('Y-m-d 00:00:00')); + $end = strtotime(date('Y-m-d 23:59:59')); + + return Db::table('s2_wechat_chatroom') + ->where('wechatAccountWechatId', $wechatId) + ->whereBetween('createTime', [$start, $end]) + ->where('isDeleted', 0) + ->count(); + } +} + diff --git a/Server/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php b/Server/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php index 177d67a6..0b50aa99 100644 --- a/Server/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php +++ b/Server/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php @@ -8,14 +8,25 @@ use app\common\model\DeviceUser as DeviceUserModel; use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel; use app\common\model\User as UserModel; use app\common\model\WechatAccount as WechatAccountModel; -use app\common\model\WechatCustomer as WechatCustomerModel; -use app\common\model\WechatFriendShip as WechatFriendShipModel; +// 不再使用WechatFriendShipModel和WechatCustomerModel,改为直接查询s2_wechat_friend和s2_wechat_account_score表 use app\cunkebao\controller\BaseController; use library\ResponseHelper; use think\Db; /** * 微信控制器 + * + * 性能优化建议: + * 1. 为以下字段添加索引以提高查询性能: + * - device_wechat_login表: (companyId, wechatId), (deviceId) + * - wechat_account表: (wechatId) + * - wechat_customer表: (companyId, wechatId) + * - wechat_friend_ship表: (ownerWechatId), (createTime) + * - s2_wechat_message表: (wechatAccountId, wechatTime) + * + * 2. 考虑创建以下复合索引: + * - device_wechat_login表: (companyId, deviceId, wechatId) + * - wechat_friend_ship表: (ownerWechatId, createTime) */ class GetWechatsOnDevicesV1Controller extends BaseController { @@ -66,6 +77,7 @@ class GetWechatsOnDevicesV1Controller extends BaseController /** * 获取有登录设备的微信id + * 优化:使用索引字段,减少数据查询量 * * @return array */ @@ -76,12 +88,12 @@ class GetWechatsOnDevicesV1Controller extends BaseController throw new \Exception('暂无设备数据', 200); } - return DeviceWechatLoginModel::where( - [ + // 优化:直接使用DISTINCT减少数据传输量 + return DeviceWechatLoginModel::distinct(true) + ->where([ 'companyId' => $this->getUserInfo('companyId'), // 'alive' => DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE, - ] - ) + ]) ->where('deviceId', 'in', $deviceIds) ->column('wechatId'); } @@ -110,24 +122,50 @@ class GetWechatsOnDevicesV1Controller extends BaseController /** * 获取在线微信账号列表 + * 优化:减少查询字段,使用索引,优化JOIN条件 * * @param array $where * @return \think\Paginator 分页对象 */ protected function getOnlineWechatList(array $where): \think\Paginator { + // 获取微信在线状态筛选参数(1=在线,0=离线,不传=全部) + $wechatStatus = $this->request->param('wechatStatus'); + + // 优化:只查询必要字段,使用FORCE INDEX提示数据库使用索引 $query = WechatAccountModel::alias('w') ->field( [ 'w.id', 'w.nickname', 'w.avatar', 'w.wechatId', 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatAccount', - 'l.deviceId','l.alive' + 'MAX(l.deviceId) as deviceId', 'MAX(l.alive) as alive' // 使用MAX确保GROUP BY时获取正确的在线状态 ] ) - ->join('device_wechat_login l', 'w.wechatId = l.wechatId AND l.companyId = '. $this->getUserInfo('companyId')) - ->order('w.id desc') - ->group('w.wechatId'); + // 优化:使用INNER JOIN代替LEFT JOIN,并添加索引提示 + ->join('device_wechat_login l', 'w.wechatId = l.wechatId AND l.companyId = '. $this->getUserInfo('companyId'), 'INNER') + // 添加s2_wechat_account表的LEFT JOIN,用于筛选微信在线状态 + ->join(['s2_wechat_account' => 'sa'], 'w.wechatId = sa.wechatId', 'LEFT') + ->group('w.wechatId') + // 优化:在线状态优先排序(alive=1的排在前面),然后按wechatId排序 + // 注意:ORDER BY使用SELECT中定义的别名alive,而不是聚合函数 + ->order('alive desc, w.wechatId desc'); + + // 根据wechatStatus参数筛选(1=在线,0=离线,不传=全部) + if ($wechatStatus !== null && $wechatStatus !== '') { + $wechatStatus = (int)$wechatStatus; + if ($wechatStatus === 1) { + // 筛选在线:wechatAlive = 1 + $query->where('sa.wechatAlive', 1); + } elseif ($wechatStatus === 0) { + // 筛选离线:wechatAlive = 0 或 NULL + $query->where(function($query) { + $query->where('sa.wechatAlive', 0) + ->whereOr('sa.wechatAlive', 'exp', 'IS NULL'); + }); + } + } + // 应用查询条件 foreach ($where as $key => $value) { if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') { $query->whereExp('', $value[1]); @@ -142,7 +180,12 @@ class GetWechatsOnDevicesV1Controller extends BaseController $query->where($key, $value); } - return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); + // 优化:使用简单计数查询 + return $query->paginate( + $this->request->param('limit/d', 10), + false, + ['page' => $this->request->param('page/d', 1)] + ); } /** @@ -167,9 +210,15 @@ class GetWechatsOnDevicesV1Controller extends BaseController $metrics = $this->collectWechatMetrics($wechatIds); foreach ($items as $item) { + $addLimit = $metrics['addLimit'][$item->wechatId] ?? 0; + $todayAdded = $metrics['todayAdded'][$item->wechatId] ?? 0; + // 计算今日可添加数量 = 可添加额度 - 今日已添加 + $todayCanAdd = max(0, $addLimit - $todayAdded); + $sections = $item->toArray() + [ - 'times' => $metrics['addLimit'][$item->wechatId] ?? 0, - 'addedCount' => $metrics['todayAdded'][$item->wechatId] ?? 0, + 'times' => $addLimit, + 'addedCount' => $todayAdded, + 'todayCanAdd' => $todayCanAdd, // 今日可添加数量 'wechatStatus' => $metrics['wechatStatus'][$item->wechatId] ?? 0, 'totalFriend' => $metrics['totalFriend'][$item->wechatId] ?? 0, 'deviceMemo' => $metrics['deviceMemo'][$item->wechatId] ?? '', @@ -184,6 +233,8 @@ class GetWechatsOnDevicesV1Controller extends BaseController /** * 批量收集微信账号的统计信息 + * 优化:合并查询,减少数据库访问次数,使用缓存 + * * @param array $wechatIds * @return array */ @@ -203,106 +254,167 @@ class GetWechatsOnDevicesV1Controller extends BaseController } $companyId = $this->getUserInfo('companyId'); - - // 可添加好友额度 - $weightRows = WechatCustomerModel::where('companyId', $companyId) + + // 使用缓存键,避免短时间内重复查询 + $cacheKey = 'wechat_metrics_' . md5(implode(',', $wechatIds) . '_' . $companyId); + + // 尝试从缓存获取数据(缓存5分钟) + $cachedMetrics = cache($cacheKey); + if ($cachedMetrics) { + return $cachedMetrics; + } + + // 优化1:可添加好友额度 - 从s2_wechat_account_score表获取maxAddFriendPerDay + $scoreRows = Db::table('s2_wechat_account_score') ->whereIn('wechatId', $wechatIds) - ->column('weight', 'wechatId'); - foreach ($weightRows as $wechatId => $weight) { - $decoded = json_decode($weight, true); - $metrics['addLimit'][$wechatId] = $decoded['addLimit'] ?? 0; + ->column('maxAddFriendPerDay', 'wechatId'); + foreach ($scoreRows as $wechatId => $maxAddFriendPerDay) { + $metrics['addLimit'][$wechatId] = (int)($maxAddFriendPerDay ?? 0); } - // 今日新增好友 + // 优化2:今日新增好友 - 使用索引字段和预计算 $start = strtotime(date('Y-m-d 00:00:00')); $end = strtotime(date('Y-m-d 23:59:59')); - $todayRows = WechatFriendShipModel::whereIn('ownerWechatId', $wechatIds) - ->whereBetween('createTime', [$start, $end]) - ->field('ownerWechatId, COUNT(*) as total') - ->group('ownerWechatId') - ->select(); - foreach ($todayRows as $row) { - $wechatId = is_array($row) ? ($row['ownerWechatId'] ?? '') : ($row->ownerWechatId ?? ''); + + // 使用单次查询获取所有wechatIds的今日新增和总好友数 + // 根据数据库结构使用s2_wechat_friend表而不是wechat_friend_ship + $friendshipStats = Db::query(" + SELECT + ownerWechatId, + SUM(IF(createTime BETWEEN {$start} AND {$end}, 1, 0)) as today_added, + COUNT(*) as total_friend + FROM + s2_wechat_friend + WHERE + ownerWechatId IN ('" . implode("','", $wechatIds) . "') + AND isDeleted = 0 + GROUP BY + ownerWechatId + "); + + // 处理结果 + foreach ($friendshipStats as $row) { + $wechatId = $row['ownerWechatId'] ?? ''; if ($wechatId) { - $metrics['todayAdded'][$wechatId] = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0)); + $metrics['todayAdded'][$wechatId] = (int)($row['today_added'] ?? 0); + $metrics['totalFriend'][$wechatId] = (int)($row['total_friend'] ?? 0); } } - // 总好友 - $friendRows = WechatFriendShipModel::whereIn('ownerWechatId', $wechatIds) - ->field('ownerWechatId, COUNT(*) as total') - ->group('ownerWechatId') + // 优化3:微信在线状态 - 从s2_wechat_account表获取wechatAlive + $wechatAccountRows = Db::table('s2_wechat_account') + ->whereIn('wechatId', $wechatIds) + ->field('wechatId, wechatAlive') ->select(); - foreach ($friendRows as $row) { - $wechatId = is_array($row) ? ($row['ownerWechatId'] ?? '') : ($row->ownerWechatId ?? ''); - if ($wechatId) { - $metrics['totalFriend'][$wechatId] = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0)); + + foreach ($wechatAccountRows as $row) { + $wechatId = $row['wechatId'] ?? ''; + if (!empty($wechatId)) { + $metrics['wechatStatus'][$wechatId] = (int)($row['wechatAlive'] ?? 0); } } - // 设备状态与备注 + // 优化4:设备状态与备注 - 使用INNER JOIN和索引 $loginRows = Db::name('device_wechat_login') ->alias('l') - ->leftJoin('device d', 'd.id = l.deviceId') - ->field('l.wechatId,l.alive,d.memo') + ->join('device d', 'd.id = l.deviceId', 'LEFT') + ->field('l.wechatId, l.alive, d.memo') ->where('l.companyId', $companyId) ->whereIn('l.wechatId', $wechatIds) ->order('l.id', 'desc') ->select(); + + // 使用临时数组避免重复处理 + $processedWechatIds = []; foreach ($loginRows as $row) { - $wechatId = is_array($row) ? ($row['wechatId'] ?? '') : ($row->wechatId ?? ''); - if (empty($wechatId) || isset($metrics['wechatStatus'][$wechatId])) { - continue; + $wechatId = $row['wechatId'] ?? ''; + // 只处理每个wechatId的第一条记录(最新的) + if (!empty($wechatId) && !in_array($wechatId, $processedWechatIds)) { + // 如果s2_wechat_account表中没有wechatAlive,则使用device_wechat_login的alive作为备用 + if (!isset($metrics['wechatStatus'][$wechatId])) { + $metrics['wechatStatus'][$wechatId] = (int)($row['alive'] ?? 0); + } + $metrics['deviceMemo'][$wechatId] = $row['memo'] ?? ''; + $processedWechatIds[] = $wechatId; } - $metrics['wechatStatus'][$wechatId] = (int)(is_array($row) ? ($row['alive'] ?? 0) : ($row->alive ?? 0)); - $metrics['deviceMemo'][$wechatId] = is_array($row) ? ($row['memo'] ?? '') : ($row->memo ?? ''); } - // 活跃时间 - $accountMap = Db::table('s2_wechat_account') - ->whereIn('wechatId', $wechatIds) - ->column('id', 'wechatId'); - if (!empty($accountMap)) { - $accountRows = Db::table('s2_wechat_message') - ->whereIn('wechatAccountId', array_values($accountMap)) - ->field('wechatAccountId, MAX(wechatTime) as lastTime') - ->group('wechatAccountId') - ->select(); - $accountLastTime = []; - foreach ($accountRows as $row) { - $accountId = is_array($row) ? ($row['wechatAccountId'] ?? 0) : ($row->wechatAccountId ?? 0); - if ($accountId) { - $accountLastTime[$accountId] = (int)(is_array($row) ? ($row['lastTime'] ?? 0) : ($row->lastTime ?? 0)); - } - } - foreach ($accountMap as $wechatId => $accountId) { - if (isset($accountLastTime[$accountId]) && $accountLastTime[$accountId] > 0) { - $metrics['activeTime'][$wechatId] = date('Y-m-d H:i:s', $accountLastTime[$accountId]); - } + // 优化5:活跃时间 - 使用JOIN减少查询次数 + $activeTimeResults = Db::query(" + SELECT + a.wechatId, + MAX(m.wechatTime) as lastTime + FROM + s2_wechat_account a + LEFT JOIN + s2_wechat_message m ON a.id = m.wechatAccountId + WHERE + a.wechatId IN ('" . implode("','", $wechatIds) . "') + GROUP BY + a.wechatId + "); + + foreach ($activeTimeResults as $row) { + $wechatId = $row['wechatId'] ?? ''; + $lastTime = (int)($row['lastTime'] ?? 0); + if (!empty($wechatId) && $lastTime > 0) { + $metrics['activeTime'][$wechatId] = date('Y-m-d H:i:s', $lastTime); + } else { + $metrics['activeTime'][$wechatId] = '-'; } } + + // 确保所有wechatId都有wechatStatus值(默认0) + foreach ($wechatIds as $wechatId) { + if (!isset($metrics['wechatStatus'][$wechatId])) { + $metrics['wechatStatus'][$wechatId] = 0; + } + } + + // 存入缓存,有效期5分钟 + cache($cacheKey, $metrics, 300); return $metrics; } /** * 获取在线微信账号列表 + * 优化:添加缓存,优化分页逻辑 * * @return \think\response\Json */ public function index() { try { + // 获取分页参数 + $page = $this->request->param('page/d', 1); + $limit = $this->request->param('limit/d', 10); + $keyword = $this->request->param('keyword'); + $wechatStatus = $this->request->param('wechatStatus'); + + // 创建缓存键(基于用户、分页、搜索条件和在线状态筛选) + $cacheKey = 'wechat_list_' . $this->getUserInfo('id') . '_' . $page . '_' . $limit . '_' . md5($keyword ?? '') . '_' . ($wechatStatus ?? 'all'); + + // 尝试从缓存获取数据(缓存2分钟) + $cachedData = cache($cacheKey); + if ($cachedData) { + return ResponseHelper::success($cachedData); + } + + // 如果没有缓存,执行查询 $result = $this->getOnlineWechatList( $this->makeWhere() ); + + $responseData = [ + 'list' => $this->makeResultedSet($result), + 'total' => $result->total(), + ]; + + // 存入缓存,有效期2分钟 + cache($cacheKey, $responseData, 120); - return ResponseHelper::success( - [ - 'list' => $this->makeResultedSet($result), - 'total' => $result->total(), - ] - ); + return ResponseHelper::success($responseData); } catch (\Exception $e) { return ResponseHelper::error($e->getMessage(), $e->getCode()); } diff --git a/Server/sql.sql b/Server/sql.sql index b49716bc..9e6fe53d 100644 --- a/Server/sql.sql +++ b/Server/sql.sql @@ -11,7 +11,7 @@ Target Server Version : 50736 File Encoding : 65001 - Date: 12/11/2025 11:05:39 + Date: 24/11/2025 16:50:43 */ SET NAMES utf8mb4; @@ -123,7 +123,7 @@ CREATE TABLE `ck_app_version` ( `updateContent` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `createTime` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_attachments @@ -145,7 +145,7 @@ CREATE TABLE `ck_attachments` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_hash_key`(`hash_key`) USING BTREE, INDEX `idx_server`(`server`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 481 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '附件表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 505 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '附件表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_call_recording @@ -222,7 +222,7 @@ CREATE TABLE `ck_content_item` ( INDEX `idx_wechatid`(`wechatId`) USING BTREE, INDEX `idx_friendid`(`friendId`) USING BTREE, INDEX `idx_create_time`(`createTime`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 5876 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容项目表-存储朋友圈采集数据' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 5993 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容项目表-存储朋友圈采集数据' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_content_library @@ -252,7 +252,7 @@ CREATE TABLE `ck_content_library` ( `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 87 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容库表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 100 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容库表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_coze_conversation @@ -272,7 +272,7 @@ CREATE TABLE `ck_coze_conversation` ( UNIQUE INDEX `idx_conversation_id`(`conversation_id`) USING BTREE, INDEX `idx_bot_id`(`bot_id`) USING BTREE, INDEX `idx_create_time`(`create_time`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 39 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Coze AI 会话表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 56 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Coze AI 会话表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_coze_message @@ -331,7 +331,7 @@ CREATE TABLE `ck_customer_acquisition_task` ( `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', `apiKey` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 162 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客计划表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 168 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客计划表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_device @@ -372,7 +372,7 @@ CREATE TABLE `ck_device_handle_log` ( `companyId` int(11) NULL DEFAULT NULL COMMENT '租户id', `createTime` int(11) NULL DEFAULT NULL COMMENT '操作时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 304 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 339 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_device_taskconf @@ -395,7 +395,7 @@ CREATE TABLE `ck_device_taskconf` ( `updateTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '更新时间', `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 29 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备任务配置表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 30 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备任务配置表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_device_user @@ -425,7 +425,7 @@ CREATE TABLE `ck_device_wechat_login` ( `isTips` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否提示迁移', PRIMARY KEY (`id`) USING BTREE, INDEX `wechatId`(`wechatId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 309 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备登录微信记录表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 312 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备登录微信记录表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_flow_package @@ -653,7 +653,7 @@ CREATE TABLE `ck_kf_follow_up` ( INDEX `idx_level`(`type`) USING BTREE, INDEX `idx_isRemind`(`isRemind`) USING BTREE, INDEX `idx_isProcess`(`isProcess`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '跟进提醒' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '跟进提醒' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_kf_friend_settings @@ -675,7 +675,7 @@ CREATE TABLE `ck_kf_friend_settings` ( INDEX `idx_userId`(`userId`) USING BTREE, INDEX `idx_wechatAccountId`(`wechatAccountId`) USING BTREE, INDEX `idx_friendId`(`friendId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '好友AI配置' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 42 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '好友AI配置' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_kf_keywords @@ -769,7 +769,7 @@ CREATE TABLE `ck_kf_notice` ( `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', `readTime` int(12) NULL DEFAULT NULL COMMENT '读取时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 246 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '通知消息' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 247 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '通知消息' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_kf_questions @@ -810,7 +810,7 @@ CREATE TABLE `ck_kf_reply` ( `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 130746 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 130751 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_kf_reply_group @@ -977,7 +977,7 @@ CREATE TABLE `ck_task_customer` ( INDEX `addTime`(`addTime`) USING BTREE, INDEX `passTime`(`passTime`) USING BTREE, INDEX `updateTime`(`updateTime`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 24192 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 28204 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_tokens_company @@ -990,7 +990,7 @@ CREATE TABLE `ck_tokens_company` ( `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公司算力账户' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公司算力账户' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_tokens_package @@ -1033,7 +1033,7 @@ CREATE TABLE `ck_tokens_record` ( `remarks` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 236 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '算力明细记录' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 273 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '算力明细记录' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_traffic_order @@ -1070,8 +1070,9 @@ CREATE TABLE `ck_traffic_pool` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uni_identifier`(`identifier`) USING BTREE, INDEX `idx_wechatId`(`wechatId`) USING BTREE, - INDEX `idx_mobile`(`mobile`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 959687 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池' ROW_FORMAT = Dynamic; + INDEX `idx_mobile`(`mobile`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1063510 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_traffic_profile @@ -1114,8 +1115,9 @@ CREATE TABLE `ck_traffic_source` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_identifier_sourceId_sceneId`(`identifier`, `sourceId`, `sceneId`) USING BTREE, INDEX `idx_identifier`(`identifier`) USING BTREE, - INDEX `idx_companyId`(`companyId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 564508 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量来源' ROW_FORMAT = Dynamic; + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_company_status_time`(`companyId`, `status`, `updateTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 573831 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量来源' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_traffic_source_package @@ -1242,7 +1244,7 @@ CREATE TABLE `ck_user_portrait` ( `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 17718 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户画像' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 19014 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户画像' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_users @@ -1267,7 +1269,7 @@ CREATE TABLE `ck_users` ( `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1652 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 1658 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_vendor_order @@ -1360,7 +1362,7 @@ CREATE TABLE `ck_wechat_account` ( `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uni_wechatId`(`wechatId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 3097959 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 3614968 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_wechat_customer @@ -1378,7 +1380,7 @@ CREATE TABLE `ck_wechat_customer` ( `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uni_wechatId`(`wechatId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 153 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信客服信息' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 154 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信客服信息' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_wechat_friendship @@ -1434,7 +1436,7 @@ CREATE TABLE `ck_wechat_group_member` ( `deleteTime` int(11) UNSIGNED NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_identifier_chatroomId_groupId`(`identifier`, `chatroomId`, `groupId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 549847 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 554147 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_wechat_restricts @@ -1451,7 +1453,7 @@ CREATE TABLE `ck_wechat_restricts` ( `restrictTime` int(11) NULL DEFAULT NULL COMMENT '限制日期', `recoveryTime` int(11) NULL DEFAULT NULL COMMENT '恢复日期', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1302 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信风险受限记录' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 1319 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信风险受限记录' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_wechat_tag @@ -1489,7 +1491,7 @@ CREATE TABLE `ck_workbench` ( INDEX `idx_user_id`(`userId`) USING BTREE, INDEX `idx_type`(`type`) USING BTREE, INDEX `idx_status`(`status`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 275 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台主表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 282 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台主表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_workbench_auto_like @@ -1534,7 +1536,7 @@ CREATE TABLE `ck_workbench_auto_like_item` ( INDEX `wechatFriendId`(`wechatFriendId`) USING BTREE, INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE, INDEX `momentsId`(`momentsId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 4639 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台-自动点赞记录' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 4653 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台-自动点赞记录' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_workbench_group_create @@ -1604,9 +1606,14 @@ CREATE TABLE `ck_workbench_group_push` ( `promotionSiteId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '京东广告位', `trafficPools` json NULL COMMENT '流量池', `devices` json NULL, + `groupPushSubType` tinyint(2) NULL DEFAULT 1 COMMENT '群推送子类型 1=群群发,2=群公告', + `announcementContent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `enableAiRewrite` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `aiRewritePrompt` tinyint(2) NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_workbench_id`(`workbenchId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '群消息推送扩展表' ROW_FORMAT = Dynamic; + INDEX `idx_workbench_id`(`workbenchId`) USING BTREE, + INDEX `idx_status_targetType`(`status`, `targetType`, `workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '群消息推送扩展表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_workbench_group_push_item @@ -1622,7 +1629,9 @@ CREATE TABLE `ck_workbench_group_push_item` ( `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', `isLoop` tinyint(2) NULL DEFAULT 0 COMMENT '是否循环完成', `createTime` int(11) NOT NULL COMMENT '创建时间', - PRIMARY KEY (`id`) USING BTREE + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_target_time`(`workbenchId`, `targetType`, `createTime`) USING BTREE, + INDEX `idx_workbench_target_friend`(`workbenchId`, `targetType`, `friendId`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 302 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- @@ -1677,7 +1686,7 @@ CREATE TABLE `ck_workbench_moments_sync` ( `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_workbench_id`(`workbenchId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 47 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_workbench_moments_sync_item @@ -1691,8 +1700,10 @@ CREATE TABLE `ck_workbench_moments_sync_item` ( `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', `createTime` int(11) NOT NULL COMMENT '创建时间', `isLoop` tinyint(2) NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1650 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_time`(`workbenchId`, `createTime`) USING BTREE, + INDEX `idx_workbench_content`(`workbenchId`, `contentId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1785 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_workbench_traffic_config @@ -1714,7 +1725,7 @@ CREATE TABLE `ck_workbench_traffic_config` ( `updateTime` int(11) NOT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uniq_workbench`(`workbenchId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_workbench_traffic_config_item @@ -1736,7 +1747,7 @@ CREATE TABLE `ck_workbench_traffic_config_item` ( INDEX `deviceId`(`deviceId`) USING BTREE, INDEX `wechatFriendId`(`wechatFriendId`) USING BTREE, INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 49898 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 54241 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for s2_allot_rule @@ -1869,6 +1880,7 @@ CREATE TABLE `s2_device` ( `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分组名称', `wechatAccounts` json NULL COMMENT '微信账号列表JSON', `alive` tinyint(1) NULL DEFAULT 0 COMMENT '是否在线', + `aliveTime` int(11) NULL DEFAULT 0, `lastAliveTime` int(11) NULL DEFAULT NULL COMMENT '最后在线时间', `tenantId` int(11) NULL DEFAULT NULL COMMENT '租户ID', `groupId` int(11) NULL DEFAULT NULL COMMENT '分组ID', @@ -1938,6 +1950,7 @@ CREATE TABLE `s2_friend_task` ( `accountRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', `accountUsername` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间戳', + `is_counted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)', UNIQUE INDEX `uk_task_id`(`id`) USING BTREE, INDEX `idx_tenant_id`(`tenantId`) USING BTREE, INDEX `idx_operator_account_id`(`operatorAccountId`) USING BTREE, @@ -2064,6 +2077,7 @@ CREATE TABLE `s2_wechat_account` ( `keFuAlive` tinyint(1) NULL DEFAULT 0 COMMENT '客服是否在线', `deviceAlive` tinyint(1) NULL DEFAULT 0 COMMENT '设备是否在线', `wechatAlive` tinyint(1) NULL DEFAULT 0 COMMENT '微信是否在线', + `wechatAliveTime` int(11) NULL DEFAULT 0 COMMENT '在线时间', `yesterdayMsgCount` int(11) NULL DEFAULT 0 COMMENT '昨日消息数', `sevenDayMsgCount` int(11) NULL DEFAULT 0 COMMENT '7天消息数', `thirtyDayMsgCount` int(11) NULL DEFAULT 0 COMMENT '30天消息数', @@ -2092,9 +2106,82 @@ CREATE TABLE `s2_wechat_account` ( `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', `status` tinyint(3) NULL DEFAULT 1 COMMENT '状态值', - INDEX `idx_wechat_id`(`wechatId`) USING BTREE + `healthScore` int(11) NULL DEFAULT 60 COMMENT '健康分总分(基础分+动态分)', + `baseScore` int(11) NULL DEFAULT 60 COMMENT '基础分(60-100分)', + `dynamicScore` int(11) NULL DEFAULT 0 COMMENT '动态分(扣分和加分)', + `isModifiedAlias` tinyint(1) NULL DEFAULT 0 COMMENT '是否已修改微信号(0=未修改,1=已修改)', + `lastFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后频繁时间(时间戳)', + `frequentCount` int(11) NULL DEFAULT 0 COMMENT '频繁次数(用于判断首次/再次频繁)', + `lastNoFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后不频繁时间(时间戳)', + `consecutiveNoFrequentDays` int(11) NULL DEFAULT 0 COMMENT '连续不频繁天数(用于加分)', + `scoreUpdateTime` int(11) NULL DEFAULT NULL COMMENT '评分更新时间', + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_health_score`(`healthScore`) USING BTREE, + INDEX `idx_is_modified_alias`(`isModifiedAlias`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; +-- ---------------------------- +-- Table structure for s2_wechat_account_score +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_account_score`; +CREATE TABLE `s2_wechat_account_score` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `accountId` int(11) NOT NULL COMMENT '微信账号ID(s2_wechat_account.id)', + `wechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `baseScore` int(11) NOT NULL DEFAULT 0 COMMENT '基础分(60-100分)', + `baseScoreCalculated` tinyint(1) NOT NULL DEFAULT 0 COMMENT '基础分是否已计算(0=未计算,1=已计算)', + `baseScoreCalcTime` int(11) NULL DEFAULT NULL COMMENT '基础分计算时间', + `baseInfoScore` int(11) NOT NULL DEFAULT 0 COMMENT '基础信息分(0-10分)', + `isModifiedAlias` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已修改微信号(0=未修改,1=已修改)', + `friendCountScore` int(11) NOT NULL DEFAULT 0 COMMENT '好友数量分(0-30分)', + `friendCount` int(11) NOT NULL DEFAULT 0 COMMENT '好友数量(评分时的快照)', + `friendCountSource` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友数量来源(manual=手动,sync=同步)', + `dynamicScore` int(11) NOT NULL DEFAULT 0 COMMENT '动态分(扣分和加分)', + `lastFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后频繁时间(时间戳)', + `frequentCount` int(11) NOT NULL DEFAULT 0 COMMENT '频繁次数(用于判断首次/再次频繁)', + `frequentPenalty` int(11) NOT NULL DEFAULT 0 COMMENT '频繁扣分(累计)', + `lastNoFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后不频繁时间(时间戳)', + `consecutiveNoFrequentDays` int(11) NOT NULL DEFAULT 0 COMMENT '连续不频繁天数', + `noFrequentBonus` int(11) NOT NULL DEFAULT 0 COMMENT '不频繁加分(累计)', + `isBanned` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否封号(0=否,1=是)', + `banPenalty` int(11) NOT NULL DEFAULT 0 COMMENT '封号扣分', + `healthScore` int(11) NOT NULL DEFAULT 0 COMMENT '健康分总分(基础分+动态分)', + `maxAddFriendPerDay` int(11) NOT NULL DEFAULT 0 COMMENT '每日最大加人次数', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + `lastBanTime` int(11) NULL DEFAULT NULL COMMENT '最后一次封号时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_account_id`(`accountId`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_health_score`(`healthScore`) USING BTREE, + INDEX `idx_base_score_calculated`(`baseScoreCalculated`) USING BTREE, + INDEX `idx_update_time`(`updateTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 363 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号评分记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_account_score_log +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_account_score_log`; +CREATE TABLE `s2_wechat_account_score_log` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `accountId` int(11) NOT NULL COMMENT '微信账号ID', + `wechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `field` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '变动字段(如frequentPenalty)', + `changeValue` int(11) NOT NULL DEFAULT 0 COMMENT '变动值(正加负减)', + `valueBefore` int(11) NULL DEFAULT NULL COMMENT '变更前的字段值', + `valueAfter` int(11) NULL DEFAULT NULL COMMENT '变更后的字段值', + `category` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分类:penalty/bonus/dynamic_total/health_total等', + `source` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '触发来源 friend_task/wechat_message/system', + `sourceId` bigint(20) NULL DEFAULT NULL COMMENT '关联记录ID(如任务/消息ID)', + `extra` json NULL COMMENT '附加信息(JSON)', + `totalScoreBefore` int(11) NULL DEFAULT NULL COMMENT '变更前健康总分', + `totalScoreAfter` int(11) NULL DEFAULT NULL COMMENT '变更后健康总分', + `createTime` int(11) NOT NULL COMMENT '记录时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_account_field`(`accountId`, `field`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号健康分加减分日志' ROW_FORMAT = Dynamic; + -- ---------------------------- -- Table structure for s2_wechat_chatroom -- ---------------------------- @@ -2150,7 +2237,7 @@ CREATE TABLE `s2_wechat_chatroom_member` ( UNIQUE INDEX `uk_chatroom_wechat`(`chatroomId`, `wechatId`) USING BTREE, INDEX `chatroomId`(`chatroomId`) USING BTREE, INDEX `wechatId`(`wechatId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 495043 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 495174 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for s2_wechat_friend @@ -2198,6 +2285,9 @@ CREATE TABLE `s2_wechat_friend` ( `R` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', `F` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', `M` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `realName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名', + `company` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '公司', + `position` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '职位', UNIQUE INDEX `uk_owner_wechat_account`(`ownerWechatId`, `wechatId`, `wechatAccountId`) USING BTREE, INDEX `idx_wechat_account_id`(`wechatAccountId`) USING BTREE, INDEX `idx_wechat_id`(`wechatId`) USING BTREE, @@ -2257,6 +2347,7 @@ CREATE TABLE `s2_wechat_message` ( `msgId` bigint(20) NULL DEFAULT NULL COMMENT '消息ID', `recallId` tinyint(1) NULL DEFAULT 0 COMMENT '撤回ID', `isRead` tinyint(1) NULL DEFAULT 0 COMMENT '是否读取', + `is_counted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_wechatChatroomId`(`wechatChatroomId`) USING BTREE, INDEX `idx_wechatAccountId`(`wechatAccountId`) USING BTREE, @@ -2296,6 +2387,6 @@ CREATE TABLE `s2_wechat_moments` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `idx_sns_account`(`snsId`, `wechatAccountId`) USING BTREE, INDEX `idx_account_friend`(`wechatAccountId`, `wechatFriendId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 39669 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信朋友圈数据表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 40130 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信朋友圈数据表' ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;