设备关联微信
This commit is contained in:
@@ -33,6 +33,11 @@ export const fetchDeviceDetail = async (id: string | number): Promise<ApiRespons
|
|||||||
return api.get<ApiResponse<any>>(`/v1/devices/${id}`);
|
return api.get<ApiResponse<any>>(`/v1/devices/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取设备关联的微信账号
|
||||||
|
export const fetchDeviceRelatedAccounts = async (id: string | number): Promise<ApiResponse<any>> => {
|
||||||
|
return api.get<ApiResponse<any>>(`/v1/devices/${id}/related-accounts`);
|
||||||
|
};
|
||||||
|
|
||||||
// 更新设备任务配置
|
// 更新设备任务配置
|
||||||
export const updateDeviceTaskConfig = async (
|
export const updateDeviceTaskConfig = async (
|
||||||
id: string | number,
|
id: string | number,
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { useState, useEffect } from "react"
|
|||||||
import { useParams, useRouter } from "next/navigation"
|
import { useParams, useRouter } from "next/navigation"
|
||||||
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 { ChevronLeft, Smartphone, Battery, Wifi, MessageCircle, Users, Settings, History } from "lucide-react"
|
import { ChevronLeft, Smartphone, Battery, Wifi, MessageCircle, Users, Settings, History, RefreshCw } from "lucide-react"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import { fetchDeviceDetail, updateDeviceTaskConfig } from "@/api/devices"
|
import { fetchDeviceDetail, fetchDeviceRelatedAccounts, updateDeviceTaskConfig } from "@/api/devices"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
interface WechatAccount {
|
interface WechatAccount {
|
||||||
@@ -18,10 +18,13 @@ interface WechatAccount {
|
|||||||
avatar: string
|
avatar: string
|
||||||
nickname: string
|
nickname: string
|
||||||
wechatId: string
|
wechatId: string
|
||||||
gender: "male" | "female"
|
gender: number
|
||||||
status: "normal" | "abnormal"
|
status: number
|
||||||
addFriendStatus: "enabled" | "disabled"
|
statusText: string
|
||||||
friendCount: number
|
wechatAlive: number
|
||||||
|
wechatAliveText: string
|
||||||
|
addFriendStatus: number
|
||||||
|
totalFriend: number
|
||||||
lastActive: string
|
lastActive: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +71,7 @@ export default function DeviceDetailPage() {
|
|||||||
const [device, setDevice] = useState<Device | null>(null)
|
const [device, setDevice] = useState<Device | null>(null)
|
||||||
const [activeTab, setActiveTab] = useState("info")
|
const [activeTab, setActiveTab] = useState("info")
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [accountsLoading, setAccountsLoading] = useState(false)
|
||||||
const [savingFeatures, setSavingFeatures] = useState({
|
const [savingFeatures, setSavingFeatures] = useState({
|
||||||
autoAddFriend: false,
|
autoAddFriend: false,
|
||||||
autoReply: false,
|
autoReply: false,
|
||||||
@@ -146,16 +150,24 @@ export default function DeviceDetailPage() {
|
|||||||
avatar: "/placeholder.svg", // 默认头像
|
avatar: "/placeholder.svg", // 默认头像
|
||||||
nickname: serverData.memo || "微信账号",
|
nickname: serverData.memo || "微信账号",
|
||||||
wechatId: serverData.imei || "",
|
wechatId: serverData.imei || "",
|
||||||
gender: "male", // 默认性别
|
gender: 1, // 默认性别
|
||||||
status: serverData.alive === 1 ? "normal" : "abnormal",
|
status: serverData.alive === 1 ? 1 : 0,
|
||||||
addFriendStatus: "enabled",
|
statusText: serverData.alive === 1 ? "可加友" : "已停用",
|
||||||
friendCount: serverData.totalFriend || 0,
|
wechatAlive: serverData.alive === 1 ? 1 : 0,
|
||||||
|
wechatAliveText: serverData.alive === 1 ? "正常" : "异常",
|
||||||
|
addFriendStatus: 1,
|
||||||
|
totalFriend: serverData.totalFriend || 0,
|
||||||
lastActive: serverData.lastUpdateTime || new Date().toISOString()
|
lastActive: serverData.lastUpdateTime || new Date().toISOString()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
setDevice(formattedDevice)
|
setDevice(formattedDevice)
|
||||||
|
|
||||||
|
// 如果当前激活标签是"accounts",则加载关联微信账号
|
||||||
|
if (activeTab === "accounts") {
|
||||||
|
fetchRelatedAccounts()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果API返回错误,则使用备用模拟数据
|
// 如果API返回错误,则使用备用模拟数据
|
||||||
toast.error("获取设备信息失败,显示备用数据")
|
toast.error("获取设备信息失败,显示备用数据")
|
||||||
@@ -185,10 +197,13 @@ export default function DeviceDetailPage() {
|
|||||||
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png",
|
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png",
|
||||||
nickname: "老张",
|
nickname: "老张",
|
||||||
wechatId: "wxid_abc123",
|
wechatId: "wxid_abc123",
|
||||||
gender: "male",
|
gender: 1,
|
||||||
status: "normal",
|
status: 1,
|
||||||
addFriendStatus: "enabled",
|
statusText: "可加友",
|
||||||
friendCount: 523,
|
wechatAlive: 1,
|
||||||
|
wechatAliveText: "正常",
|
||||||
|
addFriendStatus: 1,
|
||||||
|
totalFriend: 523,
|
||||||
lastActive: "2024-02-09 15:20:33",
|
lastActive: "2024-02-09 15:20:33",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -196,10 +211,13 @@ export default function DeviceDetailPage() {
|
|||||||
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png",
|
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png",
|
||||||
nickname: "老李",
|
nickname: "老李",
|
||||||
wechatId: "wxid_xyz789",
|
wechatId: "wxid_xyz789",
|
||||||
gender: "male",
|
gender: 1,
|
||||||
status: "abnormal",
|
status: 0,
|
||||||
addFriendStatus: "disabled",
|
statusText: "已停用",
|
||||||
friendCount: 245,
|
wechatAlive: 0,
|
||||||
|
wechatAliveText: "异常",
|
||||||
|
addFriendStatus: 0,
|
||||||
|
totalFriend: 245,
|
||||||
lastActive: "2024-02-09 14:15:22",
|
lastActive: "2024-02-09 14:15:22",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -228,7 +246,53 @@ export default function DeviceDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchDevice()
|
fetchDevice()
|
||||||
}, [params.id])
|
}, [params.id, activeTab])
|
||||||
|
|
||||||
|
// 获取设备关联微信账号
|
||||||
|
const fetchRelatedAccounts = async () => {
|
||||||
|
if (!params.id || accountsLoading) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setAccountsLoading(true)
|
||||||
|
const response = await fetchDeviceRelatedAccounts(params.id as string)
|
||||||
|
|
||||||
|
if (response && response.code === 200 && response.data) {
|
||||||
|
const accounts = response.data.accounts || []
|
||||||
|
|
||||||
|
// 更新设备的微信账号信息
|
||||||
|
setDevice(prev => {
|
||||||
|
if (!prev) return null
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
wechatAccounts: accounts
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (accounts.length > 0) {
|
||||||
|
toast.success(`成功获取${accounts.length}个关联微信账号`)
|
||||||
|
} else {
|
||||||
|
toast.info("此设备暂无关联微信账号")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error("获取关联微信账号失败")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取关联微信账号失败:", error)
|
||||||
|
toast.error("获取关联微信账号出错")
|
||||||
|
} finally {
|
||||||
|
setAccountsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理标签页切换
|
||||||
|
const handleTabChange = (value: string) => {
|
||||||
|
setActiveTab(value)
|
||||||
|
|
||||||
|
// 当切换到"关联账号"标签时,获取最新的关联微信账号信息
|
||||||
|
if (value === "accounts") {
|
||||||
|
fetchRelatedAccounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理功能开关状态变化
|
// 处理功能开关状态变化
|
||||||
const handleFeatureChange = async (feature: keyof Device['features'], checked: boolean) => {
|
const handleFeatureChange = async (feature: keyof Device['features'], checked: boolean) => {
|
||||||
@@ -351,7 +415,7 @@ export default function DeviceDetailPage() {
|
|||||||
<div className="mt-2 text-sm text-gray-500">最后活跃:{device.lastActive}</div>
|
<div className="mt-2 text-sm text-gray-500">最后活跃:{device.lastActive}</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
<TabsTrigger value="info">基本信息</TabsTrigger>
|
<TabsTrigger value="info">基本信息</TabsTrigger>
|
||||||
<TabsTrigger value="accounts">关联账号</TabsTrigger>
|
<TabsTrigger value="accounts">关联账号</TabsTrigger>
|
||||||
@@ -435,8 +499,37 @@ export default function DeviceDetailPage() {
|
|||||||
|
|
||||||
<TabsContent value="accounts">
|
<TabsContent value="accounts">
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-md font-medium">微信账号列表</h3>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={fetchRelatedAccounts}
|
||||||
|
disabled={accountsLoading}
|
||||||
|
>
|
||||||
|
{accountsLoading ? (
|
||||||
|
<>
|
||||||
|
<div className="w-4 h-4 mr-2 rounded-full border-2 border-blue-500 border-t-transparent animate-spin"></div>
|
||||||
|
刷新中
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-1" />
|
||||||
|
刷新
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="h-[calc(100vh-300px)]">
|
<ScrollArea className="h-[calc(100vh-300px)]">
|
||||||
{device.wechatAccounts && device.wechatAccounts.length > 0 ? (
|
{accountsLoading && (
|
||||||
|
<div className="flex justify-center items-center py-8">
|
||||||
|
<div className="w-6 h-6 rounded-full border-2 border-blue-500 border-t-transparent animate-spin mr-2"></div>
|
||||||
|
<span className="text-gray-500">加载微信账号中...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!accountsLoading && device.wechatAccounts && device.wechatAccounts.length > 0 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{device.wechatAccounts.map((account) => (
|
{device.wechatAccounts.map((account) => (
|
||||||
<div key={account.id} className="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg">
|
<div key={account.id} className="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||||
@@ -448,26 +541,38 @@ export default function DeviceDetailPage() {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="font-medium truncate">{account.nickname}</div>
|
<div className="font-medium truncate">{account.nickname}</div>
|
||||||
<Badge variant={getBadgeVariant(account.status)}>
|
<Badge variant={account.wechatAlive === 1 ? "default" : "destructive"}>
|
||||||
{account.status === "normal" ? "正常" : "异常"}
|
{account.wechatAliveText}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500 mt-1">微信号: {account.wechatId}</div>
|
<div className="text-sm text-gray-500 mt-1">微信号: {account.wechatId}</div>
|
||||||
<div className="text-sm text-gray-500">性别: {account.gender === "male" ? "男" : "女"}</div>
|
<div className="text-sm text-gray-500">性别: {account.gender === 1 ? "男" : "女"}</div>
|
||||||
<div className="flex items-center justify-between mt-2">
|
<div className="flex items-center justify-between mt-2">
|
||||||
<span className="text-sm text-gray-500">好友数: {account.friendCount}</span>
|
<span className="text-sm text-gray-500">好友数: {account.totalFriend}</span>
|
||||||
<Badge variant={getBadgeVariant(account.addFriendStatus)}>
|
<Badge variant={account.status === 1 ? "outline" : "secondary"}>
|
||||||
{account.addFriendStatus === "enabled" ? "可加友" : "已停用"}
|
{account.statusText}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 mt-1">最后活跃: {account.lastActive}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8 text-gray-500">
|
!accountsLoading && (
|
||||||
<p>此设备暂无关联的微信账号</p>
|
<div className="text-center py-8 text-gray-500">
|
||||||
</div>
|
<p>此设备暂无关联的微信账号</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2"
|
||||||
|
onClick={fetchRelatedAccounts}
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-1" />
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ import { useRouter } from "next/navigation"
|
|||||||
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"
|
||||||
import { ChevronLeft, Plus, Filter, Search, RefreshCw, QrCode } from "lucide-react"
|
import { ChevronLeft, Plus, Filter, Search, RefreshCw, QrCode, Smartphone, Loader2, AlertTriangle } from "lucide-react"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { toast } from "@/components/ui/use-toast"
|
import { toast } from "@/components/ui/use-toast"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import { fetchDeviceList, deleteDevice } from "@/api/devices"
|
import { fetchDeviceList, deleteDevice } from "@/api/devices"
|
||||||
import { ServerDevice } from "@/types/device"
|
import { ServerDevice } from "@/types/device"
|
||||||
|
import { api } from "@/lib/api"
|
||||||
|
|
||||||
// 设备接口更新为与服务端接口对应的类型
|
// 设备接口更新为与服务端接口对应的类型
|
||||||
interface Device extends ServerDevice {
|
interface Device extends ServerDevice {
|
||||||
@@ -37,6 +39,13 @@ export default function DevicesPage() {
|
|||||||
const observerTarget = useRef<HTMLDivElement>(null)
|
const observerTarget = useRef<HTMLDivElement>(null)
|
||||||
// 使用ref来追踪当前页码,避免依赖effect循环
|
// 使用ref来追踪当前页码,避免依赖effect循环
|
||||||
const pageRef = useRef(1)
|
const pageRef = useRef(1)
|
||||||
|
// 添加设备相关状态
|
||||||
|
const [deviceImei, setDeviceImei] = useState("")
|
||||||
|
const [deviceName, setDeviceName] = useState("")
|
||||||
|
const [qrCodeImage, setQrCodeImage] = useState("")
|
||||||
|
const [isLoadingQRCode, setIsLoadingQRCode] = useState(false)
|
||||||
|
const [isSubmittingImei, setIsSubmittingImei] = useState(false)
|
||||||
|
const [activeTab, setActiveTab] = useState("scan")
|
||||||
|
|
||||||
const devicesPerPage = 20 // 每页显示20条记录
|
const devicesPerPage = 20 // 每页显示20条记录
|
||||||
|
|
||||||
@@ -155,6 +164,259 @@ export default function DevicesPage() {
|
|||||||
}
|
}
|
||||||
}, [hasMore, isLoading, loadNextPage])
|
}, [hasMore, isLoading, loadNextPage])
|
||||||
|
|
||||||
|
// 获取设备二维码
|
||||||
|
const fetchDeviceQRCode = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoadingQRCode(true)
|
||||||
|
setQrCodeImage("") // 清空当前二维码
|
||||||
|
|
||||||
|
console.log("正在请求二维码...");
|
||||||
|
|
||||||
|
// 发起请求获取二维码 - 直接使用fetch避免api工具添加基础URL
|
||||||
|
const response = await fetch('http://yi.54word.com/v1/api/device/add', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({})
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("二维码请求响应状态:", response.status);
|
||||||
|
|
||||||
|
// 保存原始响应文本以便调试
|
||||||
|
const responseText = await response.text();
|
||||||
|
console.log("原始响应内容:", responseText);
|
||||||
|
|
||||||
|
// 尝试将响应解析为JSON
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = JSON.parse(responseText);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("响应不是有效的JSON:", e);
|
||||||
|
toast({
|
||||||
|
title: "获取二维码失败",
|
||||||
|
description: "服务器返回的数据格式无效",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("二维码响应数据:", result);
|
||||||
|
|
||||||
|
if (result && result.code === 200) {
|
||||||
|
// 尝试多种可能的返回数据结构
|
||||||
|
let qrcodeData = null;
|
||||||
|
|
||||||
|
if (result.data?.qrCode) {
|
||||||
|
qrcodeData = result.data.qrCode;
|
||||||
|
console.log("找到二维码数据在 result.data.qrCode");
|
||||||
|
} else if (result.data?.qrcode) {
|
||||||
|
qrcodeData = result.data.qrcode;
|
||||||
|
console.log("找到二维码数据在 result.data.qrcode");
|
||||||
|
} else if (result.data?.image) {
|
||||||
|
qrcodeData = result.data.image;
|
||||||
|
console.log("找到二维码数据在 result.data.image");
|
||||||
|
} else if (result.data?.url) {
|
||||||
|
// 如果返回的是URL而不是base64
|
||||||
|
qrcodeData = result.data.url;
|
||||||
|
console.log("找到二维码URL在 result.data.url");
|
||||||
|
setQrCodeImage(qrcodeData);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "二维码已更新",
|
||||||
|
description: "请使用手机扫描新的二维码添加设备",
|
||||||
|
});
|
||||||
|
|
||||||
|
return; // 直接返回,不进行base64处理
|
||||||
|
} else if (typeof result.data === 'string') {
|
||||||
|
// 如果data直接是字符串
|
||||||
|
qrcodeData = result.data;
|
||||||
|
console.log("二维码数据直接在 result.data 字符串中");
|
||||||
|
} else {
|
||||||
|
console.error("无法找到二维码数据:", result);
|
||||||
|
toast({
|
||||||
|
title: "获取二维码失败",
|
||||||
|
description: "返回数据格式不正确",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据是否为空
|
||||||
|
if (!qrcodeData) {
|
||||||
|
console.error("二维码数据为空");
|
||||||
|
toast({
|
||||||
|
title: "获取二维码失败",
|
||||||
|
description: "服务器返回的二维码数据为空",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("处理前的二维码数据:", qrcodeData);
|
||||||
|
|
||||||
|
// 检查是否已经是完整的data URL
|
||||||
|
if (qrcodeData.startsWith('data:image')) {
|
||||||
|
console.log("数据已包含data:image前缀");
|
||||||
|
setQrCodeImage(qrcodeData);
|
||||||
|
}
|
||||||
|
// 检查是否是URL
|
||||||
|
else if (qrcodeData.startsWith('http')) {
|
||||||
|
console.log("数据是HTTP URL");
|
||||||
|
setQrCodeImage(qrcodeData);
|
||||||
|
}
|
||||||
|
// 尝试作为base64处理
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
// 确保base64字符串没有空格等干扰字符
|
||||||
|
const cleanedBase64 = qrcodeData.trim();
|
||||||
|
console.log("处理后的base64数据:", cleanedBase64.substring(0, 30) + "...");
|
||||||
|
|
||||||
|
// 直接以图片src格式设置
|
||||||
|
setQrCodeImage(`data:image/png;base64,${cleanedBase64}`);
|
||||||
|
|
||||||
|
// 预加载图片,确认是否有效
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
console.log("二维码图片加载成功");
|
||||||
|
};
|
||||||
|
img.onerror = (e) => {
|
||||||
|
console.error("二维码图片加载失败:", e);
|
||||||
|
toast({
|
||||||
|
title: "二维码加载失败",
|
||||||
|
description: "服务器返回的数据无法显示为图片",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
img.src = `data:image/png;base64,${cleanedBase64}`;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("处理base64数据出错:", e);
|
||||||
|
toast({
|
||||||
|
title: "获取二维码失败",
|
||||||
|
description: "图片数据处理失败",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "二维码已更新",
|
||||||
|
description: "请使用手机扫描新的二维码添加设备",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("获取二维码失败:", result);
|
||||||
|
toast({
|
||||||
|
title: "获取二维码失败",
|
||||||
|
description: result?.msg || "请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取二维码失败", error);
|
||||||
|
toast({
|
||||||
|
title: "获取二维码失败",
|
||||||
|
description: "请检查网络连接后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoadingQRCode(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开添加设备模态框时获取二维码
|
||||||
|
const handleOpenAddDeviceModal = () => {
|
||||||
|
setIsAddDeviceOpen(true)
|
||||||
|
setDeviceImei("")
|
||||||
|
setDeviceName("")
|
||||||
|
fetchDeviceQRCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过IMEI添加设备
|
||||||
|
const handleAddDeviceByImei = async () => {
|
||||||
|
if (!deviceImei) {
|
||||||
|
toast({
|
||||||
|
title: "IMEI不能为空",
|
||||||
|
description: "请输入有效的设备IMEI",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmittingImei(true);
|
||||||
|
console.log("正在添加设备,IMEI:", deviceImei, "设备名称:", deviceName);
|
||||||
|
|
||||||
|
// 使用api.post发送请求到/v1/devices
|
||||||
|
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/devices`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
imei: deviceImei,
|
||||||
|
memo: deviceName
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("添加设备响应状态:", response.status);
|
||||||
|
|
||||||
|
// 保存原始响应文本以便调试
|
||||||
|
const responseText = await response.text();
|
||||||
|
console.log("原始响应内容:", responseText);
|
||||||
|
|
||||||
|
// 尝试将响应解析为JSON
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = JSON.parse(responseText);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("响应不是有效的JSON:", e);
|
||||||
|
toast({
|
||||||
|
title: "添加设备失败",
|
||||||
|
description: "服务器返回的数据格式无效",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("添加设备响应:", result);
|
||||||
|
|
||||||
|
if (result && result.code === 200) {
|
||||||
|
toast({
|
||||||
|
title: "设备添加成功",
|
||||||
|
description: result.data?.msg || "设备已成功添加",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空输入并关闭弹窗
|
||||||
|
setDeviceImei("");
|
||||||
|
setDeviceName("");
|
||||||
|
setIsAddDeviceOpen(false);
|
||||||
|
|
||||||
|
// 刷新设备列表
|
||||||
|
loadDevices(1, true);
|
||||||
|
} else {
|
||||||
|
console.error("添加设备失败:", result);
|
||||||
|
toast({
|
||||||
|
title: "添加设备失败",
|
||||||
|
description: result?.msg || "请检查设备信息是否正确",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("添加设备请求失败:", error);
|
||||||
|
toast({
|
||||||
|
title: "请求失败",
|
||||||
|
description: "网络错误,请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSubmittingImei(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新设备列表
|
// 刷新设备列表
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
setCurrentPage(1)
|
setCurrentPage(1)
|
||||||
@@ -231,7 +493,7 @@ export default function DevicesPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<h1 className="text-lg font-medium">设备管理</h1>
|
<h1 className="text-lg font-medium">设备管理</h1>
|
||||||
</div>
|
</div>
|
||||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => setIsAddDeviceOpen(true)}>
|
<Button className="bg-blue-600 hover:bg-blue-700" onClick={handleOpenAddDeviceModal}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
添加设备
|
添加设备
|
||||||
</Button>
|
</Button>
|
||||||
@@ -359,22 +621,128 @@ export default function DevicesPage() {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>添加设备</DialogTitle>
|
<DialogTitle>添加设备</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
<div className="space-y-2">
|
<Tabs defaultValue="scan" value={activeTab} onValueChange={setActiveTab} className="mt-4">
|
||||||
<label className="text-sm font-medium">设备名称</label>
|
<TabsList className="grid grid-cols-2 w-full">
|
||||||
<Input placeholder="请输入设备名称" />
|
<TabsTrigger value="scan" className="flex items-center">
|
||||||
</div>
|
<QrCode className="h-4 w-4 mr-2" />
|
||||||
<div className="space-y-2">
|
扫码添加
|
||||||
<label className="text-sm font-medium">IMEI</label>
|
</TabsTrigger>
|
||||||
<Input placeholder="请输入设备IMEI" />
|
<TabsTrigger value="manual" className="flex items-center">
|
||||||
</div>
|
<Smartphone className="h-4 w-4 mr-2" />
|
||||||
<div className="flex justify-end space-x-2">
|
手动添加
|
||||||
<Button variant="outline" onClick={() => setIsAddDeviceOpen(false)}>
|
</TabsTrigger>
|
||||||
取消
|
</TabsList>
|
||||||
</Button>
|
|
||||||
<Button>添加</Button>
|
<TabsContent value="scan" className="space-y-4 py-4">
|
||||||
</div>
|
<div className="flex flex-col items-center justify-center p-6 space-y-4">
|
||||||
</div>
|
<div className="bg-white p-4 rounded-lg shadow-md border border-gray-200 w-full max-w-[280px] min-h-[280px] flex flex-col items-center justify-center">
|
||||||
|
{isLoadingQRCode ? (
|
||||||
|
<div className="flex flex-col items-center justify-center space-y-3">
|
||||||
|
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||||
|
<p className="text-sm text-gray-500">正在获取二维码...</p>
|
||||||
|
</div>
|
||||||
|
) : qrCodeImage ? (
|
||||||
|
<div id="qrcode-container" className="flex flex-col items-center space-y-3">
|
||||||
|
<div className="relative w-64 h-64 flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
src={qrCodeImage}
|
||||||
|
alt="设备添加二维码"
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
onError={(e) => {
|
||||||
|
console.error("二维码图片加载失败");
|
||||||
|
// 隐藏图片
|
||||||
|
e.currentTarget.style.display = 'none';
|
||||||
|
// 显示错误信息
|
||||||
|
const container = document.getElementById('qrcode-container');
|
||||||
|
if (container) {
|
||||||
|
const errorEl = container.querySelector('.qrcode-error');
|
||||||
|
if (errorEl) {
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="qrcode-error hidden absolute inset-0 flex flex-col items-center justify-center text-center text-red-500 bg-white">
|
||||||
|
<AlertTriangle className="h-10 w-10 mb-2" />
|
||||||
|
<p>未能加载二维码,请点击刷新按钮重试</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-center text-gray-600 mt-2">
|
||||||
|
请使用手机扫描此二维码添加设备
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-gray-500">
|
||||||
|
<QrCode className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p>点击下方按钮获取二维码</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={fetchDeviceQRCode}
|
||||||
|
disabled={isLoadingQRCode}
|
||||||
|
className="w-48"
|
||||||
|
>
|
||||||
|
{isLoadingQRCode ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
获取中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
刷新二维码
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="manual" className="space-y-4 py-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">设备名称</label>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入设备名称"
|
||||||
|
value={deviceName}
|
||||||
|
onChange={(e) => setDeviceName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
为设备添加一个便于识别的名称
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">设备IMEI</label>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入设备IMEI"
|
||||||
|
value={deviceImei}
|
||||||
|
onChange={(e) => setDeviceImei(e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
请输入设备IMEI码,可在设备信息中查看
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<Button variant="outline" onClick={() => setIsAddDeviceOpen(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddDeviceByImei}
|
||||||
|
disabled={isSubmittingImei || !deviceImei.trim()}
|
||||||
|
>
|
||||||
|
{isSubmittingImei ? (
|
||||||
|
<>
|
||||||
|
<div className="w-4 h-4 mr-2 rounded-full border-2 border-white border-t-transparent animate-spin"></div>
|
||||||
|
提交中...
|
||||||
|
</>
|
||||||
|
) : "添加"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Route::group('v1/', function () {
|
|||||||
|
|
||||||
// 设备管理相关
|
// 设备管理相关
|
||||||
Route::group('devices', function () {
|
Route::group('devices', function () {
|
||||||
|
Route::get(':id/related-accounts', 'app\\devices\\controller\\Device@getRelatedAccounts'); // 设备关联微信账号路由
|
||||||
Route::get('', 'app\\devices\\controller\\Device@index'); // 获取设备列表
|
Route::get('', 'app\\devices\\controller\\Device@index'); // 获取设备列表
|
||||||
Route::get('count', 'app\\devices\\controller\\Device@count'); // 获取设备总数
|
Route::get('count', 'app\\devices\\controller\\Device@count'); // 获取设备总数
|
||||||
Route::get(':id', 'app\\devices\\controller\\Device@read'); // 获取设备详情
|
Route::get(':id', 'app\\devices\\controller\\Device@read'); // 获取设备详情
|
||||||
@@ -28,4 +29,6 @@ Route::group('v1/', function () {
|
|||||||
Route::put('refresh', 'app\\devices\\controller\\DeviceWechat@refresh'); // 刷新设备微信状态
|
Route::put('refresh', 'app\\devices\\controller\\DeviceWechat@refresh'); // 刷新设备微信状态
|
||||||
Route::post('transfer-friends', 'app\\devices\\controller\\DeviceWechat@transferFriends'); // 微信好友转移
|
Route::post('transfer-friends', 'app\\devices\\controller\\DeviceWechat@transferFriends'); // 微信好友转移
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
})->middleware(['jwt']);
|
})->middleware(['jwt']);
|
||||||
@@ -181,12 +181,6 @@ class Device extends Controller
|
|||||||
try {
|
try {
|
||||||
// 获取登录用户信息
|
// 获取登录用户信息
|
||||||
$userInfo = request()->userInfo;
|
$userInfo = request()->userInfo;
|
||||||
if (empty($userInfo)) {
|
|
||||||
return json([
|
|
||||||
'code' => 401,
|
|
||||||
'msg' => '未登录或登录已过期'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取设备ID
|
// 获取设备ID
|
||||||
$id = Request::param('id/d');
|
$id = Request::param('id/d');
|
||||||
@@ -277,13 +271,7 @@ class Device extends Controller
|
|||||||
try {
|
try {
|
||||||
// 获取登录用户信息
|
// 获取登录用户信息
|
||||||
$userInfo = request()->userInfo;
|
$userInfo = request()->userInfo;
|
||||||
if (empty($userInfo)) {
|
|
||||||
return json([
|
|
||||||
'code' => 401,
|
|
||||||
'msg' => '未登录或登录已过期'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查用户权限,只有管理员可以添加设备
|
// 检查用户权限,只有管理员可以添加设备
|
||||||
if ($userInfo['isAdmin'] != 1) {
|
if ($userInfo['isAdmin'] != 1) {
|
||||||
return json([
|
return json([
|
||||||
@@ -305,6 +293,7 @@ class Device extends Controller
|
|||||||
|
|
||||||
// 验证IMEI是否已存在
|
// 验证IMEI是否已存在
|
||||||
$exists = DeviceModel::where('imei', $data['imei'])->where('isDeleted', 0)->find();
|
$exists = DeviceModel::where('imei', $data['imei'])->where('isDeleted', 0)->find();
|
||||||
|
|
||||||
if ($exists) {
|
if ($exists) {
|
||||||
return json([
|
return json([
|
||||||
'code' => 400,
|
'code' => 400,
|
||||||
@@ -314,10 +303,12 @@ class Device extends Controller
|
|||||||
|
|
||||||
// 设置设备公司ID
|
// 设置设备公司ID
|
||||||
$data['companyId'] = $userInfo['companyId'];
|
$data['companyId'] = $userInfo['companyId'];
|
||||||
|
$data['id'] = time();
|
||||||
|
|
||||||
// 添加设备
|
// 添加设备
|
||||||
$id = DeviceModel::addDevice($data);
|
$id = DeviceModel::addDevice($data);
|
||||||
|
|
||||||
|
// 此处调用底层API
|
||||||
return json([
|
return json([
|
||||||
'code' => 200,
|
'code' => 200,
|
||||||
'msg' => '添加成功',
|
'msg' => '添加成功',
|
||||||
@@ -465,4 +456,72 @@ class Device extends Controller
|
|||||||
return json(['code' => 500, 'msg' => '更新任务配置失败']);
|
return json(['code' => 500, 'msg' => '更新任务配置失败']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备关联的微信账号
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function getRelatedAccounts()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 获取登录用户信息
|
||||||
|
$userInfo = request()->userInfo;
|
||||||
|
|
||||||
|
// 获取设备ID
|
||||||
|
$deviceId = $this->request->param('id/d');
|
||||||
|
if (empty($deviceId)) {
|
||||||
|
return json([
|
||||||
|
'code' => 400,
|
||||||
|
'msg' => '设备ID不能为空'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否有权限访问该设备
|
||||||
|
if ($userInfo['isAdmin'] != 1) {
|
||||||
|
// 非管理员需要检查是否有权限访问该设备
|
||||||
|
$hasPermission = \app\common\model\DeviceUser::checkUserDevicePermission(
|
||||||
|
$userInfo['id'],
|
||||||
|
$deviceId,
|
||||||
|
$userInfo['companyId']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$hasPermission) {
|
||||||
|
return json([
|
||||||
|
'code' => 403,
|
||||||
|
'msg' => '您没有权限查看该设备'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备信息,确认设备存在
|
||||||
|
$device = DeviceModel::where('id', $deviceId)
|
||||||
|
->where('isDeleted', 0)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$device) {
|
||||||
|
return json([
|
||||||
|
'code' => 404,
|
||||||
|
'msg' => '设备不存在或已删除'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备关联的微信账号
|
||||||
|
$wechatAccounts = \app\devices\model\DeviceWechatLogin::getDeviceRelatedAccounts($deviceId, $userInfo['companyId']);
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '获取成功',
|
||||||
|
'data' => [
|
||||||
|
'deviceId' => $deviceId,
|
||||||
|
'accounts' => $wechatAccounts,
|
||||||
|
'total' => count($wechatAccounts)
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return json([
|
||||||
|
'code' => 500,
|
||||||
|
'msg' => '获取失败:' . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
207
Server/application/devices/model/DeviceWechatLogin.php
Normal file
207
Server/application/devices/model/DeviceWechatLogin.php
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\devices\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备微信登录记录模型类
|
||||||
|
*/
|
||||||
|
class DeviceWechatLogin extends Model
|
||||||
|
{
|
||||||
|
// 设置表名
|
||||||
|
protected $name = 'device_wechat_login';
|
||||||
|
|
||||||
|
// 设置主键
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
// 自动写入时间戳
|
||||||
|
protected $autoWriteTimestamp = 'int';
|
||||||
|
|
||||||
|
// 定义时间戳字段名
|
||||||
|
protected $createTime = 'createTime';
|
||||||
|
|
||||||
|
// 定义字段类型
|
||||||
|
protected $type = [
|
||||||
|
'id' => 'integer',
|
||||||
|
'deviceId' => 'integer',
|
||||||
|
'companyId' => 'integer',
|
||||||
|
'createTime' => 'integer'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询设备关联的微信ID列表
|
||||||
|
* @param int $deviceId 设备ID
|
||||||
|
* @param int $companyId 公司/租户ID
|
||||||
|
* @return array 微信ID列表
|
||||||
|
*/
|
||||||
|
public static function getDeviceWechatIds($deviceId, $companyId = null)
|
||||||
|
{
|
||||||
|
$query = self::where('deviceId', $deviceId);
|
||||||
|
|
||||||
|
// 如果提供了公司ID,则添加对应的条件
|
||||||
|
if ($companyId !== null) {
|
||||||
|
$query->where('companyId', $companyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取微信ID
|
||||||
|
$records = $query->select();
|
||||||
|
$wechatIds = [];
|
||||||
|
|
||||||
|
foreach ($records as $record) {
|
||||||
|
if (!empty($record['wechatId'])) {
|
||||||
|
$wechatIds[] = $record['wechatId'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wechatIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据微信ID查询关联的设备
|
||||||
|
* @param string $wechatId 微信ID
|
||||||
|
* @param int $companyId 公司/租户ID
|
||||||
|
* @return array 设备ID列表
|
||||||
|
*/
|
||||||
|
public static function getWechatDeviceIds($wechatId, $companyId = null)
|
||||||
|
{
|
||||||
|
$query = self::where('wechatId', $wechatId);
|
||||||
|
|
||||||
|
// 如果提供了公司ID,则添加对应的条件
|
||||||
|
if ($companyId !== null) {
|
||||||
|
$query->where('companyId', $companyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取设备ID
|
||||||
|
$records = $query->select();
|
||||||
|
$deviceIds = [];
|
||||||
|
|
||||||
|
foreach ($records as $record) {
|
||||||
|
if (!empty($record['deviceId'])) {
|
||||||
|
$deviceIds[] = $record['deviceId'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $deviceIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加设备微信登录记录
|
||||||
|
* @param int $deviceId 设备ID
|
||||||
|
* @param string $wechatId 微信ID
|
||||||
|
* @param int $companyId 公司/租户ID
|
||||||
|
* @return int 新增记录ID
|
||||||
|
*/
|
||||||
|
public static function addRecord($deviceId, $wechatId, $companyId)
|
||||||
|
{
|
||||||
|
// 检查是否已存在相同记录
|
||||||
|
$exists = self::where('deviceId', $deviceId)
|
||||||
|
->where('wechatId', $wechatId)
|
||||||
|
->where('companyId', $companyId)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
return $exists['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新记录
|
||||||
|
$model = new self();
|
||||||
|
$model->deviceId = $deviceId;
|
||||||
|
$model->wechatId = $wechatId;
|
||||||
|
$model->companyId = $companyId;
|
||||||
|
$model->save();
|
||||||
|
|
||||||
|
return $model->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除设备微信登录记录
|
||||||
|
* @param int $deviceId 设备ID
|
||||||
|
* @param string $wechatId 微信ID,为null时删除设备所有记录
|
||||||
|
* @param int $companyId 公司/租户ID,为null时不限公司
|
||||||
|
* @return bool 删除结果
|
||||||
|
*/
|
||||||
|
public static function removeRecord($deviceId, $wechatId = null, $companyId = null)
|
||||||
|
{
|
||||||
|
$query = self::where('deviceId', $deviceId);
|
||||||
|
|
||||||
|
if ($wechatId !== null) {
|
||||||
|
$query->where('wechatId', $wechatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($companyId !== null) {
|
||||||
|
$query->where('companyId', $companyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联Device模型
|
||||||
|
* @return \think\model\relation\BelongsTo
|
||||||
|
*/
|
||||||
|
public function device()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('Device', 'deviceId');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备关联的微信账号信息
|
||||||
|
* @param int $deviceId 设备ID
|
||||||
|
* @param int $companyId 公司/租户ID
|
||||||
|
* @return array 微信账号信息列表
|
||||||
|
*/
|
||||||
|
public static function getDeviceRelatedAccounts($deviceId, $companyId = null)
|
||||||
|
{
|
||||||
|
// 获取设备关联的微信ID列表
|
||||||
|
$wechatIds = self::getDeviceWechatIds($deviceId, $companyId);
|
||||||
|
if (empty($wechatIds)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询微信账号信息
|
||||||
|
$accounts = \think\Db::name('wechat_account')
|
||||||
|
->alias('wa')
|
||||||
|
->field([
|
||||||
|
'wa.id',
|
||||||
|
'wa.wechatId',
|
||||||
|
'wa.accountNickname',
|
||||||
|
'wa.nickname',
|
||||||
|
'wa.accountUserName',
|
||||||
|
'wa.avatar',
|
||||||
|
'wa.gender',
|
||||||
|
'wa.wechatAlive',
|
||||||
|
'wa.status',
|
||||||
|
'wa.totalFriend',
|
||||||
|
'wa.createTime',
|
||||||
|
'wa.updateTime'
|
||||||
|
])
|
||||||
|
->whereIn('wa.wechatId', $wechatIds)
|
||||||
|
->where('wa.isDeleted', 0)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
// 处理结果数据
|
||||||
|
$result = [];
|
||||||
|
foreach ($accounts as $account) {
|
||||||
|
// 计算最后活跃时间
|
||||||
|
$lastActive = date('Y-m-d H:i:s', max($account['updateTime'], $account['createTime']));
|
||||||
|
|
||||||
|
// 格式化数据
|
||||||
|
$result[] = [
|
||||||
|
'id' => $account['id'],
|
||||||
|
'wechatId' => $account['wechatId'],
|
||||||
|
'nickname' => $account['accountNickname'] ?: $account['nickname'] ?: '未命名微信',
|
||||||
|
'accountUserName' => $account['accountUserName'],
|
||||||
|
'avatar' => $account['avatar'],
|
||||||
|
'gender' => intval($account['gender']),
|
||||||
|
'status' => intval($account['status']),
|
||||||
|
'statusText' => intval($account['status']) === 1 ? '可加友' : '已停用',
|
||||||
|
'wechatAlive' => intval($account['wechatAlive']),
|
||||||
|
'wechatAliveText' => intval($account['wechatAlive']) === 1 ? '正常' : '异常',
|
||||||
|
'totalFriend' => intval($account['totalFriend']),
|
||||||
|
'lastActive' => $lastActive
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user