算力功能

This commit is contained in:
wong
2025-12-06 17:13:07 +08:00
parent f06eb0e9d7
commit a557319b3e
13 changed files with 3100 additions and 735 deletions

View File

@@ -121,13 +121,13 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
}
} else {
// 多选模式:原有的逻辑
if (tempSelectedOptions.some(v => v.id === device.id)) {
setTempSelectedOptions(
tempSelectedOptions.filter(v => v.id !== device.id),
);
} else {
const newSelectedOptions = [...tempSelectedOptions, device];
setTempSelectedOptions(newSelectedOptions);
if (tempSelectedOptions.some(v => v.id === device.id)) {
setTempSelectedOptions(
tempSelectedOptions.filter(v => v.id !== device.id),
);
} else {
const newSelectedOptions = [...tempSelectedOptions, device];
setTempSelectedOptions(newSelectedOptions);
}
}
};

View File

@@ -34,15 +34,15 @@ const PopupFooter: React.FC<PopupFooterProps> = ({
{/* 分页栏 */}
<div className={style.paginationRow}>
{onSelectAll && (
<div className={style.totalCount}>
<Checkbox
checked={isAllSelected}
onChange={e => onSelectAll(e.target.checked)}
className={style.selectAllCheckbox}
>
</Checkbox>
</div>
<div className={style.totalCount}>
<Checkbox
checked={isAllSelected}
onChange={e => onSelectAll(e.target.checked)}
className={style.selectAllCheckbox}
>
</Checkbox>
</div>
)}
<div className={style.paginationControls}>
<Button

View File

@@ -106,7 +106,7 @@ export default function ContentForm() {
setUseAI(data.aiEnabled === 1);
} else {
// 兼容旧数据,默认根据是否有 aiPrompt 判断
setUseAI(!!data.aiPrompt);
setUseAI(!!data.aiPrompt);
}
setEnabled(data.status === 1);
// 时间范围
@@ -151,7 +151,7 @@ export default function ContentForm() {
.map(s => s.trim())
.filter(Boolean),
catchType,
aiPrompt,
aiPrompt,
aiEnabled: useAI ? 1 : 0,
timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0,
startTime: dateRange[0] ? formatDate(dateRange[0]) : "",
@@ -347,37 +347,37 @@ export default function ContentForm() {
<div className={style["form-card"]}>
<Collapse className={style["keyword-collapse"]}>
<Collapse.Panel
key="keywords"
<Collapse.Panel
key="keywords"
title={
<div className={style["keyword-header"]}>
<span className={style["keyword-title"]}></span>
<DownOutlined className={style["keyword-arrow"]} />
</div>
}
>
<div className={style["form-section"]}>
<label className={style["form-label"]}></label>
<TextArea
placeholder="多个关键词用逗号分隔"
value={keywordsInclude}
onChange={e => setKeywordsInclude(e.target.value)}
className={style["input"]}
autoSize={{ minRows: 2, maxRows: 4 }}
/>
</div>
<div className={style["form-section"]}>
<label className={style["form-label"]}></label>
<TextArea
placeholder="多个关键词用逗号分隔"
value={keywordsExclude}
onChange={e => setKeywordsExclude(e.target.value)}
className={style["input"]}
autoSize={{ minRows: 2, maxRows: 4 }}
/>
</div>
</Collapse.Panel>
</Collapse>
>
<div className={style["form-section"]}>
<label className={style["form-label"]}></label>
<TextArea
placeholder="多个关键词用逗号分隔"
value={keywordsInclude}
onChange={e => setKeywordsInclude(e.target.value)}
className={style["input"]}
autoSize={{ minRows: 2, maxRows: 4 }}
/>
</div>
<div className={style["form-section"]}>
<label className={style["form-label"]}></label>
<TextArea
placeholder="多个关键词用逗号分隔"
value={keywordsExclude}
onChange={e => setKeywordsExclude(e.target.value)}
className={style["input"]}
autoSize={{ minRows: 2, maxRows: 4 }}
/>
</div>
</Collapse.Panel>
</Collapse>
</div>
{/* 采集内容类型 */}
@@ -400,38 +400,38 @@ export default function ContentForm() {
);
}}
>
{type === "text"
? "文本"
: type === "image"
? "图片"
: "视频"}
{type === "text"
? "文本"
: type === "image"
? "图片"
: "视频"}
</button>
))}
</div>
</div>
<div className={style["form-card"]}>
<div
className={style["form-section"]}
style={{ display: "flex", alignItems: "center", gap: 12 }}
>
<Switch checked={useAI} onChange={setUseAI} />
<span className={style["ai-desc"]}>
<div
className={style["form-section"]}
style={{ display: "flex", alignItems: "center", gap: 12 }}
>
<Switch checked={useAI} onChange={setUseAI} />
<span className={style["ai-desc"]}>
,AI生成
</span>
</div>
{useAI && (
<div className={style["form-section"]}>
<label className={style["form-label"]}>AI提示词</label>
<TextArea
</span>
</div>
{useAI && (
<div className={style["form-section"]}>
<label className={style["form-label"]}>AI提示词</label>
<TextArea
placeholder="请输入AI提示词"
value={aiPrompt}
onChange={e => setAIPrompt(e.target.value)}
className={style["input"]}
autoSize={{ minRows: 4, maxRows: 10 }}
/>
</div>
)}
</div>
)}
</div>
<div className={style["form-card"]}>
@@ -441,51 +441,51 @@ export default function ContentForm() {
<div className={style["date-inputs"]}>
<div className={style["date-item"]}>
<label className={style["date-label"]}></label>
<AntdInput
readOnly
<AntdInput
readOnly
value={
dateRange[0]
? `${dateRange[0].getFullYear()}/${String(dateRange[0].getMonth() + 1).padStart(2, "0")}/${String(dateRange[0].getDate()).padStart(2, "0")}`
: ""
}
placeholder="年/月/日"
placeholder="年/月/日"
className={style["date-input"]}
onClick={() => setShowStartPicker(true)}
/>
<DatePicker
visible={showStartPicker}
title="开始时间"
value={dateRange[0]}
onClose={() => setShowStartPicker(false)}
onConfirm={val => {
setDateRange([val, dateRange[1]]);
setShowStartPicker(false);
}}
/>
</div>
onClick={() => setShowStartPicker(true)}
/>
<DatePicker
visible={showStartPicker}
title="开始时间"
value={dateRange[0]}
onClose={() => setShowStartPicker(false)}
onConfirm={val => {
setDateRange([val, dateRange[1]]);
setShowStartPicker(false);
}}
/>
</div>
<div className={style["date-item"]}>
<label className={style["date-label"]}></label>
<AntdInput
readOnly
<AntdInput
readOnly
value={
dateRange[1]
? `${dateRange[1].getFullYear()}/${String(dateRange[1].getMonth() + 1).padStart(2, "0")}/${String(dateRange[1].getDate()).padStart(2, "0")}`
: ""
}
placeholder="年/月/日"
placeholder="年/月/日"
className={style["date-input"]}
onClick={() => setShowEndPicker(true)}
/>
<DatePicker
visible={showEndPicker}
title="结束时间"
value={dateRange[1]}
onClose={() => setShowEndPicker(false)}
onConfirm={val => {
setDateRange([dateRange[0], val]);
setShowEndPicker(false);
}}
/>
onClick={() => setShowEndPicker(true)}
/>
<DatePicker
visible={showEndPicker}
title="结束时间"
value={dateRange[1]}
onClose={() => setShowEndPicker(false)}
onConfirm={val => {
setDateRange([dateRange[0], val]);
setShowEndPicker(false);
}}
/>
</div>
</div>
</div>
@@ -493,7 +493,7 @@ export default function ContentForm() {
<div className={style["form-card"]}>
<div className={style["enable-section"]}>
<span className={style["enable-label"]}></span>
<Switch checked={enabled} onChange={setEnabled} />
<Switch checked={enabled} onChange={setEnabled} />
</div>
</div>
</form>

View File

@@ -4,7 +4,7 @@ import request from "@/api/request";
export interface PowerPackage {
id: number;
name: string;
tokens: number; // 算力点数
tokens: number | string; // 算力点数(可能是字符串,如"2,800"
price: number; // 价格(分)
originalPrice: number; // 原价(分)
unitPrice: number; // 单价
@@ -13,7 +13,7 @@ export interface PowerPackage {
isRecommend: number; // 是否推荐
isHot: number; // 是否热门
isVip: number; // 是否VIP
features: string[]; // 功能特性
features?: string[]; // 功能特性(可选)
description: string[]; // 描述关键词
status: number;
createTime: string;

View File

@@ -6,6 +6,9 @@ export interface Statistics {
monthUsed: number; // 本月使用
remainingTokens: number; // 剩余算力
totalConsumed: number; // 总消耗
yesterdayUsed?: number; // 昨日消耗
historyConsumed?: number; // 历史消耗
estimatedDays?: number; // 预计可用天数
}
// 算力统计接口
export function getStatistics(): Promise<Statistics> {
@@ -143,3 +146,42 @@ export function buyPackage(params: { id: number; price: number }) {
export function buyCustomPower(params: { amount: number }) {
return request("/v1/power/buy-custom", params, "POST");
}
// 查询订单状态
export interface QueryOrderResponse {
id: number;
mchId: number;
companyId: number;
userId: number;
orderType: number;
status: number; // 0: 待支付, 1: 已支付
goodsId: number;
goodsName: string;
goodsSpecs: string;
money: number;
orderNo: string;
payType: number | null;
payTime: number | null;
payInfo: any;
createTime: number;
}
export function queryOrder(orderNo: string): Promise<QueryOrderResponse> {
return request("/v1/tokens/queryOrder", { orderNo }, "GET");
}
// 账号信息
export interface Account {
id: number;
userName: string;
realName: string;
nickname: string;
departmentId: number;
departmentName: string;
avatar: string;
}
// 获取账号列表
export function getAccountList(): Promise<{ list: Account[]; total: number }> {
return request("/v1/kefu/accounts/list", undefined, "GET");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,7 @@ export interface TokensUseRecordItem {
export interface TokensUseRecordList {
list: TokensUseRecordItem[];
total?: number;
}
//算力使用明细

View File

@@ -339,8 +339,8 @@
height: 5px;
border-radius: 10px 10px 0 0;
background: #1677ff;
}
}
}
.stat-icon-chat {
width: 20px;
@@ -700,7 +700,7 @@
.adm-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
border-radius: 50%;
}
}
@@ -710,13 +710,13 @@
}
.friend-name-row {
display: flex;
align-items: center;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
margin-bottom: 4px;
}
.friend-name {
.friend-name {
font-size: 15px;
font-weight: 600;
color: #111;
@@ -727,7 +727,7 @@
display: flex;
flex-wrap: wrap;
gap: 4px;
}
}
.friend-tag {
font-size: 11px;
@@ -735,17 +735,17 @@
border-radius: 999px;
background: #f5f5f5;
color: #666;
}
}
.friend-id-row {
font-size: 12px;
font-size: 12px;
color: #999;
margin-bottom: 6px;
}
}
.friend-status-row {
display: flex;
flex-wrap: wrap;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
@@ -764,7 +764,7 @@
font-size: 11px;
color: #999;
margin-bottom: 4px;
}
}
.value-amount {
font-size: 14px;

View File

@@ -370,13 +370,13 @@ const ScenarioList: React.FC = () => {
title={scenarioName || ""}
right={
scenarioId !== "10" ? (
<Button
size="small"
color="primary"
onClick={handleCreateNewPlan}
>
<PlusOutlined />
</Button>
<Button
size="small"
color="primary"
onClick={handleCreateNewPlan}
>
<PlusOutlined />
</Button>
) : null
}
/>
@@ -427,13 +427,13 @@ const ScenarioList: React.FC = () => {
{searchTerm ? "没有找到匹配的计划" : "暂无计划"}
</div>
{scenarioId !== "10" && (
<Button
color="primary"
onClick={handleCreateNewPlan}
className={style["create-first-btn"]}
>
<PlusOutlined />
</Button>
<Button
color="primary"
onClick={handleCreateNewPlan}
className={style["create-first-btn"]}
>
<PlusOutlined />
</Button>
)}
</div>
) : (

View File

@@ -16,6 +16,8 @@ class TokensRecordController extends BaseController
$limit = $this->request->param('limit', 10);
$type = $this->request->param('type', '');
$form = $this->request->param('form', '');
$startTime = $this->request->param('startTime', '');
$endTime = $this->request->param('endTime', '');
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
@@ -32,6 +34,26 @@ class TokensRecordController extends BaseController
$where[] = ['form','=',$form];
}
// 时间筛选
if (!empty($startTime)) {
// 支持时间戳或日期字符串格式
$startTimestamp = is_numeric($startTime) ? intval($startTime) : strtotime($startTime);
if ($startTimestamp !== false) {
$where[] = ['createTime', '>=', $startTimestamp];
}
}
if (!empty($endTime)) {
// 支持时间戳或日期字符串格式
$endTimestamp = is_numeric($endTime) ? intval($endTime) : strtotime($endTime);
if ($endTimestamp !== false) {
// 如果是日期字符串自动设置为当天的23:59:59
if (!is_numeric($endTime)) {
$endTimestamp = strtotime(date('Y-m-d 23:59:59', $endTimestamp));
}
$where[] = ['createTime', '<=', $endTimestamp];
}
}
$query = TokensRecord::where($where);
$total = $query->count();

View File

@@ -495,7 +495,7 @@ class PaymentService
switch ($order['orderType']) {
case 1:
// 处理购买算力
$token = TokensCompany::where(['companyId' => $order->companyId])->find();
$token = TokensCompany::where(['companyId' => $order->companyId,'userId' => $order->userId])->find();
$goodsSpecs = json_decode($order->goodsSpecs, true);
if (!empty($token)) {
$token->tokens = $token->tokens + $goodsSpecs['tokens'];
@@ -504,6 +504,7 @@ class PaymentService
$newTokens = $token->tokens;
} else {
$tokensCompany = new TokensCompany();
$tokensCompany->userId = $order->userId;
$tokensCompany->companyId = $order->companyId;
$tokensCompany->tokens = $goodsSpecs['tokens'];
$tokensCompany->createTime = time();

View File

@@ -72,7 +72,7 @@ class TokensController extends BaseController
} else {
//获取配置的tokens比例
$tokens_multiple = Env::get('payment.tokens_multiple', 28);
$tokens_multiple = Env::get('payment.tokens_multiple', 20);
$specs = [
'id' => 0,
'name' => '自定义购买算力',
@@ -119,7 +119,7 @@ class TokensController extends BaseController
return ResponseHelper::success($order, '订单已支付');
} else {
$errorMsg = !empty($order['payInfo']) ? $order['payInfo'] : '订单未支付';
return ResponseHelper::success($order,$errorMsg,400);
return ResponseHelper::success($order,$errorMsg);
}
} else {
return ResponseHelper::success($order, '订单已支付');
@@ -140,6 +140,7 @@ class TokensController extends BaseController
$status = $this->request->param('status', ''); // 订单状态筛选
$keyword = $this->request->param('keyword', ''); // 关键词搜索(订单号)
$orderType = $this->request->param('orderType', ''); // 订单类型筛选
$payType = $this->request->param('payType', ''); // 支付类型筛选
$startTime = $this->request->param('startTime', ''); // 开始时间
$endTime = $this->request->param('endTime', ''); // 结束时间
@@ -166,6 +167,11 @@ class TokensController extends BaseController
$where[] = ['orderType', '=', $orderType];
}
// 支付类型筛选
if($payType !== '') {
$where[] = ['payType', '=', $payType];
}
// 时间范围筛选
if (!empty($startTime)) {
$where[] = ['createTime', '>=', strtotime($startTime)];
@@ -300,16 +306,63 @@ class TokensController extends BaseController
])->sum('tokens');
$totalRecharged = intval($totalRecharged);
// 计算预计可用天数(基于过去一个月的平均消耗)
$estimatedDays = $this->calculateEstimatedDays($companyId, $remainingTokens);
return ResponseHelper::success([
'totalTokens' => $totalRecharged, // 总算力(累计充值)
'todayUsed' => $todayUsed, // 今日使用
'monthUsed' => $monthUsed, // 本月使用
'remainingTokens' => $remainingTokens, // 剩余算力
'totalConsumed' => $totalConsumed, // 累计消费
'estimatedDays' => $estimatedDays, // 预计可用天数
], '获取成功');
} catch (\Exception $e) {
return ResponseHelper::error('获取算力统计失败:' . $e->getMessage());
}
}
/**
* 计算预计可用天数(基于过去一个月的平均消耗)
* @param int $companyId 公司ID
* @param int $remainingTokens 当前剩余算力
* @return int 预计可用天数,-1表示无法计算无消耗记录或余额为0
*/
private function calculateEstimatedDays($companyId, $remainingTokens)
{
// 如果余额为0或负数无法计算
if ($remainingTokens <= 0) {
return -1;
}
// 计算过去30天的消耗总量只统计减少的记录type=0
$oneMonthAgo = time() - (30 * 24 * 60 * 60); // 30天前的时间戳
$totalConsumed = TokensRecord::where([
['companyId', '=', $companyId],
['type', '=', 0], // 只统计减少的记录
['createTime', '>=', $oneMonthAgo]
])->sum('tokens');
$totalConsumed = intval($totalConsumed);
// 如果过去30天没有消耗记录无法计算
if ($totalConsumed <= 0) {
return -1;
}
// 计算平均每天消耗量
$avgDailyConsumption = $totalConsumed / 30;
// 如果平均每天消耗为0无法计算
if ($avgDailyConsumption <= 0) {
return -1;
}
// 计算预计可用天数 = 当前余额 / 平均每天消耗量
$estimatedDays = floor($remainingTokens / $avgDailyConsumption);
return $estimatedDays;
}
}