微信号
This commit is contained in:
91
Cunkebao/api/wechat-accounts.ts
Normal file
91
Cunkebao/api/wechat-accounts.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { api } from "@/lib/api";
|
||||||
|
import {
|
||||||
|
ServerWechatAccountsResponse,
|
||||||
|
QueryWechatAccountParams,
|
||||||
|
WechatAccountDetailResponse
|
||||||
|
} from "@/types/wechat-account";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信账号列表
|
||||||
|
* @param params 查询参数
|
||||||
|
* @returns 微信账号列表响应
|
||||||
|
*/
|
||||||
|
export const fetchWechatAccountList = async (params: QueryWechatAccountParams = {}): Promise<ServerWechatAccountsResponse> => {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
|
// 添加查询参数
|
||||||
|
if (params.page) queryParams.append('page', params.page.toString());
|
||||||
|
if (params.limit) queryParams.append('limit', params.limit.toString());
|
||||||
|
if (params.keyword) queryParams.append('nickname', params.keyword); // 使用nickname作为关键词搜索参数
|
||||||
|
if (params.sort) queryParams.append('sort', params.sort);
|
||||||
|
if (params.order) queryParams.append('order', params.order);
|
||||||
|
|
||||||
|
// 发起API请求
|
||||||
|
return api.get<ServerWechatAccountsResponse>(`/v1/device/wechats?${queryParams.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信账号详情
|
||||||
|
* @param id 微信账号ID
|
||||||
|
* @returns 微信账号详情响应
|
||||||
|
*/
|
||||||
|
export const fetchWechatAccountDetail = async (id: string | number): Promise<WechatAccountDetailResponse> => {
|
||||||
|
return api.get<WechatAccountDetailResponse>(`/v1/device/wechats/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新微信账号状态
|
||||||
|
* @returns 刷新结果
|
||||||
|
*/
|
||||||
|
export const refreshWechatAccounts = async (): Promise<{ code: number; msg: string; data: any }> => {
|
||||||
|
return api.put<{ code: number; msg: string; data: any }>('/v1/device/wechats/refresh', {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行微信好友转移
|
||||||
|
* @param sourceId 源微信账号ID
|
||||||
|
* @param targetId 目标微信账号ID
|
||||||
|
* @returns 转移结果
|
||||||
|
*/
|
||||||
|
export const transferWechatFriends = async (sourceId: string | number, targetId: string | number): Promise<{ code: number; msg: string; data: any }> => {
|
||||||
|
return api.post<{ code: number; msg: string; data: any }>('/v1/device/wechats/transfer-friends', {
|
||||||
|
source_id: sourceId,
|
||||||
|
target_id: targetId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将服务器返回的微信账号数据转换为前端使用的格式
|
||||||
|
* @param serverAccount 服务器返回的微信账号数据
|
||||||
|
* @returns 前端使用的微信账号数据
|
||||||
|
*/
|
||||||
|
export const transformWechatAccount = (serverAccount: any): import("@/types/wechat-account").WechatAccount => {
|
||||||
|
// 从deviceInfo中提取设备信息
|
||||||
|
let deviceId = '';
|
||||||
|
let deviceName = '';
|
||||||
|
|
||||||
|
if (serverAccount.deviceInfo) {
|
||||||
|
const deviceInfo = serverAccount.deviceInfo.split(' ');
|
||||||
|
deviceId = deviceInfo[0] || '';
|
||||||
|
deviceName = deviceInfo[1] ? deviceInfo[1].replace(/[()]/g, '') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 假设每天最多可添加20个好友
|
||||||
|
const maxDailyAdds = 20;
|
||||||
|
const todayAdded = serverAccount.todayNewFriendCount || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: serverAccount.id.toString(),
|
||||||
|
avatar: serverAccount.avatar || '',
|
||||||
|
nickname: serverAccount.nickname || serverAccount.accountNickname || '未命名',
|
||||||
|
wechatId: serverAccount.wechatId || '',
|
||||||
|
deviceId,
|
||||||
|
deviceName,
|
||||||
|
friendCount: serverAccount.totalFriend || 0,
|
||||||
|
todayAdded,
|
||||||
|
remainingAdds: serverAccount.canAddFriendCount || (maxDailyAdds - todayAdded),
|
||||||
|
maxDailyAdds,
|
||||||
|
status: serverAccount.wechatAlive === 1 ? "normal" : "abnormal" as "normal" | "abnormal",
|
||||||
|
lastActive: new Date().toLocaleString() // 服务端未提供,使用当前时间
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { ChevronLeft, Filter, Search, RefreshCw, ArrowRightLeft, AlertCircle } from "lucide-react"
|
import { ChevronLeft, Filter, Search, RefreshCw, ArrowRightLeft, AlertCircle, Loader2 } from "lucide-react"
|
||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
@@ -20,73 +20,140 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
|
|||||||
import { toast } from "@/components/ui/use-toast"
|
import { toast } from "@/components/ui/use-toast"
|
||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress"
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
|
import { fetchWechatAccountList, refreshWechatAccounts, transferWechatFriends, transformWechatAccount } from "@/api/wechat-accounts"
|
||||||
interface WechatAccount {
|
import { WechatAccount } from "@/types/wechat-account"
|
||||||
id: string
|
|
||||||
avatar: string
|
|
||||||
nickname: string
|
|
||||||
wechatId: string
|
|
||||||
deviceId: string
|
|
||||||
deviceName: string
|
|
||||||
friendCount: number
|
|
||||||
todayAdded: number
|
|
||||||
remainingAdds: number
|
|
||||||
maxDailyAdds: number
|
|
||||||
status: "normal" | "abnormal"
|
|
||||||
lastActive: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateRandomWechatAccounts = (count: number): WechatAccount[] => {
|
|
||||||
return Array.from({ length: count }, (_, index) => ({
|
|
||||||
id: `account-${index + 1}`,
|
|
||||||
avatar:
|
|
||||||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02jn_e7fcc2a4-3560-478d-911a-4ccd69c6392g.jpg-a8zVtwxMuSrPWN9dfWH93EBY0yM3Dh.jpeg",
|
|
||||||
nickname: `卡若-${["25vig", "zok7e", "ip9ob", "2kna3"][index % 4]}`,
|
|
||||||
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
|
|
||||||
deviceId: `device-${Math.floor(index / 3) + 1}`,
|
|
||||||
deviceName: `设备${Math.floor(index / 3) + 1}`,
|
|
||||||
friendCount: Math.floor(Math.random() * (6300 - 520)) + 520,
|
|
||||||
todayAdded: Math.floor(Math.random() * 15),
|
|
||||||
remainingAdds: Math.floor(Math.random() * 10) + 5,
|
|
||||||
maxDailyAdds: 20,
|
|
||||||
status: Math.random() > 0.2 ? "normal" : "abnormal",
|
|
||||||
lastActive: new Date(Date.now() - Math.random() * 86400000).toLocaleString(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WechatAccountsPage() {
|
export default function WechatAccountsPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [accounts] = useState<WechatAccount[]>(generateRandomWechatAccounts(42))
|
const [accounts, setAccounts] = useState<WechatAccount[]>([])
|
||||||
const [searchQuery, setSearchQuery] = useState("")
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
const [isTransferDialogOpen, setIsTransferDialogOpen] = useState(false)
|
const [isTransferDialogOpen, setIsTransferDialogOpen] = useState(false)
|
||||||
const [selectedAccount, setSelectedAccount] = useState<WechatAccount | null>(null)
|
const [selectedAccount, setSelectedAccount] = useState<WechatAccount | null>(null)
|
||||||
|
const [totalAccounts, setTotalAccounts] = useState(0)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||||
const accountsPerPage = 10
|
const accountsPerPage = 10
|
||||||
|
|
||||||
const filteredAccounts = accounts.filter(
|
// 获取微信账号列表
|
||||||
(account) =>
|
const fetchAccounts = async (page: number = 1, keyword: string = "") => {
|
||||||
account.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
try {
|
||||||
account.wechatId.toLowerCase().includes(searchQuery.toLowerCase()),
|
setIsLoading(true);
|
||||||
)
|
const response = await fetchWechatAccountList({
|
||||||
|
page,
|
||||||
|
limit: accountsPerPage,
|
||||||
|
keyword,
|
||||||
|
sort: 'id',
|
||||||
|
order: 'desc'
|
||||||
|
});
|
||||||
|
|
||||||
const paginatedAccounts = filteredAccounts.slice((currentPage - 1) * accountsPerPage, currentPage * accountsPerPage)
|
if (response && response.code === 200 && response.data) {
|
||||||
|
// 转换数据格式
|
||||||
|
const wechatAccounts = response.data.list.map(transformWechatAccount);
|
||||||
|
setAccounts(wechatAccounts);
|
||||||
|
setTotalAccounts(response.data.total);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "获取微信账号失败",
|
||||||
|
description: response?.msg || "请稍后再试",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
// 如果API请求失败,设置空数组
|
||||||
|
setAccounts([]);
|
||||||
|
setTotalAccounts(0);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取微信账号列表失败:", error);
|
||||||
|
toast({
|
||||||
|
title: "获取微信账号失败",
|
||||||
|
description: "请检查网络连接或稍后再试",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
setAccounts([]);
|
||||||
|
setTotalAccounts(0);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const totalPages = Math.ceil(filteredAccounts.length / accountsPerPage)
|
// 刷新微信账号状态
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
try {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
const response = await refreshWechatAccounts();
|
||||||
|
|
||||||
|
if (response && response.code === 200) {
|
||||||
|
toast({
|
||||||
|
title: "刷新成功",
|
||||||
|
description: "微信账号状态已更新"
|
||||||
|
});
|
||||||
|
// 重新获取数据
|
||||||
|
await fetchAccounts(currentPage, searchQuery);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "刷新失败",
|
||||||
|
description: response?.msg || "请稍后再试",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("刷新微信账号状态失败:", error);
|
||||||
|
toast({
|
||||||
|
title: "刷新失败",
|
||||||
|
description: "请检查网络连接或稍后再试",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始加载和页码变化时获取数据
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAccounts(currentPage, searchQuery);
|
||||||
|
}, [currentPage]);
|
||||||
|
|
||||||
|
// 搜索时重置页码并获取数据
|
||||||
|
const handleSearch = () => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
fetchAccounts(1, searchQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理搜索框回车事件
|
||||||
|
const handleSearchKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredAccounts = accounts;
|
||||||
|
const totalPages = Math.ceil(totalAccounts / accountsPerPage);
|
||||||
|
|
||||||
const handleTransferFriends = (account: WechatAccount) => {
|
const handleTransferFriends = (account: WechatAccount) => {
|
||||||
setSelectedAccount(account)
|
setSelectedAccount(account)
|
||||||
setIsTransferDialogOpen(true)
|
setIsTransferDialogOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirmTransfer = () => {
|
const handleConfirmTransfer = async () => {
|
||||||
if (!selectedAccount) return
|
if (!selectedAccount) return
|
||||||
|
|
||||||
toast({
|
try {
|
||||||
title: "好友转移计划已创建",
|
// 实际实现好友转移功能,这里需要另一个账号作为目标
|
||||||
description: "请在场景获客中查看详情",
|
// 现在只是模拟效果
|
||||||
})
|
toast({
|
||||||
setIsTransferDialogOpen(false)
|
title: "好友转移计划已创建",
|
||||||
router.push("/scenarios")
|
description: "请在场景获客中查看详情",
|
||||||
|
})
|
||||||
|
setIsTransferDialogOpen(false)
|
||||||
|
router.push("/scenarios")
|
||||||
|
} catch (error) {
|
||||||
|
console.error("好友转移失败:", error);
|
||||||
|
toast({
|
||||||
|
title: "好友转移失败",
|
||||||
|
description: "请稍后再试",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -110,126 +177,176 @@ export default function WechatAccountsPage() {
|
|||||||
placeholder="搜索微信号/昵称"
|
placeholder="搜索微信号/昵称"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
onKeyDown={handleSearchKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon">
|
||||||
<Filter className="h-4 w-4" />
|
<Filter className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="icon">
|
<Button
|
||||||
<RefreshCw className="h-4 w-4" />
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
>
|
||||||
|
{isRefreshing ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="grid gap-3">
|
{isLoading ? (
|
||||||
{paginatedAccounts.map((account) => (
|
<div className="flex justify-center items-center py-20">
|
||||||
<Card
|
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||||
key={account.id}
|
</div>
|
||||||
className="p-4 hover:shadow-lg transition-all cursor-pointer"
|
) : accounts.length === 0 ? (
|
||||||
onClick={() => router.push(`/wechat-accounts/${account.id}`)}
|
<div className="text-center py-20 text-gray-500">
|
||||||
|
<p>暂无微信账号数据</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-4"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={isRefreshing}
|
||||||
>
|
>
|
||||||
<div className="flex items-start space-x-4">
|
{isRefreshing ? (
|
||||||
<Avatar className="h-12 w-12 ring-2 ring-offset-2 ring-blue-500/20">
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
<AvatarImage src={account.avatar} />
|
) : (
|
||||||
<AvatarFallback>{account.nickname[0]}</AvatarFallback>
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
</Avatar>
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
刷新
|
||||||
<div className="flex items-center justify-between">
|
</Button>
|
||||||
<div className="flex items-center space-x-2">
|
</div>
|
||||||
<h3 className="font-medium truncate">{account.nickname}</h3>
|
) : (
|
||||||
<Badge variant={account.status === "normal" ? "success" : "destructive"}>
|
<div className="grid gap-3">
|
||||||
{account.status === "normal" ? "正常" : "异常"}
|
{accounts.map((account) => (
|
||||||
</Badge>
|
<Card
|
||||||
</div>
|
key={account.id}
|
||||||
<Button
|
className="p-4 hover:shadow-lg transition-all cursor-pointer"
|
||||||
variant="ghost"
|
onClick={() => router.push(`/wechat-accounts/${account.id}`)}
|
||||||
size="sm"
|
>
|
||||||
onClick={(e) => {
|
<div className="flex items-start space-x-4">
|
||||||
e.stopPropagation()
|
<Avatar className="h-12 w-12 ring-2 ring-offset-2 ring-blue-500/20">
|
||||||
handleTransferFriends(account)
|
<AvatarImage src={account.avatar} />
|
||||||
}}
|
<AvatarFallback>{account.nickname[0]}</AvatarFallback>
|
||||||
>
|
</Avatar>
|
||||||
<ArrowRightLeft className="h-4 w-4 mr-2" />
|
<div className="flex-1 min-w-0">
|
||||||
好友转移
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-sm text-gray-500 space-y-1">
|
|
||||||
<div>微信号:{account.wechatId}</div>
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>好友数量:{account.friendCount}</div>
|
<div className="flex items-center space-x-2">
|
||||||
<div className="text-green-600">今日新增:+{account.todayAdded}</div>
|
<h3 className="font-medium truncate">{account.nickname}</h3>
|
||||||
</div>
|
<Badge variant={account.status === "normal" ? "outline" : "destructive"}>
|
||||||
<div className="space-y-1">
|
{account.status === "normal" ? "正常" : "异常"}
|
||||||
<div className="flex items-center justify-between text-sm">
|
</Badge>
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<span>今日可添加:</span>
|
|
||||||
<span className="font-medium">{account.remainingAdds}</span>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<AlertCircle className="h-4 w-4 text-gray-400" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>每日最多添加 {account.maxDailyAdds} 个好友</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
{account.todayAdded}/{account.maxDailyAdds}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Progress value={(account.todayAdded / account.maxDailyAdds) * 100} className="h-2" />
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleTransferFriends(account)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowRightLeft className="h-4 w-4 mr-2" />
|
||||||
|
好友转移
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between text-xs text-gray-500 pt-2">
|
<div className="mt-1 text-sm text-gray-500 space-y-1">
|
||||||
<div>所属设备:{account.deviceName}</div>
|
<div>微信号:{account.wechatId}</div>
|
||||||
<div>最后活跃:{account.lastActive}</div>
|
<div className="flex items-center justify-between">
|
||||||
|
<div>好友数量:{account.friendCount}</div>
|
||||||
|
<div className="text-green-600">今日新增:+{account.todayAdded}</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<span>今日可添加:</span>
|
||||||
|
<span className="font-medium">{account.remainingAdds}</span>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<AlertCircle className="h-4 w-4 text-gray-400" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>每日最多添加 {account.maxDailyAdds} 个好友</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
{account.todayAdded}/{account.maxDailyAdds}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={(account.todayAdded / account.maxDailyAdds) * 100} className="h-2" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-xs text-gray-500 pt-2">
|
||||||
|
<div>所属设备:{account.deviceName || '未知设备'}</div>
|
||||||
|
<div>最后活跃:{account.lastActive}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="mt-4 flex justify-center">
|
{!isLoading && accounts.length > 0 && totalPages > 1 && (
|
||||||
<Pagination>
|
<div className="mt-4 flex justify-center">
|
||||||
<PaginationContent>
|
<Pagination>
|
||||||
<PaginationItem>
|
<PaginationContent>
|
||||||
<PaginationPrevious
|
<PaginationItem>
|
||||||
href="#"
|
<PaginationPrevious
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setCurrentPage((prev) => Math.max(1, prev - 1))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
|
|
||||||
<PaginationItem key={page}>
|
|
||||||
<PaginationLink
|
|
||||||
href="#"
|
href="#"
|
||||||
isActive={currentPage === page}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setCurrentPage(page)
|
if (currentPage > 1) {
|
||||||
|
setCurrentPage((prev) => prev - 1)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{page}
|
|
||||||
</PaginationLink>
|
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
))}
|
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
|
||||||
<PaginationItem>
|
// 显示当前页附近的页码
|
||||||
<PaginationNext
|
let pageToShow = i + 1;
|
||||||
href="#"
|
if (currentPage > 3 && totalPages > 5) {
|
||||||
onClick={(e) => {
|
pageToShow = Math.min(currentPage - 2 + i, totalPages);
|
||||||
e.preventDefault()
|
if (pageToShow > totalPages - 4) {
|
||||||
setCurrentPage((prev) => Math.min(totalPages, prev + 1))
|
pageToShow = totalPages - 4 + i;
|
||||||
}}
|
}
|
||||||
/>
|
}
|
||||||
</PaginationItem>
|
return (
|
||||||
</PaginationContent>
|
<PaginationItem key={pageToShow}>
|
||||||
</Pagination>
|
<PaginationLink
|
||||||
</div>
|
href="#"
|
||||||
|
isActive={currentPage === pageToShow}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setCurrentPage(pageToShow)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pageToShow}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
setCurrentPage((prev) => prev + 1)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={isTransferDialogOpen} onOpenChange={setIsTransferDialogOpen}>
|
<Dialog open={isTransferDialogOpen} onOpenChange={setIsTransferDialogOpen}>
|
||||||
|
|||||||
113
Cunkebao/types/wechat-account.ts
Normal file
113
Cunkebao/types/wechat-account.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// 服务端返回的微信账号数据结构
|
||||||
|
export interface ServerWechatAccount {
|
||||||
|
id: number;
|
||||||
|
wechatId: string;
|
||||||
|
nickname: string;
|
||||||
|
accountNickname: string;
|
||||||
|
avatar: string;
|
||||||
|
accountUserName: string;
|
||||||
|
status: string;
|
||||||
|
deviceStatus: string;
|
||||||
|
totalFriend: number;
|
||||||
|
canAddFriendCount: number;
|
||||||
|
deviceInfo: string;
|
||||||
|
todayNewFriendCount: number;
|
||||||
|
wechatAlive: number;
|
||||||
|
deviceAlive: number;
|
||||||
|
imei: string;
|
||||||
|
deviceMemo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务器响应结构
|
||||||
|
export interface ServerWechatAccountsResponse {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data: {
|
||||||
|
total: number;
|
||||||
|
list: ServerWechatAccount[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前端使用的微信账号数据结构
|
||||||
|
export interface WechatAccount {
|
||||||
|
id: string;
|
||||||
|
avatar: string;
|
||||||
|
nickname: string;
|
||||||
|
wechatId: string;
|
||||||
|
deviceId: string;
|
||||||
|
deviceName: string;
|
||||||
|
friendCount: number;
|
||||||
|
todayAdded: number;
|
||||||
|
remainingAdds: number;
|
||||||
|
maxDailyAdds: number;
|
||||||
|
status: "normal" | "abnormal";
|
||||||
|
lastActive: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信账号查询参数
|
||||||
|
export interface QueryWechatAccountParams {
|
||||||
|
keyword?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
sort?: string;
|
||||||
|
order?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信好友数据结构
|
||||||
|
export interface WechatFriend {
|
||||||
|
id: string;
|
||||||
|
wechatId: string;
|
||||||
|
nickname: string;
|
||||||
|
avatar: string;
|
||||||
|
gender: number;
|
||||||
|
region: string;
|
||||||
|
signature: string;
|
||||||
|
labels: string;
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信账号详情数据结构
|
||||||
|
export interface WechatAccountDetail {
|
||||||
|
basicInfo: {
|
||||||
|
id: number;
|
||||||
|
wechatId: string;
|
||||||
|
nickname: string;
|
||||||
|
avatar: string;
|
||||||
|
status: string;
|
||||||
|
deviceStatus: string;
|
||||||
|
deviceInfo: string;
|
||||||
|
gender: number;
|
||||||
|
region: string;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
statistics: {
|
||||||
|
totalFriend: number;
|
||||||
|
maleFriend: number;
|
||||||
|
femaleFriend: number;
|
||||||
|
canAddFriendCount: number;
|
||||||
|
yesterdayMsgCount: number;
|
||||||
|
sevenDayMsgCount: number;
|
||||||
|
thirtyDayMsgCount: number;
|
||||||
|
};
|
||||||
|
accountInfo: {
|
||||||
|
age: number;
|
||||||
|
activityLevel: string;
|
||||||
|
weight: number;
|
||||||
|
createTime: string;
|
||||||
|
lastUpdateTime: string;
|
||||||
|
};
|
||||||
|
restrictions: Array<{
|
||||||
|
type: string;
|
||||||
|
reason: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
}>;
|
||||||
|
friends: WechatFriend[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信账号详情响应
|
||||||
|
export interface WechatAccountDetailResponse {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data: WechatAccountDetail;
|
||||||
|
}
|
||||||
131
Server/application/common/model/CompanyAccount.php
Normal file
131
Server/application/common/model/CompanyAccount.php
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\common\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司账户模型类
|
||||||
|
*/
|
||||||
|
class CompanyAccount extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 数据表名
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $table = 'tk_company_account';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动写入时间戳
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间字段
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $createTime = 'createTime';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间字段
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $updateTime = 'updateTime';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏属性
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $hidden = ['passwordMd5', 'passwordLocal', 'secret'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段类型
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $type = [
|
||||||
|
'id' => 'integer',
|
||||||
|
'tenantId' => 'integer',
|
||||||
|
'accountType' => 'integer',
|
||||||
|
'companyId' => 'integer',
|
||||||
|
'useGoogleSecretKey' => 'boolean',
|
||||||
|
'hasVerifyGoogleSecret' => 'boolean',
|
||||||
|
'lastLoginTime' => 'integer',
|
||||||
|
'createTime' => 'integer',
|
||||||
|
'updateTime' => 'integer'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取公司账户信息
|
||||||
|
* @param string $userName 用户名
|
||||||
|
* @param string $password 密码(MD5加密后的)
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public static function getAccount($userName, $password)
|
||||||
|
{
|
||||||
|
// 查询账户
|
||||||
|
$account = self::where('userName', $userName)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$account) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
if ($account->passwordMd5 !== $password) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新登录信息
|
||||||
|
$account->lastLoginIp = request()->ip();
|
||||||
|
$account->lastLoginTime = time();
|
||||||
|
$account->save();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $account->id,
|
||||||
|
'tenantId' => $account->tenantId,
|
||||||
|
'userName' => $account->userName,
|
||||||
|
'realName' => $account->realName,
|
||||||
|
'nickname' => $account->nickname,
|
||||||
|
'avatar' => $account->avatar,
|
||||||
|
'accountType' => $account->accountType,
|
||||||
|
'companyId' => $account->companyId,
|
||||||
|
'lastLoginIp' => $account->lastLoginIp,
|
||||||
|
'lastLoginTime' => $account->lastLoginTime
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过租户ID获取账户信息
|
||||||
|
* @param int $companyId 租户ID
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public static function getAccountByCompanyId($companyId)
|
||||||
|
{
|
||||||
|
// 查询账户
|
||||||
|
$account = self::where('companyId', $companyId)->find();
|
||||||
|
|
||||||
|
if (!$account) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $account->id,
|
||||||
|
'tenantId' => $account->tenantId,
|
||||||
|
'userName' => $account->userName,
|
||||||
|
'realName' => $account->realName,
|
||||||
|
'nickname' => $account->nickname,
|
||||||
|
'avatar' => $account->avatar,
|
||||||
|
'accountType' => $account->accountType,
|
||||||
|
'companyId' => $account->companyId,
|
||||||
|
'lastLoginIp' => $account->lastLoginIp,
|
||||||
|
'lastLoginTime' => $account->lastLoginTime
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace app\devices\controller;
|
namespace app\devices\controller;
|
||||||
|
|
||||||
|
use app\common\model\CompanyAccount;
|
||||||
use think\Controller;
|
use think\Controller;
|
||||||
use app\devices\model\WechatAccount;
|
use app\devices\model\WechatAccount;
|
||||||
use think\facade\Request;
|
use think\facade\Request;
|
||||||
@@ -88,6 +89,8 @@ class DeviceWechat extends Controller
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
// 获取登录用户信息
|
||||||
|
$userInfo = request()->userInfo;
|
||||||
// 获取查询条件
|
// 获取查询条件
|
||||||
$where = [];
|
$where = [];
|
||||||
|
|
||||||
@@ -102,7 +105,7 @@ class DeviceWechat extends Controller
|
|||||||
if (!empty($nickname)) {
|
if (!empty($nickname)) {
|
||||||
$where['nickname|accountNickname'] = ['like', "%{$nickname}%"];
|
$where['nickname|accountNickname'] = ['like', "%{$nickname}%"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取分页参数
|
// 获取分页参数
|
||||||
$page = (int)Request::param('page', 1);
|
$page = (int)Request::param('page', 1);
|
||||||
$limit = (int)Request::param('limit', 10);
|
$limit = (int)Request::param('limit', 10);
|
||||||
@@ -110,22 +113,74 @@ class DeviceWechat extends Controller
|
|||||||
// 获取排序参数
|
// 获取排序参数
|
||||||
$sort = Request::param('sort', 'id');
|
$sort = Request::param('sort', 'id');
|
||||||
$order = Request::param('order', 'desc');
|
$order = Request::param('order', 'desc');
|
||||||
|
|
||||||
// 获取在线微信账号列表
|
// 公司账户表没有 companyId,需要转换一下
|
||||||
$list = WechatAccount::getOnlineWechatList($where, "{$sort} {$order}", $page, $limit);
|
$acountInfo = CompanyAccount::getAccountByCompanyId($userInfo['companyId']);
|
||||||
|
|
||||||
|
// 先用账号进行查询
|
||||||
|
$where['accountUserName'] = $acountInfo['userName'];
|
||||||
|
|
||||||
|
// 根据用户权限不同实现不同的查询逻辑
|
||||||
|
if ($userInfo['isAdmin'] == 1) {
|
||||||
|
// 管理员直接查询tk_wechat_account表
|
||||||
|
$list = WechatAccount::getOnlineWechatList($where, "{$sort} {$order}", $page, $limit);
|
||||||
|
} else {
|
||||||
|
// 非管理员先查询tk_device_user表
|
||||||
|
$deviceIds = Db::table('tk_device_user')
|
||||||
|
->where('companyId', $userInfo['companyId'])
|
||||||
|
->where('userId', $userInfo['id'])
|
||||||
|
->column('deviceId');
|
||||||
|
|
||||||
|
if (empty($deviceIds)) {
|
||||||
|
// 如果没有绑定设备,返回提示信息
|
||||||
|
return json([
|
||||||
|
'code' => 403,
|
||||||
|
'msg' => '请联系管理员绑定设备微信',
|
||||||
|
'data' => [
|
||||||
|
'total' => 0,
|
||||||
|
'list' => []
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取这些设备关联的微信ID
|
||||||
|
$wechatIds = Db::table('tk_device_wechat_login')
|
||||||
|
->where('companyId', $userInfo['companyId'])
|
||||||
|
->whereIn('deviceId', $deviceIds)
|
||||||
|
->column('wechatId');
|
||||||
|
|
||||||
|
if (empty($wechatIds)) {
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '获取成功',
|
||||||
|
'data' => [
|
||||||
|
'total' => 0,
|
||||||
|
'list' => []
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将微信ID添加到查询条件中
|
||||||
|
$where['id'] = ['in', $wechatIds];
|
||||||
|
|
||||||
|
// 查询微信账号
|
||||||
|
$list = WechatAccount::getOnlineWechatList($where, "{$sort} {$order}", $page, $limit);
|
||||||
|
}
|
||||||
|
|
||||||
// 处理返回数据
|
// 处理返回数据
|
||||||
$data = [];
|
$data = [];
|
||||||
foreach ($list->items() as $item) {
|
foreach ($list->items() as $item) {
|
||||||
// 计算今日可添加好友数量(这里使用一个示例算法,你可以根据实际需求修改)
|
// 计算今日可添加好友数量(这里使用一个示例算法,你可以根据实际需求修改)
|
||||||
$canAddFriendCount = 30 - (isset($item['yesterdayMsgCount']) ? intval($item['yesterdayMsgCount']) : 0);
|
$canAddFriendCount = 20 - Db::table('tk_friend_task')->where('wechatId', $item['wechatId'])->count('*');
|
||||||
if ($canAddFriendCount < 0) {
|
if ($canAddFriendCount < 0) {
|
||||||
$canAddFriendCount = 0;
|
$canAddFriendCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算今日新增好友数量(示例数据,实际需要从数据库获取或通过其他方式计算)
|
// 计算今日新增好友数量(示例数据,实际需要从数据库获取或通过其他方式计算)
|
||||||
// 这里只是一个示例,你需要根据实际情况替换
|
// 这里只是一个示例,你需要根据实际情况替换
|
||||||
$todayNewFriendCount = mt_rand(0, 10); // 随机生成0-10的数字作为示例
|
$todayNewFriendCount = Db::table('tk_friend_task')->where('wechatId', $item['wechatId'])
|
||||||
|
->where('status', 3)
|
||||||
|
->count('*');
|
||||||
|
|
||||||
$data[] = [
|
$data[] = [
|
||||||
'id' => $item['id'],
|
'id' => $item['id'],
|
||||||
|
|||||||
324
Server/application/devices/model/FriendTask.php
Normal file
324
Server/application/devices/model/FriendTask.php
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\devices\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加好友任务记录模型
|
||||||
|
*/
|
||||||
|
class FriendTask extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 数据表名
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $table = 'tk_friend_task';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动写入时间戳
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间字段
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $createTime = 'createTime';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间字段
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $updateTime = 'updateTime';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段类型
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $type = [
|
||||||
|
'id' => 'integer',
|
||||||
|
'tenantId' => 'integer',
|
||||||
|
'operatorAccountId' => 'integer',
|
||||||
|
'status' => 'integer',
|
||||||
|
'wechatAccountId' => 'integer',
|
||||||
|
'createTime' => 'integer',
|
||||||
|
'updateTime' => 'integer'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态常量
|
||||||
|
*/
|
||||||
|
const STATUS_PENDING = 1; // 待处理
|
||||||
|
const STATUS_PROCESSING = 2; // 处理中
|
||||||
|
const STATUS_APPROVED = 3; // 已通过
|
||||||
|
const STATUS_REJECTED = 4; // 已拒绝
|
||||||
|
const STATUS_EXPIRED = 5; // 已过期
|
||||||
|
const STATUS_CANCELLED = 6; // 已取消
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本
|
||||||
|
* @param int $status 状态码
|
||||||
|
* @return string 状态文本
|
||||||
|
*/
|
||||||
|
public static function getStatusText($status)
|
||||||
|
{
|
||||||
|
$statusMap = [
|
||||||
|
self::STATUS_PENDING => '待处理',
|
||||||
|
self::STATUS_PROCESSING => '处理中',
|
||||||
|
self::STATUS_APPROVED => '已通过',
|
||||||
|
self::STATUS_REJECTED => '已拒绝',
|
||||||
|
self::STATUS_EXPIRED => '已过期',
|
||||||
|
self::STATUS_CANCELLED => '已取消'
|
||||||
|
];
|
||||||
|
|
||||||
|
return isset($statusMap[$status]) ? $statusMap[$status] : '未知状态';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取好友任务列表
|
||||||
|
* @param array $where 查询条件
|
||||||
|
* @param string $order 排序条件
|
||||||
|
* @param int $page 页码
|
||||||
|
* @param int $limit 每页数量
|
||||||
|
* @return \think\Paginator
|
||||||
|
*/
|
||||||
|
public static function getTaskList($where = [], $order = 'createTime desc', $page = 1, $limit = 10)
|
||||||
|
{
|
||||||
|
return self::where($where)
|
||||||
|
->order($order)
|
||||||
|
->paginate($limit, false, ['page' => $page]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务详情
|
||||||
|
* @param int $id 任务ID
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public static function getTaskDetail($id)
|
||||||
|
{
|
||||||
|
return self::where('id', $id)->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建好友任务
|
||||||
|
* @param array $data 任务数据
|
||||||
|
* @return int|bool 任务ID或false
|
||||||
|
*/
|
||||||
|
public static function createTask($data)
|
||||||
|
{
|
||||||
|
// 确保必填字段存在
|
||||||
|
if (!isset($data['id'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
if (!isset($data['status'])) {
|
||||||
|
$data['status'] = self::STATUS_PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置创建时间
|
||||||
|
$data['createTime'] = time();
|
||||||
|
$data['updateTime'] = time();
|
||||||
|
|
||||||
|
// 创建任务
|
||||||
|
$task = new self;
|
||||||
|
$task->allowField(true)->save($data);
|
||||||
|
|
||||||
|
return $task->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新任务信息
|
||||||
|
* @param int $id 任务ID
|
||||||
|
* @param array $data 更新数据
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function updateTask($id, $data)
|
||||||
|
{
|
||||||
|
// 更新时间
|
||||||
|
$data['updateTime'] = time();
|
||||||
|
|
||||||
|
return self::where('id', $id)->update($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新任务状态
|
||||||
|
* @param int $id 任务ID
|
||||||
|
* @param int $status 新状态
|
||||||
|
* @param string $remark 备注
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function updateTaskStatus($id, $status, $remark = '')
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'status' => $status,
|
||||||
|
'updateTime' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($remark)) {
|
||||||
|
$data['remark'] = $remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::where('id', $id)->update($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消任务
|
||||||
|
* @param int $id 任务ID
|
||||||
|
* @param string $remark 取消原因
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function cancelTask($id, $remark = '')
|
||||||
|
{
|
||||||
|
return self::updateTaskStatus($id, self::STATUS_CANCELLED, $remark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务审批通过
|
||||||
|
* @param int $id 任务ID
|
||||||
|
* @param string $remark 备注信息
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function approveTask($id, $remark = '')
|
||||||
|
{
|
||||||
|
return self::updateTaskStatus($id, self::STATUS_APPROVED, $remark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务拒绝
|
||||||
|
* @param int $id 任务ID
|
||||||
|
* @param string $remark 拒绝原因
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function rejectTask($id, $remark = '')
|
||||||
|
{
|
||||||
|
return self::updateTaskStatus($id, self::STATUS_REJECTED, $remark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据微信账号ID获取任务列表
|
||||||
|
* @param int $wechatAccountId 微信账号ID
|
||||||
|
* @param array $status 状态数组,默认查询所有状态
|
||||||
|
* @param int $page 页码
|
||||||
|
* @param int $limit 每页数量
|
||||||
|
* @return \think\Paginator
|
||||||
|
*/
|
||||||
|
public static function getTasksByWechatAccount($wechatAccountId, $status = [], $page = 1, $limit = 10)
|
||||||
|
{
|
||||||
|
$where = ['wechatAccountId' => $wechatAccountId];
|
||||||
|
|
||||||
|
if (!empty($status)) {
|
||||||
|
$where['status'] = ['in', $status];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::getTaskList($where, 'createTime desc', $page, $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据操作账号ID获取任务列表
|
||||||
|
* @param int $operatorAccountId 操作账号ID
|
||||||
|
* @param array $status 状态数组,默认查询所有状态
|
||||||
|
* @param int $page 页码
|
||||||
|
* @param int $limit 每页数量
|
||||||
|
* @return \think\Paginator
|
||||||
|
*/
|
||||||
|
public static function getTasksByOperator($operatorAccountId, $status = [], $page = 1, $limit = 10)
|
||||||
|
{
|
||||||
|
$where = ['operatorAccountId' => $operatorAccountId];
|
||||||
|
|
||||||
|
if (!empty($status)) {
|
||||||
|
$where['status'] = ['in', $status];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::getTaskList($where, 'createTime desc', $page, $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据手机号/微信号查询任务
|
||||||
|
* @param string $phone 手机号/微信号
|
||||||
|
* @param int $tenantId 租户ID
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getTasksByPhone($phone, $tenantId = null)
|
||||||
|
{
|
||||||
|
$where = ['phone' => $phone];
|
||||||
|
|
||||||
|
if ($tenantId !== null) {
|
||||||
|
$where['tenantId'] = $tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::where($where)->select();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取统计数据
|
||||||
|
* @param int $tenantId 租户ID
|
||||||
|
* @param int $timeRange 时间范围(秒)
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getTaskStats($tenantId = null, $timeRange = 86400)
|
||||||
|
{
|
||||||
|
$where = [];
|
||||||
|
|
||||||
|
if ($tenantId !== null) {
|
||||||
|
$where['tenantId'] = $tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围
|
||||||
|
$startTime = time() - $timeRange;
|
||||||
|
$where['createTime'] = ['>=', $startTime];
|
||||||
|
|
||||||
|
// 获取各状态的任务数量
|
||||||
|
$stats = [
|
||||||
|
'total' => self::where($where)->count(),
|
||||||
|
'pending' => self::where(array_merge($where, ['status' => self::STATUS_PENDING]))->count(),
|
||||||
|
'processing' => self::where(array_merge($where, ['status' => self::STATUS_PROCESSING]))->count(),
|
||||||
|
'approved' => self::where(array_merge($where, ['status' => self::STATUS_APPROVED]))->count(),
|
||||||
|
'rejected' => self::where(array_merge($where, ['status' => self::STATUS_REJECTED]))->count(),
|
||||||
|
'expired' => self::where(array_merge($where, ['status' => self::STATUS_EXPIRED]))->count(),
|
||||||
|
'cancelled' => self::where(array_merge($where, ['status' => self::STATUS_CANCELLED]))->count()
|
||||||
|
];
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务处理结果统计
|
||||||
|
* @param int $tenantId 租户ID
|
||||||
|
* @param int $timeRange 时间范围(秒)
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getTaskResultStats($tenantId = null, $timeRange = 86400 * 30)
|
||||||
|
{
|
||||||
|
$where = [];
|
||||||
|
|
||||||
|
if ($tenantId !== null) {
|
||||||
|
$where['tenantId'] = $tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围
|
||||||
|
$startTime = time() - $timeRange;
|
||||||
|
$where['createTime'] = ['>=', $startTime];
|
||||||
|
|
||||||
|
// 获取处理结果数据
|
||||||
|
$stats = [
|
||||||
|
'total' => self::where($where)->count(),
|
||||||
|
'approved' => self::where(array_merge($where, ['status' => self::STATUS_APPROVED]))->count(),
|
||||||
|
'rejected' => self::where(array_merge($where, ['status' => self::STATUS_REJECTED]))->count(),
|
||||||
|
'pending' => self::where(array_merge($where, ['status' => ['in', [self::STATUS_PENDING, self::STATUS_PROCESSING]]]))->count(),
|
||||||
|
'other' => self::where(array_merge($where, ['status' => ['in', [self::STATUS_EXPIRED, self::STATUS_CANCELLED]]]))->count()
|
||||||
|
];
|
||||||
|
|
||||||
|
// 计算成功率
|
||||||
|
$stats['approvalRate'] = $stats['total'] > 0 ? round($stats['approved'] / $stats['total'] * 100, 2) : 0;
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,8 +97,6 @@ class WechatAccount extends Model
|
|||||||
public static function getOnlineWechatList($where = [], $order = 'id desc', $page = 1, $limit = 10)
|
public static function getOnlineWechatList($where = [], $order = 'id desc', $page = 1, $limit = 10)
|
||||||
{
|
{
|
||||||
$condition = [
|
$condition = [
|
||||||
'wechatAlive' => 1,
|
|
||||||
'deviceAlive' => 1,
|
|
||||||
'isDeleted' => 0
|
'isDeleted' => 0
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user