算力功能改版

This commit is contained in:
wong
2025-12-09 15:01:38 +08:00
parent c9a5d3091f
commit 3e145ca123
18 changed files with 763 additions and 203 deletions

View File

@@ -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"]}>

View File

@@ -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");

View File

@@ -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>

View File

@@ -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");
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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");
}

View File

@@ -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("");
}}
>

View File

@@ -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',

View File

@@ -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,

View File

@@ -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('用户不存在');
}

View File

@@ -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,

View File

@@ -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']
]
]);

View File

@@ -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();

View File

@@ -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仅管理员
});

View File

@@ -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');

View File

@@ -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());
}
}
}

View File

@@ -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,