算力功能改版
This commit is contained in:
@@ -149,14 +149,14 @@ const Home: React.FC = () => {
|
||||
<div className={style["stat-card"]} onClick={handleDevicesClick}>
|
||||
<div className={style["stat-label"]}>设备数量</div>
|
||||
<div className={style["stat-value"]}>
|
||||
<span>{dashboard.deviceNum || 42}</span>
|
||||
<span>{dashboard.deviceNum || 0}</span>
|
||||
<MobileOutlined style={{ fontSize: 20, color: "#3b82f6" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["stat-card"]} onClick={handleWechatClick}>
|
||||
<div className={style["stat-label"]}>微信号数量</div>
|
||||
<div className={style["stat-value"]}>
|
||||
<span>{dashboard.wechatNum || 42}</span>
|
||||
<span>{dashboard.wechatNum || 0}</span>
|
||||
<TeamOutlined style={{ fontSize: 20, color: "#3b82f6" }} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,7 +166,7 @@ const Home: React.FC = () => {
|
||||
>
|
||||
<div className={style["stat-label"]}>在线微信号</div>
|
||||
<div className={style["stat-value"]}>
|
||||
<span>{dashboard.aliveWechatNum || 35}</span>
|
||||
<span>{dashboard.aliveWechatNum || 0}</span>
|
||||
<LineChartOutlined style={{ fontSize: 20, color: "#3b82f6" }} />
|
||||
</div>
|
||||
<div className={style["progress-bar"]}>
|
||||
|
||||
@@ -42,3 +42,7 @@ export const fetchDeviceQRCode = (accountId: string) =>
|
||||
// 通过IMEI添加设备
|
||||
export const addDeviceByImei = (imei: string, name: string) =>
|
||||
request("/v1/api/device/add-by-imei", { imei, name }, "POST");
|
||||
|
||||
// 获取设备添加结果(用于轮询检查)
|
||||
export const fetchAddResults = (params: { accountId?: string }) =>
|
||||
request("/v1/devices/add-results", params, "GET");
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
fetchDeviceQRCode,
|
||||
addDeviceByImei,
|
||||
deleteDevice,
|
||||
fetchAddResults,
|
||||
} from "./api";
|
||||
import type { Device } from "@/types/device";
|
||||
import { comfirm } from "@/utils/common";
|
||||
@@ -44,12 +45,18 @@ const Devices: React.FC = () => {
|
||||
const [name, setName] = useState("");
|
||||
const [addLoading, setAddLoading] = useState(false);
|
||||
|
||||
// 轮询监听相关
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const pollingRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const loadDevicesRef = useRef<((reset?: boolean) => Promise<void>) | null>(null);
|
||||
|
||||
// 删除弹窗
|
||||
const [delVisible, setDelVisible] = useState(false);
|
||||
const [delLoading, setDelLoading] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { user } = useUserStore();
|
||||
|
||||
// 加载设备列表
|
||||
const loadDevices = useCallback(
|
||||
async (reset = false) => {
|
||||
@@ -74,6 +81,11 @@ const Devices: React.FC = () => {
|
||||
[loading, search, page],
|
||||
);
|
||||
|
||||
// 更新 loadDevices 的 ref
|
||||
useEffect(() => {
|
||||
loadDevicesRef.current = loadDevices;
|
||||
}, [loadDevices]);
|
||||
|
||||
// 首次加载和搜索
|
||||
useEffect(() => {
|
||||
loadDevices(true);
|
||||
@@ -110,6 +122,56 @@ const Devices: React.FC = () => {
|
||||
return true;
|
||||
});
|
||||
|
||||
// 开始轮询监听设备状态
|
||||
const startPolling = useCallback(() => {
|
||||
if (isPolling) return;
|
||||
|
||||
setIsPolling(true);
|
||||
|
||||
const pollDeviceStatus = async () => {
|
||||
try {
|
||||
const res = await fetchAddResults({ accountId: user?.s2_accountId });
|
||||
if (res.added) {
|
||||
Toast.show({ content: "设备添加成功!", position: "top" });
|
||||
setAddVisible(false);
|
||||
setIsPolling(false);
|
||||
if (pollingRef.current) {
|
||||
clearInterval(pollingRef.current);
|
||||
pollingRef.current = null;
|
||||
}
|
||||
// 刷新设备列表
|
||||
if (loadDevicesRef.current) {
|
||||
await loadDevicesRef.current(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("轮询检查设备状态失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 每3秒检查一次设备状态
|
||||
pollingRef.current = setInterval(pollDeviceStatus, 3000);
|
||||
}, [isPolling, user?.s2_accountId]);
|
||||
|
||||
// 停止轮询
|
||||
const stopPolling = useCallback(() => {
|
||||
setIsPolling(false);
|
||||
if (pollingRef.current) {
|
||||
clearInterval(pollingRef.current);
|
||||
pollingRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 组件卸载时清理轮询
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pollingRef.current) {
|
||||
clearInterval(pollingRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 获取二维码
|
||||
const handleGetQr = async () => {
|
||||
setQrLoading(true);
|
||||
@@ -119,6 +181,8 @@ const Devices: React.FC = () => {
|
||||
if (!accountId) throw new Error("未获取到用户信息");
|
||||
const res = await fetchDeviceQRCode(accountId);
|
||||
setQrCode(res.qrCode);
|
||||
// 获取二维码后开始轮询监听
|
||||
startPolling();
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e.message || "获取二维码失败", position: "top" });
|
||||
} finally {
|
||||
@@ -362,7 +426,11 @@ const Devices: React.FC = () => {
|
||||
{/* 添加设备弹窗 */}
|
||||
<Popup
|
||||
visible={addVisible}
|
||||
onMaskClick={() => setAddVisible(false)}
|
||||
onMaskClick={() => {
|
||||
setAddVisible(false);
|
||||
stopPolling();
|
||||
setQrCode(null);
|
||||
}}
|
||||
bodyStyle={{
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
@@ -403,6 +471,13 @@ const Devices: React.FC = () => {
|
||||
<div style={{ color: "#888", fontSize: 12, marginTop: 8 }}>
|
||||
请用手机扫码添加设备
|
||||
</div>
|
||||
{isPolling && (
|
||||
<div
|
||||
style={{ color: "#1890ff", fontSize: 12, marginTop: 8 }}
|
||||
>
|
||||
正在监听设备添加状态...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -173,6 +173,8 @@ export function queryOrder(orderNo: string): Promise<QueryOrderResponse> {
|
||||
// 账号信息
|
||||
export interface Account {
|
||||
id: number;
|
||||
uid?: number; // 用户ID(用于分配算力)
|
||||
userId?: number; // 用户ID(别名)
|
||||
userName: string;
|
||||
realName: string;
|
||||
nickname: string;
|
||||
@@ -185,3 +187,15 @@ export interface Account {
|
||||
export function getAccountList(): Promise<{ list: Account[]; total: number }> {
|
||||
return request("/v1/kefu/accounts/list", undefined, "GET");
|
||||
}
|
||||
|
||||
// 分配算力接口参数
|
||||
export interface AllocateTokensParams {
|
||||
targetUserId: number; // 目标用户ID
|
||||
tokens: number; // 分配的算力数量
|
||||
remarks?: string; // 备注
|
||||
}
|
||||
|
||||
// 分配算力
|
||||
export function allocateTokens(params: AllocateTokensParams): Promise<any> {
|
||||
return request("/v1/tokens/allocate", params, "POST");
|
||||
}
|
||||
|
||||
@@ -85,6 +85,10 @@
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.navTab {
|
||||
@@ -218,115 +222,87 @@
|
||||
margin-bottom: 12px;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: none;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); // 更柔和的阴影
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.recordItem:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 左侧区域 */
|
||||
.recordLeft {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recordIconWrapper {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0; // 图标顶部与标题顶部对齐
|
||||
}
|
||||
|
||||
.recordIconWrapperConsume {
|
||||
background-color: #fff4e6; // 浅橙色背景
|
||||
}
|
||||
|
||||
.recordIconWrapperRecharge {
|
||||
background-color: #f6ffed; // 浅绿色背景
|
||||
}
|
||||
|
||||
.recordIcon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.recordIconWrapperConsume .recordIcon {
|
||||
color: #ff7a00; // 橙色图标
|
||||
}
|
||||
|
||||
.recordIconWrapperRecharge .recordIcon {
|
||||
color: #52c41a; // 绿色图标
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.recordInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.recordTitle {
|
||||
font-size: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
color: #333; // 深灰色标题文字
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recordTime {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 右侧区域 */
|
||||
.recordRight {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
padding-top: 19px; // 与时间戳对齐:标题高度(15px * 1.4) + margin-bottom(4px) ≈ 19px
|
||||
margin-left: 16px; // 增加左右间距
|
||||
}
|
||||
|
||||
.recordPowerWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.recordPower {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4; // 与时间戳行高一致
|
||||
}
|
||||
|
||||
.recordPowerConsume {
|
||||
color: #ff7a00; // 橙色 - 扣除
|
||||
}
|
||||
|
||||
.recordPowerRecharge {
|
||||
color: #52c41a; // 绿色 - 增加
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.recordPowerUnit {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
line-height: 1.4; // 与时间戳行高一致
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, Toast, Picker } from "antd-mobile";
|
||||
import { Card, Toast, Picker, InfiniteScroll } from "antd-mobile";
|
||||
import style from "./index.module.scss";
|
||||
import {
|
||||
ThunderboltOutlined,
|
||||
@@ -25,7 +25,8 @@ import {
|
||||
import { Button, Dialog, Input, Popup } from "antd-mobile";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import { getStatistics, getOrderList, queryOrder, getAccountList } from "./api";
|
||||
import { useUserStore } from "@/store";
|
||||
import { getStatistics, getOrderList, queryOrder, getAccountList, allocateTokens } from "./api";
|
||||
import type { QueryOrderResponse, Account } from "./api";
|
||||
import { getTokensUseRecord } from "../usage-records/api";
|
||||
import { getTaocanList, buyPackage } from "../buy-power/api";
|
||||
@@ -116,10 +117,20 @@ const getRecordTitle = (item: TokensUseRecordItem): string => {
|
||||
};
|
||||
|
||||
const PowerManagement: React.FC = () => {
|
||||
const user = useUserStore(state => state.user);
|
||||
const isAdmin = user?.isAdmin === 1; // 判断是否为管理员
|
||||
const [activeTab, setActiveTab] = useState("details"); // details, buy, orders, allocation
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [stats, setStats] = useState<Statistics | null>(null);
|
||||
const [records, setRecords] = useState<TokensUseRecordItem[]>([]);
|
||||
const [recordPage, setRecordPage] = useState(1);
|
||||
const [recordTotal, setRecordTotal] = useState(0);
|
||||
const [recordHasMore, setRecordHasMore] = useState(true);
|
||||
const [recordLoadingMore, setRecordLoadingMore] = useState(false);
|
||||
const [orderPage, setOrderPage] = useState(1);
|
||||
const [orderTotal, setOrderTotal] = useState(0);
|
||||
const [orderHasMore, setOrderHasMore] = useState(true);
|
||||
const [orderLoadingMore, setOrderLoadingMore] = useState(false);
|
||||
const [packages, setPackages] = useState<PowerPackage[]>([]);
|
||||
const [buyLoading, setBuyLoading] = useState(false);
|
||||
const [customAmount, setCustomAmount] = useState<string>("");
|
||||
@@ -135,7 +146,11 @@ const PowerManagement: React.FC = () => {
|
||||
const [allocationAccount, setAllocationAccount] = useState<Account | null>(null);
|
||||
const [allocationAmount, setAllocationAmount] = useState<string>("");
|
||||
const [allocationAccountVisible, setAllocationAccountVisible] = useState(false);
|
||||
const [allocationRecords, setAllocationRecords] = useState<any[]>([]);
|
||||
const [allocationRecords, setAllocationRecords] = useState<TokensUseRecordItem[]>([]);
|
||||
const [allocationPage, setAllocationPage] = useState(1);
|
||||
const [allocationTotal, setAllocationTotal] = useState(0);
|
||||
const [allocationHasMore, setAllocationHasMore] = useState(true);
|
||||
const [allocationLoadingMore, setAllocationLoadingMore] = useState(false);
|
||||
const [allocationAccountFilter, setAllocationAccountFilter] = useState<string>("all");
|
||||
const [allocationTimeFilter, setAllocationTimeFilter] = useState<string>("7days");
|
||||
const [allocationAccountFilterVisible, setAllocationAccountFilterVisible] = useState(false);
|
||||
@@ -182,6 +197,9 @@ const PowerManagement: React.FC = () => {
|
||||
{ label: "AI改写", value: "12" },
|
||||
{ label: "AI客服", value: "13" },
|
||||
{ label: "生成群公告", value: "14" },
|
||||
{ label: "商家", value: "1001" },
|
||||
{ label: "充值", value: "1002" },
|
||||
{ label: "系统", value: "1003" },
|
||||
];
|
||||
|
||||
const actionOptions = [
|
||||
@@ -197,30 +215,48 @@ const PowerManagement: React.FC = () => {
|
||||
{ label: "全部时间", value: "all" },
|
||||
];
|
||||
|
||||
// 导航标签数据
|
||||
// 导航标签数据(根据管理员权限过滤)
|
||||
const navTabs = [
|
||||
{ key: "details", label: "算力明细" },
|
||||
{ key: "buy", label: "购买算力" },
|
||||
{ key: "orders", label: "购买记录" },
|
||||
{ key: "allocation", label: "算力分配" },
|
||||
// 只有管理员才能看到算力分配
|
||||
...(isAdmin ? [{ key: "allocation", label: "算力分配" }] : []),
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// 如果非管理员尝试访问分配页面,自动跳转到明细页面
|
||||
if (activeTab === "allocation" && !isAdmin) {
|
||||
setActiveTab("details");
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeTab === "details") {
|
||||
fetchRecords();
|
||||
// 筛选条件变化时重置
|
||||
setRecordPage(1);
|
||||
setRecordHasMore(true);
|
||||
fetchRecords(1, false);
|
||||
} else if (activeTab === "buy") {
|
||||
fetchPackages();
|
||||
} else if (activeTab === "orders") {
|
||||
fetchOrders();
|
||||
} else if (activeTab === "allocation") {
|
||||
// 筛选条件变化时重置
|
||||
setOrderPage(1);
|
||||
setOrderHasMore(true);
|
||||
fetchOrders(1, false);
|
||||
} else if (activeTab === "allocation" && isAdmin) {
|
||||
fetchAccounts();
|
||||
// 重置分配记录
|
||||
setAllocationPage(1);
|
||||
setAllocationHasMore(true);
|
||||
fetchAllocationRecords(1, false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeTab, filterType, filterAction, filterTime, orderStatus, orderTime, orderPayType]);
|
||||
}, [activeTab, filterType, filterAction, filterTime, orderStatus, orderTime, orderPayType, orderKeyword, isAdmin]);
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
@@ -232,13 +268,17 @@ const PowerManagement: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRecords = async () => {
|
||||
setLoading(true);
|
||||
const fetchRecords = async (page: number = 1, append: boolean = false) => {
|
||||
if (append) {
|
||||
setRecordLoadingMore(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
// 根据筛选条件构建参数
|
||||
const params: any = {
|
||||
page: "1",
|
||||
limit: "50", // 显示更多记录
|
||||
page: String(page),
|
||||
limit: "10",
|
||||
type: filterAction === "consume" ? "0" : filterAction === "recharge" ? "1" : undefined,
|
||||
};
|
||||
|
||||
@@ -266,15 +306,31 @@ const PowerManagement: React.FC = () => {
|
||||
|
||||
// 处理返回数据:request拦截器会返回 res.data.data,所以直接使用 res.list
|
||||
let list = Array.isArray(res?.list) ? res.list : [];
|
||||
console.log("处理后的列表:", list, "列表长度:", list.length);
|
||||
const total = res?.total || 0;
|
||||
|
||||
console.log("最终列表:", list);
|
||||
setRecords(list);
|
||||
console.log("处理后的列表:", list, "列表长度:", list.length, "总数:", total);
|
||||
|
||||
if (append) {
|
||||
// 追加数据
|
||||
setRecords(prev => [...prev, ...list]);
|
||||
} else {
|
||||
// 替换数据
|
||||
setRecords(list);
|
||||
}
|
||||
|
||||
setRecordTotal(total);
|
||||
// 判断是否还有更多数据
|
||||
const hasMore = records.length + list.length < total;
|
||||
setRecordHasMore(hasMore);
|
||||
} catch (error) {
|
||||
console.error("获取使用记录失败:", error);
|
||||
Toast.show({ content: "获取使用记录失败", position: "top" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (append) {
|
||||
setRecordLoadingMore(false);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -291,12 +347,16 @@ const PowerManagement: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchOrders = async () => {
|
||||
setLoading(true);
|
||||
const fetchOrders = async (page: number = 1, append: boolean = false) => {
|
||||
if (append) {
|
||||
setOrderLoadingMore(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
const params: any = {
|
||||
page: "1",
|
||||
limit: "50",
|
||||
page: String(page),
|
||||
limit: "10",
|
||||
orderType: "1", // 算力充值
|
||||
};
|
||||
|
||||
@@ -331,12 +391,34 @@ const PowerManagement: React.FC = () => {
|
||||
|
||||
const res = await getOrderList(params);
|
||||
const list = res.list || [];
|
||||
setOrders(list);
|
||||
const total = res.total || 0;
|
||||
|
||||
if (append) {
|
||||
// 追加数据
|
||||
setOrders(prev => [...prev, ...list]);
|
||||
} else {
|
||||
// 替换数据
|
||||
setOrders(list);
|
||||
}
|
||||
|
||||
setOrderTotal(total);
|
||||
// 判断是否还有更多数据
|
||||
if (append) {
|
||||
const hasMore = orders.length + list.length < total;
|
||||
setOrderHasMore(hasMore);
|
||||
} else {
|
||||
const hasMore = list.length < total;
|
||||
setOrderHasMore(hasMore);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取订单列表失败:", error);
|
||||
Toast.show({ content: "获取订单列表失败", position: "top" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (append) {
|
||||
setOrderLoadingMore(false);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -556,14 +638,72 @@ const PowerManagement: React.FC = () => {
|
||||
return account.nickname || `账号${account.id}`;
|
||||
};
|
||||
|
||||
// 获取分配记录
|
||||
const fetchAllocationRecords = async () => {
|
||||
// TODO: 实现真实的分配记录接口
|
||||
setAllocationRecords([]);
|
||||
// 获取分配记录(form=1001的算力明细记录)
|
||||
const fetchAllocationRecords = async (page: number = 1, append: boolean = false) => {
|
||||
if (append) {
|
||||
setAllocationLoadingMore(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
const params: any = {
|
||||
page: String(page),
|
||||
limit: "10",
|
||||
form: "1001", // 商家类型
|
||||
};
|
||||
|
||||
// 账号筛选
|
||||
if (allocationAccountFilter !== "all") {
|
||||
// 这里需要根据账号ID筛选,可能需要后端支持
|
||||
// 暂时先不处理,等后端接口支持
|
||||
}
|
||||
|
||||
// 时间筛选
|
||||
if (allocationTimeFilter !== "all") {
|
||||
const now = new Date();
|
||||
const daysMap: Record<string, number> = {
|
||||
"7days": 7,
|
||||
"30days": 30,
|
||||
"90days": 90,
|
||||
};
|
||||
const days = daysMap[allocationTimeFilter] || 7;
|
||||
const startTime = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
||||
params.startTime = startTime.toISOString().slice(0, 10);
|
||||
params.endTime = now.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
const res = await getTokensUseRecord(params);
|
||||
let list = Array.isArray(res?.list) ? res.list : [];
|
||||
const total = res?.total || 0;
|
||||
|
||||
if (append) {
|
||||
setAllocationRecords(prev => [...prev, ...list]);
|
||||
} else {
|
||||
setAllocationRecords(list);
|
||||
}
|
||||
|
||||
setAllocationTotal(total);
|
||||
if (append) {
|
||||
const hasMore = allocationRecords.length + list.length < total;
|
||||
setAllocationHasMore(hasMore);
|
||||
} else {
|
||||
const hasMore = list.length < total;
|
||||
setAllocationHasMore(hasMore);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取分配记录失败:", error);
|
||||
Toast.show({ content: "获取分配记录失败", position: "top" });
|
||||
} finally {
|
||||
if (append) {
|
||||
setAllocationLoadingMore(false);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理确认分配
|
||||
const handleConfirmAllocation = () => {
|
||||
const handleConfirmAllocation = async () => {
|
||||
if (!allocationAccount) {
|
||||
Toast.show({ content: "请选择账号", position: "top" });
|
||||
return;
|
||||
@@ -578,18 +718,38 @@ const PowerManagement: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 调用真实的分配接口
|
||||
Toast.show({ content: "分配成功", position: "top" });
|
||||
setAllocationAccount(null);
|
||||
setAllocationAmount("");
|
||||
// 重新加载记录
|
||||
fetchAllocationRecords();
|
||||
setLoading(true);
|
||||
try {
|
||||
// 调用分配接口
|
||||
await allocateTokens({
|
||||
targetUserId: allocationAccount.uid || allocationAccount.userId || allocationAccount.id,
|
||||
tokens: amount,
|
||||
remarks: `分配给${getAccountDisplayName(allocationAccount)}`,
|
||||
});
|
||||
|
||||
Toast.show({ content: "分配成功", position: "top" });
|
||||
setAllocationAccount(null);
|
||||
setAllocationAmount("");
|
||||
|
||||
// 刷新统计数据
|
||||
fetchStats();
|
||||
|
||||
// 重新加载记录
|
||||
setAllocationPage(1);
|
||||
setAllocationHasMore(true);
|
||||
fetchAllocationRecords(1, false);
|
||||
} catch (error) {
|
||||
console.error("分配失败:", error);
|
||||
Toast.show({ content: "分配失败,请重试", position: "top" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 分配记录筛选选项
|
||||
const allocationAccountOptions = [
|
||||
{ label: "全部账号", value: "all" },
|
||||
...accounts.map(acc => ({ label: getAccountDisplayName(acc), value: acc.id.toString() })),
|
||||
...accounts.map(acc => ({ label: getAccountDisplayName(acc), value: (acc.uid || acc.userId || acc.id).toString() })),
|
||||
];
|
||||
|
||||
const allocationTimeOptions = [
|
||||
@@ -765,6 +925,30 @@ const PowerManagement: React.FC = () => {
|
||||
// 移除导航逻辑,只切换tab
|
||||
};
|
||||
|
||||
// 触底加载更多 - 算力明细
|
||||
const loadMoreRecords = async () => {
|
||||
if (recordLoadingMore || !recordHasMore) return;
|
||||
const nextPage = recordPage + 1;
|
||||
setRecordPage(nextPage);
|
||||
await fetchRecords(nextPage, true);
|
||||
};
|
||||
|
||||
// 触底加载更多 - 订单
|
||||
const loadMoreOrders = async () => {
|
||||
if (orderLoadingMore || !orderHasMore) return;
|
||||
const nextPage = orderPage + 1;
|
||||
setOrderPage(nextPage);
|
||||
await fetchOrders(nextPage, true);
|
||||
};
|
||||
|
||||
// 触底加载更多 - 分配记录
|
||||
const loadMoreAllocationRecords = async () => {
|
||||
if (allocationLoadingMore || !allocationHasMore) return;
|
||||
const nextPage = allocationPage + 1;
|
||||
setAllocationPage(nextPage);
|
||||
await fetchAllocationRecords(nextPage, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
loading={loading && !stats}
|
||||
@@ -931,31 +1115,56 @@ const PowerManagement: React.FC = () => {
|
||||
<div className={style.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : records.length > 0 ? (
|
||||
records.map(record => (
|
||||
<Card key={record.id} className={style.recordItem}>
|
||||
<div className={style.recordLeft}>
|
||||
<div className={`${style.recordIconWrapper} ${record.type === 0 ? style.recordIconWrapperConsume : style.recordIconWrapperRecharge}`}>
|
||||
<ThunderboltOutlined className={style.recordIcon} />
|
||||
</div>
|
||||
<div className={style.recordInfo}>
|
||||
<div className={style.recordTitle}>
|
||||
{getRecordTitle(record)}
|
||||
<>
|
||||
{records.map(record => {
|
||||
const isConsume = record.type === 0;
|
||||
const powerColor = isConsume ? '#ff7a00' : '#52c41a';
|
||||
const iconBgColor = isConsume ? '#fff4e6' : '#f6ffed';
|
||||
|
||||
return (
|
||||
<div key={record.id} className={style.recordItem}>
|
||||
{/* 左侧:图标 + 文字信息 */}
|
||||
<div className={style.recordLeft}>
|
||||
<div
|
||||
className={style.recordIconWrapper}
|
||||
style={{ backgroundColor: iconBgColor }}
|
||||
>
|
||||
<ThunderboltOutlined
|
||||
className={style.recordIcon}
|
||||
style={{ color: powerColor }}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.recordInfo}>
|
||||
<div
|
||||
className={style.recordTitle}
|
||||
style={{ color: powerColor }}
|
||||
>
|
||||
{getRecordTitle(record)}
|
||||
</div>
|
||||
<div className={style.recordTime}>
|
||||
{formatDateTime(record.createTime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.recordTime}>
|
||||
{formatDateTime(record.createTime)}
|
||||
{/* 右侧:算力数值 */}
|
||||
<div className={style.recordRight}>
|
||||
<div
|
||||
className={style.recordPower}
|
||||
style={{ color: powerColor }}
|
||||
>
|
||||
{isConsume ? '-' : '+'}{formatNumber(Math.abs(record.tokens))}
|
||||
</div>
|
||||
<div className={style.recordPowerUnit}>算力点</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.recordRight}>
|
||||
<div className={style.recordPowerWrapper}>
|
||||
<div className={`${style.recordPower} ${record.type === 0 ? style.recordPowerConsume : style.recordPowerRecharge}`}>
|
||||
{record.type === 0 ? '-' : '+'}{formatNumber(Math.abs(record.tokens))}
|
||||
</div>
|
||||
<div className={style.recordPowerUnit}>算力点</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
);
|
||||
})}
|
||||
<InfiniteScroll
|
||||
loadMore={loadMoreRecords}
|
||||
hasMore={recordHasMore}
|
||||
threshold={100}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className={style.emptyRecords}>
|
||||
<div className={style.emptyText}>暂无消耗记录</div>
|
||||
@@ -1140,7 +1349,11 @@ const PowerManagement: React.FC = () => {
|
||||
onChange={value => {
|
||||
setOrderKeyword(value);
|
||||
}}
|
||||
onEnterPress={() => fetchOrders()}
|
||||
onEnterPress={() => {
|
||||
setOrderPage(1);
|
||||
setOrderHasMore(true);
|
||||
fetchOrders(1, false);
|
||||
}}
|
||||
className={style.searchInput}
|
||||
/>
|
||||
</div>
|
||||
@@ -1221,53 +1434,60 @@ const PowerManagement: React.FC = () => {
|
||||
<div className={style.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : orders.length > 0 ? (
|
||||
orders.map(order => {
|
||||
const statusInfo = getOrderStatusInfo(order.status);
|
||||
// tokens 已经是格式化好的字符串,如 "280,000"
|
||||
const tokens = order.tokens || "0";
|
||||
<>
|
||||
{orders.map(order => {
|
||||
const statusInfo = getOrderStatusInfo(order.status);
|
||||
// tokens 已经是格式化好的字符串,如 "280,000"
|
||||
const tokens = order.tokens || "0";
|
||||
|
||||
return (
|
||||
<Card key={order.id} className={style.orderCard}>
|
||||
<div className={style.orderHeader}>
|
||||
<div className={style.orderTitle}>
|
||||
{order.goodsName || "算力充值"}
|
||||
</div>
|
||||
<div className={style.orderAmount}>
|
||||
+{tokens}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.orderInfo}>
|
||||
<div className={style.orderLeft}>
|
||||
<div className={style.orderNumber}>
|
||||
订单号: {order.orderNo || "-"}
|
||||
return (
|
||||
<Card key={order.id} className={style.orderCard}>
|
||||
<div className={style.orderHeader}>
|
||||
<div className={style.orderTitle}>
|
||||
{order.goodsName || "算力充值"}
|
||||
</div>
|
||||
<div className={style.orderPayment}>
|
||||
{order.payType === 1 ? (
|
||||
<WechatOutlined className={style.paymentIcon} />
|
||||
) : order.payType === 2 ? (
|
||||
<AlipayCircleOutlined className={style.paymentIcon} />
|
||||
) : null}
|
||||
{order.payType ? getPayTypeText(order.payType) : order.payTypeText || "未支付"}
|
||||
<div className={style.orderAmount}>
|
||||
+{tokens}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.orderRight}>
|
||||
<div
|
||||
className={style.orderStatus}
|
||||
style={{
|
||||
color: statusInfo.color,
|
||||
backgroundColor: statusInfo.bgColor,
|
||||
}}
|
||||
>
|
||||
{order.statusText || statusInfo.text}
|
||||
<div className={style.orderInfo}>
|
||||
<div className={style.orderLeft}>
|
||||
<div className={style.orderNumber}>
|
||||
订单号: {order.orderNo || "-"}
|
||||
</div>
|
||||
<div className={style.orderPayment}>
|
||||
{order.payType === 1 ? (
|
||||
<WechatOutlined className={style.paymentIcon} />
|
||||
) : order.payType === 2 ? (
|
||||
<AlipayCircleOutlined className={style.paymentIcon} />
|
||||
) : null}
|
||||
{order.payType ? getPayTypeText(order.payType) : order.payTypeText || "未支付"}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.orderTime}>
|
||||
{formatDateTime(order.createTime)}
|
||||
<div className={style.orderRight}>
|
||||
<div
|
||||
className={style.orderStatus}
|
||||
style={{
|
||||
color: statusInfo.color,
|
||||
backgroundColor: statusInfo.bgColor,
|
||||
}}
|
||||
>
|
||||
{order.statusText || statusInfo.text}
|
||||
</div>
|
||||
<div className={style.orderTime}>
|
||||
{formatDateTime(order.createTime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<InfiniteScroll
|
||||
loadMore={loadMoreOrders}
|
||||
hasMore={orderHasMore}
|
||||
threshold={100}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className={style.emptyRecords}>
|
||||
<div className={style.emptyText}>暂无订单记录</div>
|
||||
@@ -1292,12 +1512,12 @@ const PowerManagement: React.FC = () => {
|
||||
<div className={style.formItem}>
|
||||
<label className={style.formLabel}>选择账号</label>
|
||||
<Picker
|
||||
columns={[accounts.map(acc => ({ label: getAccountDisplayName(acc), value: acc.id }))]}
|
||||
columns={[accounts.map(acc => ({ label: getAccountDisplayName(acc), value: acc.uid || acc.userId || acc.id }))]}
|
||||
visible={allocationAccountVisible}
|
||||
onClose={() => setAllocationAccountVisible(false)}
|
||||
value={allocationAccount ? [allocationAccount.id] : []}
|
||||
value={allocationAccount ? [allocationAccount.uid || allocationAccount.userId || allocationAccount.id] : []}
|
||||
onConfirm={value => {
|
||||
const selectedAccount = accounts.find(acc => acc.id === value[0]);
|
||||
const selectedAccount = accounts.find(acc => (acc.uid || acc.userId || acc.id) === value[0]);
|
||||
setAllocationAccount(selectedAccount || null);
|
||||
setAllocationAccountVisible(false);
|
||||
}}
|
||||
@@ -1353,6 +1573,10 @@ const PowerManagement: React.FC = () => {
|
||||
onConfirm={value => {
|
||||
setAllocationAccountFilter(value[0] as string);
|
||||
setAllocationAccountFilterVisible(false);
|
||||
// 重新加载记录
|
||||
setAllocationPage(1);
|
||||
setAllocationHasMore(true);
|
||||
fetchAllocationRecords(1, false);
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
@@ -1374,6 +1598,10 @@ const PowerManagement: React.FC = () => {
|
||||
onConfirm={value => {
|
||||
setAllocationTimeFilter(value[0] as string);
|
||||
setAllocationTimeFilterVisible(false);
|
||||
// 重新加载记录
|
||||
setAllocationPage(1);
|
||||
setAllocationHasMore(true);
|
||||
fetchAllocationRecords(1, false);
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
@@ -1391,25 +1619,41 @@ const PowerManagement: React.FC = () => {
|
||||
|
||||
{/* 记录列表 */}
|
||||
<div className={style.allocationRecordsList}>
|
||||
{allocationRecords.length > 0 ? (
|
||||
allocationRecords.map(record => (
|
||||
<div key={record.id} className={style.allocationRecordItem}>
|
||||
<div className={style.allocationRecordLeft}>
|
||||
<UserOutlined className={style.allocationRecordIcon} />
|
||||
<div className={style.allocationRecordInfo}>
|
||||
<div className={style.allocationRecordName}>
|
||||
{record.accountName}
|
||||
{loading && allocationRecords.length === 0 ? (
|
||||
<div className={style.loadingContainer}>
|
||||
<div className={style.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : allocationRecords.length > 0 ? (
|
||||
<>
|
||||
{allocationRecords.map(record => {
|
||||
// 从remarks中提取账号信息,格式可能是 "账号名 - 其他信息" 或直接是账号名
|
||||
const accountName = record.remarks || `账号${record.wechatAccountId || record.id}`;
|
||||
|
||||
return (
|
||||
<div key={record.id} className={style.allocationRecordItem}>
|
||||
<div className={style.allocationRecordLeft}>
|
||||
<UserOutlined className={style.allocationRecordIcon} />
|
||||
<div className={style.allocationRecordInfo}>
|
||||
<div className={style.allocationRecordName}>
|
||||
{accountName}
|
||||
</div>
|
||||
<div className={style.allocationRecordTime}>
|
||||
{formatDateTime(record.createTime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.allocationRecordTime}>
|
||||
{record.createTime}
|
||||
<div className={style.allocationRecordAmount}>
|
||||
+{formatNumber(Math.abs(record.tokens))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.allocationRecordAmount}>
|
||||
{record.amount}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})}
|
||||
<InfiniteScroll
|
||||
loadMore={loadMoreAllocationRecords}
|
||||
hasMore={allocationHasMore}
|
||||
threshold={100}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className={style.emptyRecords}>
|
||||
<div className={style.emptyText}>暂无分配记录</div>
|
||||
|
||||
@@ -49,6 +49,8 @@ export function transferWechatFriends(params: {
|
||||
wechatId: string;
|
||||
devices: number[];
|
||||
inherit: boolean;
|
||||
greeting?: string;
|
||||
firstMessage?: string;
|
||||
}) {
|
||||
return request("/v1/wechats/transfer-friends", params, "POST");
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ const WechatAccountDetail: React.FC = () => {
|
||||
const [showTransferConfirm, setShowTransferConfirm] = useState(false);
|
||||
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>([]);
|
||||
const [inheritInfo, setInheritInfo] = useState(true);
|
||||
const [greeting, setGreeting] = useState("");
|
||||
const [firstMessage, setFirstMessage] = useState("");
|
||||
const [transferLoading, setTransferLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
@@ -294,6 +296,10 @@ const WechatAccountDetail: React.FC = () => {
|
||||
const handleTransferFriends = () => {
|
||||
setSelectedDevices([]);
|
||||
setInheritInfo(true);
|
||||
// 设置默认打招呼内容,使用当前微信账号昵称
|
||||
const nickname = accountInfo?.nickname || "未知";
|
||||
setGreeting(`这个是${nickname}的新号,之前那个号没用了,重新加一下您`);
|
||||
setFirstMessage("");
|
||||
setShowTransferConfirm(true);
|
||||
};
|
||||
|
||||
@@ -321,7 +327,9 @@ const WechatAccountDetail: React.FC = () => {
|
||||
await transferWechatFriends({
|
||||
wechatId: id,
|
||||
devices: selectedDevices.map(device => device.id),
|
||||
inherit: inheritInfo
|
||||
inherit: inheritInfo,
|
||||
greeting: greeting.trim(),
|
||||
firstMessage: firstMessage.trim()
|
||||
});
|
||||
|
||||
Toast.show({
|
||||
@@ -330,6 +338,7 @@ const WechatAccountDetail: React.FC = () => {
|
||||
});
|
||||
setShowTransferConfirm(false);
|
||||
setSelectedDevices([]);
|
||||
setFirstMessage("");
|
||||
navigate("/scenarios");
|
||||
} catch (error) {
|
||||
console.error("好友转移失败:", error);
|
||||
@@ -1049,6 +1058,38 @@ const WechatAccountDetail: React.FC = () => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 打招呼 */}
|
||||
<div className={style["form-item"]}>
|
||||
<div className={style["form-label"]}>打招呼</div>
|
||||
<div className={style["form-control"]}>
|
||||
<Input.TextArea
|
||||
placeholder="请输入打招呼内容(可选)"
|
||||
value={greeting}
|
||||
onChange={(e) => setGreeting(e.target.value)}
|
||||
rows={3}
|
||||
maxLength={200}
|
||||
showCount
|
||||
style={{ resize: "none" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 通过后首次消息 */}
|
||||
<div className={style["form-item"]}>
|
||||
<div className={style["form-label"]}>好友通过后的首次消息</div>
|
||||
<div className={style["form-control"]}>
|
||||
<Input.TextArea
|
||||
placeholder="请输入好友通过验证后发送的首条消息(可选)"
|
||||
value={firstMessage}
|
||||
onChange={(e) => setFirstMessage(e.target.value)}
|
||||
rows={3}
|
||||
maxLength={200}
|
||||
showCount
|
||||
style={{ resize: "none" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style["popup-actions"]}>
|
||||
@@ -1068,6 +1109,10 @@ const WechatAccountDetail: React.FC = () => {
|
||||
onClick={() => {
|
||||
setShowTransferConfirm(false);
|
||||
setSelectedDevices([]);
|
||||
// 重置为默认打招呼内容
|
||||
const nickname = accountInfo?.nickname || "未知";
|
||||
setGreeting(`这个是${nickname}的新号,之前那个号没用了,重新加一下您`);
|
||||
setFirstMessage("");
|
||||
}}
|
||||
>
|
||||
取消
|
||||
|
||||
@@ -29,6 +29,7 @@ class AccountsController extends BaseController
|
||||
|
||||
$query = Db::table('s2_company_account')
|
||||
->alias('a')
|
||||
->join('users u', 'a.id = u.s2_accountId')
|
||||
->where([
|
||||
['a.departmentId', '=', $companyId],
|
||||
['a.status', '=', 0],
|
||||
@@ -48,6 +49,7 @@ class AccountsController extends BaseController
|
||||
$total = (clone $query)->count();
|
||||
$list = $query->field([
|
||||
'a.id',
|
||||
'u.id as uid',
|
||||
'a.userName',
|
||||
'a.realName',
|
||||
'a.nickname',
|
||||
|
||||
@@ -562,7 +562,7 @@ class AiChatController extends BaseController
|
||||
$data = [
|
||||
'tokens' => $tokenCount * 20,
|
||||
'type' => 0,
|
||||
'form' => 1,
|
||||
'form' => 13,
|
||||
'wechatAccountId' => $params['wechatAccountId'],
|
||||
'friendIdOrGroupId' => $params['friendId'],
|
||||
'remarks' => $remarks,
|
||||
@@ -816,7 +816,7 @@ class AiChatController extends BaseController
|
||||
$data = [
|
||||
'tokens' => $res['data']['token'],
|
||||
'type' => 0,
|
||||
'form' => 1,
|
||||
'form' => 13,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'friendIdOrGroupId' => $friendId,
|
||||
'remarks' => $remarks,
|
||||
|
||||
@@ -100,9 +100,6 @@ class TokensRecordController extends BaseController
|
||||
return ResponseHelper::error('类型参数错误,0为减少,1为增加');
|
||||
}
|
||||
|
||||
if (!in_array($form, [0, 1, 2, 3, 4, 5])) {
|
||||
return ResponseHelper::error('来源参数错误');
|
||||
}
|
||||
|
||||
// 重试机制,最多重试3次
|
||||
$maxRetries = 3;
|
||||
@@ -130,7 +127,7 @@ class TokensRecordController extends BaseController
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 使用悲观锁获取用户当前tokens余额,确保并发安全
|
||||
$userInfo = TokensCompany::where('companyId', $companyId)->lock(true)->find();
|
||||
$userInfo = TokensCompany::where(['companyId'=> $companyId,'userId' => $userId])->lock(true)->find();
|
||||
if (!$userInfo) {
|
||||
throw new \Exception('用户不存在');
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ class WechatChatroomController extends BaseController
|
||||
$data = [
|
||||
'tokens' => $res['data']['token'],
|
||||
'type' => 0,
|
||||
'form' => 3,
|
||||
'form' => 14,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'friendIdOrGroupId' => $groupId,
|
||||
'remarks' => $remarks,
|
||||
|
||||
@@ -51,7 +51,8 @@ class Attachment extends Controller
|
||||
'data' => [
|
||||
'id' => $existFile['id'],
|
||||
'name' => $existFile['name'],
|
||||
'url' => $existFile['source']
|
||||
'url' => $existFile['source'],
|
||||
'size' => isset($existFile['size']) ? $existFile['size'] : 0
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -97,7 +98,8 @@ class Attachment extends Controller
|
||||
'data' => [
|
||||
'id' => $attachment->id,
|
||||
'name' => $attachmentData['name'],
|
||||
'url' => $attachmentData['source']
|
||||
'url' => $attachmentData['source'],
|
||||
'size' => $attachmentData['size']
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use app\common\util\PaymentUtil;
|
||||
use think\facade\Env;
|
||||
use think\facade\Request;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\User;
|
||||
|
||||
/**
|
||||
* 支付服务(内部调用)
|
||||
@@ -495,6 +496,13 @@ class PaymentService
|
||||
switch ($order['orderType']) {
|
||||
case 1:
|
||||
// 处理购买算力
|
||||
// 查询用户信息,判断是否为管理员(需要同时匹配userId和companyId)
|
||||
$user = User::where([
|
||||
'id' => $order->userId,
|
||||
'companyId' => $order->companyId
|
||||
])->find();
|
||||
$isAdmin = (!empty($user) && isset($user->isAdmin) && $user->isAdmin == 1) ? 1 : 0;
|
||||
|
||||
$token = TokensCompany::where(['companyId' => $order->companyId,'userId' => $order->userId])->find();
|
||||
$goodsSpecs = json_decode($order->goodsSpecs, true);
|
||||
if (!empty($token)) {
|
||||
@@ -507,6 +515,7 @@ class PaymentService
|
||||
$tokensCompany->userId = $order->userId;
|
||||
$tokensCompany->companyId = $order->companyId;
|
||||
$tokensCompany->tokens = $goodsSpecs['tokens'];
|
||||
$tokensCompany->isAdmin = $isAdmin;
|
||||
$tokensCompany->createTime = time();
|
||||
$tokensCompany->updateTime = time();
|
||||
$tokensCompany->save();
|
||||
|
||||
@@ -130,6 +130,7 @@ Route::group('v1/', function () {
|
||||
Route::get('get-item-detail', 'app\cunkebao\controller\ContentLibraryController@getItemDetail'); // 获取内容库素材详情
|
||||
Route::post('update-item', 'app\cunkebao\controller\ContentLibraryController@updateItem'); // 更新内容库素材
|
||||
Route::any('aiEditContent', 'app\cunkebao\controller\ContentLibraryController@aiEditContent');
|
||||
Route::post('import-excel', 'app\cunkebao\controller\ContentLibraryController@importExcel'); // 导入Excel表格(支持图片)
|
||||
});
|
||||
|
||||
// 好友相关
|
||||
@@ -162,6 +163,7 @@ Route::group('v1/', function () {
|
||||
Route::get('queryOrder', 'app\cunkebao\controller\TokensController@queryOrder'); // 查询订单(扫码付款)
|
||||
Route::get('orderList', 'app\cunkebao\controller\TokensController@getOrderList'); // 获取订单列表
|
||||
Route::get('statistics', 'app\cunkebao\controller\TokensController@getTokensStatistics'); // 获取算力统计
|
||||
Route::post('allocate', 'app\cunkebao\controller\TokensController@allocateTokens'); // 分配token(仅管理员)
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class StatsController extends Controller
|
||||
$where = [
|
||||
['departmentId','=',$this->request->userInfo['companyId']]
|
||||
];
|
||||
if (!empty($this->request->userInfo['isAdmin'])){
|
||||
if (empty($this->request->userInfo['isAdmin'])){
|
||||
$where[] = ['id','=',$this->request->userInfo['s2_accountId']];
|
||||
}
|
||||
$accounts = Db::table('s2_company_account')->where($where)->column('id');
|
||||
@@ -407,7 +407,7 @@ class StatsController extends Controller
|
||||
$where = [
|
||||
['departmentId','=',$companyId]
|
||||
];
|
||||
if (!empty($this->request->userInfo['isAdmin'])){
|
||||
if (empty($this->request->userInfo['isAdmin'])){
|
||||
$where[] = ['id','=',$this->request->userInfo['s2_accountId']];
|
||||
}
|
||||
$accounts = Db::table('s2_company_account')->where($where)->column('id');
|
||||
|
||||
@@ -4,10 +4,12 @@ namespace app\cunkebao\controller;
|
||||
|
||||
use app\common\controller\PaymentService;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\User;
|
||||
use app\cunkebao\model\TokensPackage;
|
||||
use app\chukebao\model\TokensCompany;
|
||||
use app\chukebao\model\TokensRecord;
|
||||
use library\ResponseHelper;
|
||||
use think\Db;
|
||||
use think\facade\Env;
|
||||
|
||||
class TokensController extends BaseController
|
||||
@@ -149,6 +151,7 @@ class TokensController extends BaseController
|
||||
|
||||
// 构建查询条件
|
||||
$where = [
|
||||
['userId', '=', $userId],
|
||||
['companyId', '=', $companyId]
|
||||
];
|
||||
|
||||
@@ -257,13 +260,14 @@ class TokensController extends BaseController
|
||||
public function getTokensStatistics()
|
||||
{
|
||||
try {
|
||||
$userId = $this->getUserInfo('id');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
if (empty($companyId)) {
|
||||
return ResponseHelper::error('公司信息获取失败');
|
||||
}
|
||||
|
||||
// 获取公司算力余额
|
||||
$tokensCompany = TokensCompany::where('companyId', $companyId)->find();
|
||||
$tokensCompany = TokensCompany::where(['companyId' => $companyId,'userId' => $userId])->find();
|
||||
$remainingTokens = $tokensCompany ? intval($tokensCompany->tokens) : 0;
|
||||
|
||||
// 获取今日开始和结束时间戳
|
||||
@@ -276,6 +280,7 @@ class TokensController extends BaseController
|
||||
|
||||
// 统计今日消费(type=0表示消费)
|
||||
$todayUsed = TokensRecord::where([
|
||||
['userId', '=', $userId],
|
||||
['companyId', '=', $companyId],
|
||||
['type', '=', 0], // 0为减少(消费)
|
||||
['createTime', '>=', $todayStart],
|
||||
@@ -285,6 +290,7 @@ class TokensController extends BaseController
|
||||
|
||||
// 统计本月消费
|
||||
$monthUsed = TokensRecord::where([
|
||||
['userId', '=', $userId],
|
||||
['companyId', '=', $companyId],
|
||||
['type', '=', 0], // 0为减少(消费)
|
||||
['createTime', '>=', $monthStart],
|
||||
@@ -294,6 +300,7 @@ class TokensController extends BaseController
|
||||
|
||||
// 计算总算力(当前剩余 + 历史总消费)
|
||||
$totalConsumed = TokensRecord::where([
|
||||
['userId', '=', $userId],
|
||||
['companyId', '=', $companyId],
|
||||
['type', '=', 0]
|
||||
])->sum('tokens');
|
||||
@@ -301,13 +308,14 @@ class TokensController extends BaseController
|
||||
|
||||
// 总充值算力
|
||||
$totalRecharged = TokensRecord::where([
|
||||
['userId', '=', $userId],
|
||||
['companyId', '=', $companyId],
|
||||
['type', '=', 1] // 1为增加(充值)
|
||||
])->sum('tokens');
|
||||
$totalRecharged = intval($totalRecharged);
|
||||
|
||||
// 计算预计可用天数(基于过去一个月的平均消耗)
|
||||
$estimatedDays = $this->calculateEstimatedDays($companyId, $remainingTokens);
|
||||
$estimatedDays = $this->calculateEstimatedDays($userId,$companyId, $remainingTokens);
|
||||
|
||||
return ResponseHelper::success([
|
||||
'totalTokens' => $totalRecharged, // 总算力(累计充值)
|
||||
@@ -325,11 +333,12 @@ class TokensController extends BaseController
|
||||
|
||||
/**
|
||||
* 计算预计可用天数(基于过去一个月的平均消耗)
|
||||
* @param int $userId 用户ID
|
||||
* @param int $companyId 公司ID
|
||||
* @param int $remainingTokens 当前剩余算力
|
||||
* @return int 预计可用天数,-1表示无法计算(无消耗记录或余额为0)
|
||||
*/
|
||||
private function calculateEstimatedDays($companyId, $remainingTokens)
|
||||
private function calculateEstimatedDays($userId,$companyId, $remainingTokens)
|
||||
{
|
||||
// 如果余额为0或负数,无法计算
|
||||
if ($remainingTokens <= 0) {
|
||||
@@ -340,6 +349,7 @@ class TokensController extends BaseController
|
||||
$oneMonthAgo = time() - (30 * 24 * 60 * 60); // 30天前的时间戳
|
||||
|
||||
$totalConsumed = TokensRecord::where([
|
||||
['userId', '=', $userId],
|
||||
['companyId', '=', $companyId],
|
||||
['type', '=', 0], // 只统计减少的记录
|
||||
['createTime', '>=', $oneMonthAgo]
|
||||
@@ -365,4 +375,161 @@ class TokensController extends BaseController
|
||||
|
||||
return $estimatedDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配token(仅管理员可用)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function allocateTokens()
|
||||
{
|
||||
try {
|
||||
$userId = $this->getUserInfo('id');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
$targetUserId = (int)$this->request->param('targetUserId', 0);
|
||||
$tokens = (int)$this->request->param('tokens', 0);
|
||||
$remarks = $this->request->param('remarks', '');
|
||||
|
||||
// 验证参数
|
||||
if (empty($targetUserId)) {
|
||||
return ResponseHelper::error('目标用户ID不能为空');
|
||||
}
|
||||
|
||||
if ($tokens <= 0) {
|
||||
return ResponseHelper::error('分配的token数量必须大于0');
|
||||
}
|
||||
|
||||
if (empty($companyId)) {
|
||||
return ResponseHelper::error('公司信息获取失败');
|
||||
}
|
||||
|
||||
// 验证当前用户是否为管理员
|
||||
$currentUser = User::where([
|
||||
'id' => $userId,
|
||||
'companyId' => $companyId
|
||||
])->find();
|
||||
|
||||
if (empty($currentUser)) {
|
||||
return ResponseHelper::error('用户信息不存在');
|
||||
}
|
||||
|
||||
if (empty($currentUser->isAdmin) || $currentUser->isAdmin != 1) {
|
||||
return ResponseHelper::error('只有管理员才能分配token');
|
||||
}
|
||||
|
||||
// 验证目标用户是否存在且属于同一公司
|
||||
$targetUser = User::where([
|
||||
'id' => $targetUserId,
|
||||
'companyId' => $companyId
|
||||
])->find();
|
||||
|
||||
if (empty($targetUser)) {
|
||||
return ResponseHelper::error('目标用户不存在或不属于同一公司');
|
||||
}
|
||||
|
||||
// 检查分配者的token余额
|
||||
$allocatorTokens = TokensCompany::where([
|
||||
'companyId' => $companyId,
|
||||
'userId' => $userId
|
||||
])->find();
|
||||
|
||||
$allocatorBalance = $allocatorTokens ? intval($allocatorTokens->tokens) : 0;
|
||||
|
||||
if ($allocatorBalance < $tokens) {
|
||||
return ResponseHelper::error('token余额不足,当前余额:' . $allocatorBalance);
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
Db::startTrans();
|
||||
|
||||
try {
|
||||
// 1. 减少分配者的token
|
||||
if (!empty($allocatorTokens)) {
|
||||
$allocatorTokens->tokens = $allocatorBalance - $tokens;
|
||||
$allocatorTokens->updateTime = time();
|
||||
$allocatorTokens->save();
|
||||
$allocatorNewBalance = $allocatorTokens->tokens;
|
||||
} else {
|
||||
// 如果分配者没有记录,创建一条(余额为0)
|
||||
$allocatorTokens = new TokensCompany();
|
||||
$allocatorTokens->userId = $userId;
|
||||
$allocatorTokens->companyId = $companyId;
|
||||
$allocatorTokens->tokens = 0;
|
||||
$allocatorTokens->isAdmin = 1;
|
||||
$allocatorTokens->createTime = time();
|
||||
$allocatorTokens->updateTime = time();
|
||||
$allocatorTokens->save();
|
||||
$allocatorNewBalance = 0;
|
||||
}
|
||||
|
||||
// 2. 记录分配者的减少记录
|
||||
$targetUserAccount = $targetUser->account ?? $targetUser->phone ?? '用户ID[' . $targetUserId . ']';
|
||||
$allocatorRecord = new TokensRecord();
|
||||
$allocatorRecord->companyId = $companyId;
|
||||
$allocatorRecord->userId = $userId;
|
||||
$allocatorRecord->type = 0; // 0为减少
|
||||
$allocatorRecord->form = 1001; // 1001表示分配
|
||||
$allocatorRecord->wechatAccountId = 0;
|
||||
$allocatorRecord->friendIdOrGroupId = $targetUserId;
|
||||
$allocatorRecord->remarks = !empty($remarks) ? $remarks : '分配给' . $targetUserAccount;
|
||||
$allocatorRecord->tokens = $tokens;
|
||||
$allocatorRecord->balanceTokens = $allocatorNewBalance;
|
||||
$allocatorRecord->createTime = time();
|
||||
$allocatorRecord->save();
|
||||
|
||||
// 3. 增加接收者的token
|
||||
$receiverTokens = TokensCompany::where([
|
||||
'companyId' => $companyId,
|
||||
'userId' => $targetUserId
|
||||
])->find();
|
||||
|
||||
if (!empty($receiverTokens)) {
|
||||
$receiverTokens->tokens = intval($receiverTokens->tokens) + $tokens;
|
||||
$receiverTokens->updateTime = time();
|
||||
$receiverTokens->save();
|
||||
$receiverNewBalance = $receiverTokens->tokens;
|
||||
} else {
|
||||
// 如果接收者没有记录,创建一条
|
||||
$receiverTokens = new TokensCompany();
|
||||
$receiverTokens->userId = $targetUserId;
|
||||
$receiverTokens->companyId = $companyId;
|
||||
$receiverTokens->tokens = $tokens;
|
||||
$receiverTokens->isAdmin = (!empty($targetUser->isAdmin) && $targetUser->isAdmin == 1) ? 1 : 0;
|
||||
$receiverTokens->createTime = time();
|
||||
$receiverTokens->updateTime = time();
|
||||
$receiverTokens->save();
|
||||
$receiverNewBalance = $tokens;
|
||||
}
|
||||
|
||||
// 4. 记录接收者的增加记录
|
||||
$adminAccount = $currentUser->account ?? $currentUser->phone ?? '管理员';
|
||||
$receiverRecord = new TokensRecord();
|
||||
$receiverRecord->companyId = $companyId;
|
||||
$receiverRecord->userId = $targetUserId;
|
||||
$receiverRecord->type = 1; // 1为增加
|
||||
$receiverRecord->form = 1001; // 1001表示分配
|
||||
$receiverRecord->wechatAccountId = 0;
|
||||
$receiverRecord->friendIdOrGroupId = $userId;
|
||||
$receiverRecord->remarks = !empty($remarks) ? '管理员分配:' . $remarks : '管理员分配';
|
||||
$receiverRecord->tokens = $tokens;
|
||||
$receiverRecord->balanceTokens = $receiverNewBalance;
|
||||
$receiverRecord->createTime = time();
|
||||
$receiverRecord->save();
|
||||
|
||||
Db::commit();
|
||||
|
||||
return ResponseHelper::success([
|
||||
'allocatorBalance' => $allocatorNewBalance,
|
||||
'receiverBalance' => $receiverNewBalance,
|
||||
'allocatedTokens' => $tokens
|
||||
], '分配成功');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return ResponseHelper::error('分配失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('分配失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,12 @@ class PostTransferFriends extends BaseController
|
||||
{
|
||||
$wechatId = $this->request->param('wechatId', '');
|
||||
$inherit = $this->request->param('inherit', '');
|
||||
$greeting = $this->request->param('greeting', '');
|
||||
$firstMessage = $this->request->param('firstMessage', '');
|
||||
$devices = $this->request->param('devices', []);
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
|
||||
if (empty($wechatId)){
|
||||
return ResponseHelper::error('迁移的微信不能为空');
|
||||
}
|
||||
@@ -23,13 +26,16 @@ class PostTransferFriends extends BaseController
|
||||
if (empty($devices)){
|
||||
return ResponseHelper::error('迁移的设备不能为空');
|
||||
}
|
||||
if (empty($greeting)){
|
||||
return ResponseHelper::error('打招呼不能为空');
|
||||
}
|
||||
if (!is_array($devices)){
|
||||
return ResponseHelper::error('迁移的设备必须为数组');
|
||||
}
|
||||
|
||||
$wechat = Db::name('wechat_customer')->alias('wc')
|
||||
->join('wechat_account wa', 'wc.wechatId = wa.wechatId')
|
||||
->where(['wc.wechatId' => $wechatId, 'wc.companyId' => $companyId])
|
||||
->where(['wc.wechatId' => $wechatId])
|
||||
->field('wa.*')
|
||||
->find();
|
||||
|
||||
@@ -53,11 +59,6 @@ class PostTransferFriends extends BaseController
|
||||
'id' => 'poster-3',
|
||||
'name' => '点击咨询',
|
||||
'src' => 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif'
|
||||
],
|
||||
'$posters' => [
|
||||
'id' => 'poster-3',
|
||||
'name' => '点击咨询',
|
||||
'src' => 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif'
|
||||
]
|
||||
];
|
||||
$reqConf = [
|
||||
@@ -66,18 +67,38 @@ class PostTransferFriends extends BaseController
|
||||
'endTime' => '18:00',
|
||||
'remarkType' => 'phone',
|
||||
'addFriendInterval' => 60,
|
||||
'greeting' => '您好,我是'. $wechat['nickname'] .'的辅助客服,请通过'
|
||||
'greeting' => !empty($greeting) ? $greeting :'这个是'. $wechat['nickname'] .'的新号,之前那个号没用了,重新加一下您'
|
||||
];
|
||||
|
||||
if (!empty($firstMessage)){
|
||||
$msgConf = [
|
||||
[
|
||||
'day' => 0,
|
||||
'messages' => [
|
||||
[
|
||||
'id' => 1,
|
||||
'type' => 'text',
|
||||
'content' => $firstMessage,
|
||||
'intervalUnit' => 'seconds',
|
||||
'sendInterval' => 5,
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}else{
|
||||
$msgConf = [];
|
||||
}
|
||||
|
||||
// 使用容器获取控制器实例,而不是直接实例化
|
||||
$createAddFriendPlan = app('app\cunkebao\controller\plan\PostCreateAddFriendPlanV1Controller');
|
||||
|
||||
$taskId = Db::name('customer_acquisition_task')->insertGetId([
|
||||
'name' => '迁移好友('. $wechat['nickname'] .')',
|
||||
'sceneId' => 10,
|
||||
'sceneConf' => json_encode($sceneConf),
|
||||
'reqConf' => json_encode($reqConf),
|
||||
'sceneConf' => json_encode($sceneConf,256),
|
||||
'reqConf' => json_encode($reqConf,256),
|
||||
'tagConf' => json_encode([]),
|
||||
'msgConf' => json_encode($msgConf,256),
|
||||
'userId' => $this->getUserInfo('id'),
|
||||
'companyId' => $companyId,
|
||||
'status' => 0,
|
||||
|
||||
Reference in New Issue
Block a user