Merge branch 'feature/groupCreate' into develop
This commit is contained in:
@@ -121,13 +121,13 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 多选模式:原有的逻辑
|
// 多选模式:原有的逻辑
|
||||||
if (tempSelectedOptions.some(v => v.id === device.id)) {
|
if (tempSelectedOptions.some(v => v.id === device.id)) {
|
||||||
setTempSelectedOptions(
|
setTempSelectedOptions(
|
||||||
tempSelectedOptions.filter(v => v.id !== device.id),
|
tempSelectedOptions.filter(v => v.id !== device.id),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const newSelectedOptions = [...tempSelectedOptions, device];
|
const newSelectedOptions = [...tempSelectedOptions, device];
|
||||||
setTempSelectedOptions(newSelectedOptions);
|
setTempSelectedOptions(newSelectedOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,15 +34,15 @@ const PopupFooter: React.FC<PopupFooterProps> = ({
|
|||||||
{/* 分页栏 */}
|
{/* 分页栏 */}
|
||||||
<div className={style.paginationRow}>
|
<div className={style.paginationRow}>
|
||||||
{onSelectAll && (
|
{onSelectAll && (
|
||||||
<div className={style.totalCount}>
|
<div className={style.totalCount}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isAllSelected}
|
checked={isAllSelected}
|
||||||
onChange={e => onSelectAll(e.target.checked)}
|
onChange={e => onSelectAll(e.target.checked)}
|
||||||
className={style.selectAllCheckbox}
|
className={style.selectAllCheckbox}
|
||||||
>
|
>
|
||||||
全选当前页
|
全选当前页
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={style.paginationControls}>
|
<div className={style.paginationControls}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function ContentForm() {
|
|||||||
setUseAI(data.aiEnabled === 1);
|
setUseAI(data.aiEnabled === 1);
|
||||||
} else {
|
} else {
|
||||||
// 兼容旧数据,默认根据是否有 aiPrompt 判断
|
// 兼容旧数据,默认根据是否有 aiPrompt 判断
|
||||||
setUseAI(!!data.aiPrompt);
|
setUseAI(!!data.aiPrompt);
|
||||||
}
|
}
|
||||||
setEnabled(data.status === 1);
|
setEnabled(data.status === 1);
|
||||||
// 时间范围
|
// 时间范围
|
||||||
@@ -151,7 +151,7 @@ export default function ContentForm() {
|
|||||||
.map(s => s.trim())
|
.map(s => s.trim())
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
catchType,
|
catchType,
|
||||||
aiPrompt,
|
aiPrompt,
|
||||||
aiEnabled: useAI ? 1 : 0,
|
aiEnabled: useAI ? 1 : 0,
|
||||||
timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0,
|
timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0,
|
||||||
startTime: dateRange[0] ? formatDate(dateRange[0]) : "",
|
startTime: dateRange[0] ? formatDate(dateRange[0]) : "",
|
||||||
@@ -347,37 +347,37 @@ export default function ContentForm() {
|
|||||||
|
|
||||||
<div className={style["form-card"]}>
|
<div className={style["form-card"]}>
|
||||||
<Collapse className={style["keyword-collapse"]}>
|
<Collapse className={style["keyword-collapse"]}>
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
key="keywords"
|
key="keywords"
|
||||||
title={
|
title={
|
||||||
<div className={style["keyword-header"]}>
|
<div className={style["keyword-header"]}>
|
||||||
<span className={style["keyword-title"]}>关键词设置</span>
|
<span className={style["keyword-title"]}>关键词设置</span>
|
||||||
<DownOutlined className={style["keyword-arrow"]} />
|
<DownOutlined className={style["keyword-arrow"]} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={style["form-section"]}>
|
<div className={style["form-section"]}>
|
||||||
<label className={style["form-label"]}>包含关键词</label>
|
<label className={style["form-label"]}>包含关键词</label>
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder="多个关键词用逗号分隔"
|
placeholder="多个关键词用逗号分隔"
|
||||||
value={keywordsInclude}
|
value={keywordsInclude}
|
||||||
onChange={e => setKeywordsInclude(e.target.value)}
|
onChange={e => setKeywordsInclude(e.target.value)}
|
||||||
className={style["input"]}
|
className={style["input"]}
|
||||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["form-section"]}>
|
<div className={style["form-section"]}>
|
||||||
<label className={style["form-label"]}>排除关键词</label>
|
<label className={style["form-label"]}>排除关键词</label>
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder="多个关键词用逗号分隔"
|
placeholder="多个关键词用逗号分隔"
|
||||||
value={keywordsExclude}
|
value={keywordsExclude}
|
||||||
onChange={e => setKeywordsExclude(e.target.value)}
|
onChange={e => setKeywordsExclude(e.target.value)}
|
||||||
className={style["input"]}
|
className={style["input"]}
|
||||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 采集内容类型 */}
|
{/* 采集内容类型 */}
|
||||||
@@ -400,38 +400,38 @@ export default function ContentForm() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{type === "text"
|
{type === "text"
|
||||||
? "文本"
|
? "文本"
|
||||||
: type === "image"
|
: type === "image"
|
||||||
? "图片"
|
? "图片"
|
||||||
: "视频"}
|
: "视频"}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={style["form-card"]}>
|
<div className={style["form-card"]}>
|
||||||
<div
|
<div
|
||||||
className={style["form-section"]}
|
className={style["form-section"]}
|
||||||
style={{ display: "flex", alignItems: "center", gap: 12 }}
|
style={{ display: "flex", alignItems: "center", gap: 12 }}
|
||||||
>
|
>
|
||||||
<Switch checked={useAI} onChange={setUseAI} />
|
<Switch checked={useAI} onChange={setUseAI} />
|
||||||
<span className={style["ai-desc"]}>
|
<span className={style["ai-desc"]}>
|
||||||
启用后,该内容库下的内容会通过AI生成
|
启用后,该内容库下的内容会通过AI生成
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{useAI && (
|
{useAI && (
|
||||||
<div className={style["form-section"]}>
|
<div className={style["form-section"]}>
|
||||||
<label className={style["form-label"]}>AI提示词</label>
|
<label className={style["form-label"]}>AI提示词</label>
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder="请输入AI提示词"
|
placeholder="请输入AI提示词"
|
||||||
value={aiPrompt}
|
value={aiPrompt}
|
||||||
onChange={e => setAIPrompt(e.target.value)}
|
onChange={e => setAIPrompt(e.target.value)}
|
||||||
className={style["input"]}
|
className={style["input"]}
|
||||||
autoSize={{ minRows: 4, maxRows: 10 }}
|
autoSize={{ minRows: 4, maxRows: 10 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={style["form-card"]}>
|
<div className={style["form-card"]}>
|
||||||
@@ -441,51 +441,51 @@ export default function ContentForm() {
|
|||||||
<div className={style["date-inputs"]}>
|
<div className={style["date-inputs"]}>
|
||||||
<div className={style["date-item"]}>
|
<div className={style["date-item"]}>
|
||||||
<label className={style["date-label"]}>开始时间</label>
|
<label className={style["date-label"]}>开始时间</label>
|
||||||
<AntdInput
|
<AntdInput
|
||||||
readOnly
|
readOnly
|
||||||
value={
|
value={
|
||||||
dateRange[0]
|
dateRange[0]
|
||||||
? `${dateRange[0].getFullYear()}/${String(dateRange[0].getMonth() + 1).padStart(2, "0")}/${String(dateRange[0].getDate()).padStart(2, "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"]}
|
className={style["date-input"]}
|
||||||
onClick={() => setShowStartPicker(true)}
|
onClick={() => setShowStartPicker(true)}
|
||||||
/>
|
/>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
visible={showStartPicker}
|
visible={showStartPicker}
|
||||||
title="开始时间"
|
title="开始时间"
|
||||||
value={dateRange[0]}
|
value={dateRange[0]}
|
||||||
onClose={() => setShowStartPicker(false)}
|
onClose={() => setShowStartPicker(false)}
|
||||||
onConfirm={val => {
|
onConfirm={val => {
|
||||||
setDateRange([val, dateRange[1]]);
|
setDateRange([val, dateRange[1]]);
|
||||||
setShowStartPicker(false);
|
setShowStartPicker(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["date-item"]}>
|
<div className={style["date-item"]}>
|
||||||
<label className={style["date-label"]}>结束时间</label>
|
<label className={style["date-label"]}>结束时间</label>
|
||||||
<AntdInput
|
<AntdInput
|
||||||
readOnly
|
readOnly
|
||||||
value={
|
value={
|
||||||
dateRange[1]
|
dateRange[1]
|
||||||
? `${dateRange[1].getFullYear()}/${String(dateRange[1].getMonth() + 1).padStart(2, "0")}/${String(dateRange[1].getDate()).padStart(2, "0")}`
|
? `${dateRange[1].getFullYear()}/${String(dateRange[1].getMonth() + 1).padStart(2, "0")}/${String(dateRange[1].getDate()).padStart(2, "0")}`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
placeholder="年/月/日"
|
placeholder="年/月/日"
|
||||||
className={style["date-input"]}
|
className={style["date-input"]}
|
||||||
onClick={() => setShowEndPicker(true)}
|
onClick={() => setShowEndPicker(true)}
|
||||||
/>
|
/>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
visible={showEndPicker}
|
visible={showEndPicker}
|
||||||
title="结束时间"
|
title="结束时间"
|
||||||
value={dateRange[1]}
|
value={dateRange[1]}
|
||||||
onClose={() => setShowEndPicker(false)}
|
onClose={() => setShowEndPicker(false)}
|
||||||
onConfirm={val => {
|
onConfirm={val => {
|
||||||
setDateRange([dateRange[0], val]);
|
setDateRange([dateRange[0], val]);
|
||||||
setShowEndPicker(false);
|
setShowEndPicker(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -493,7 +493,7 @@ export default function ContentForm() {
|
|||||||
<div className={style["form-card"]}>
|
<div className={style["form-card"]}>
|
||||||
<div className={style["enable-section"]}>
|
<div className={style["enable-section"]}>
|
||||||
<span className={style["enable-label"]}>是否启用</span>
|
<span className={style["enable-label"]}>是否启用</span>
|
||||||
<Switch checked={enabled} onChange={setEnabled} />
|
<Switch checked={enabled} onChange={setEnabled} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import request from "@/api/request";
|
|||||||
export interface PowerPackage {
|
export interface PowerPackage {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
tokens: number; // 算力点数
|
tokens: number | string; // 算力点数(可能是字符串,如"2,800")
|
||||||
price: number; // 价格(分)
|
price: number; // 价格(分)
|
||||||
originalPrice: number; // 原价(分)
|
originalPrice: number; // 原价(分)
|
||||||
unitPrice: number; // 单价
|
unitPrice: number; // 单价
|
||||||
@@ -13,7 +13,7 @@ export interface PowerPackage {
|
|||||||
isRecommend: number; // 是否推荐
|
isRecommend: number; // 是否推荐
|
||||||
isHot: number; // 是否热门
|
isHot: number; // 是否热门
|
||||||
isVip: number; // 是否VIP
|
isVip: number; // 是否VIP
|
||||||
features: string[]; // 功能特性
|
features?: string[]; // 功能特性(可选)
|
||||||
description: string[]; // 描述关键词
|
description: string[]; // 描述关键词
|
||||||
status: number;
|
status: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ export interface Statistics {
|
|||||||
monthUsed: number; // 本月使用
|
monthUsed: number; // 本月使用
|
||||||
remainingTokens: number; // 剩余算力
|
remainingTokens: number; // 剩余算力
|
||||||
totalConsumed: number; // 总消耗
|
totalConsumed: number; // 总消耗
|
||||||
|
yesterdayUsed?: number; // 昨日消耗
|
||||||
|
historyConsumed?: number; // 历史消耗
|
||||||
|
estimatedDays?: number; // 预计可用天数
|
||||||
}
|
}
|
||||||
// 算力统计接口
|
// 算力统计接口
|
||||||
export function getStatistics(): Promise<Statistics> {
|
export function getStatistics(): Promise<Statistics> {
|
||||||
@@ -143,3 +146,42 @@ export function buyPackage(params: { id: number; price: number }) {
|
|||||||
export function buyCustomPower(params: { amount: number }) {
|
export function buyCustomPower(params: { amount: number }) {
|
||||||
return request("/v1/power/buy-custom", params, "POST");
|
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
@@ -36,6 +36,7 @@ export interface TokensUseRecordItem {
|
|||||||
|
|
||||||
export interface TokensUseRecordList {
|
export interface TokensUseRecordList {
|
||||||
list: TokensUseRecordItem[];
|
list: TokensUseRecordItem[];
|
||||||
|
total?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
//算力使用明细
|
//算力使用明细
|
||||||
|
|||||||
@@ -339,8 +339,8 @@
|
|||||||
height: 5px;
|
height: 5px;
|
||||||
border-radius: 10px 10px 0 0;
|
border-radius: 10px 10px 0 0;
|
||||||
background: #1677ff;
|
background: #1677ff;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.stat-icon-chat {
|
.stat-icon-chat {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
@@ -700,7 +700,7 @@
|
|||||||
.adm-avatar {
|
.adm-avatar {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,13 +710,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.friend-name-row {
|
.friend-name-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.friend-name {
|
.friend-name {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #111;
|
color: #111;
|
||||||
@@ -727,7 +727,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.friend-tag {
|
.friend-tag {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -735,17 +735,17 @@
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.friend-id-row {
|
.friend-id-row {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999;
|
color: #999;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.friend-status-row {
|
.friend-status-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,7 +764,7 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #999;
|
color: #999;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-amount {
|
.value-amount {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@@ -370,13 +370,13 @@ const ScenarioList: React.FC = () => {
|
|||||||
title={scenarioName || ""}
|
title={scenarioName || ""}
|
||||||
right={
|
right={
|
||||||
scenarioId !== "10" ? (
|
scenarioId !== "10" ? (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleCreateNewPlan}
|
onClick={handleCreateNewPlan}
|
||||||
>
|
>
|
||||||
<PlusOutlined /> 新建计划
|
<PlusOutlined /> 新建计划
|
||||||
</Button>
|
</Button>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -427,13 +427,13 @@ const ScenarioList: React.FC = () => {
|
|||||||
{searchTerm ? "没有找到匹配的计划" : "暂无计划"}
|
{searchTerm ? "没有找到匹配的计划" : "暂无计划"}
|
||||||
</div>
|
</div>
|
||||||
{scenarioId !== "10" && (
|
{scenarioId !== "10" && (
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleCreateNewPlan}
|
onClick={handleCreateNewPlan}
|
||||||
className={style["create-first-btn"]}
|
className={style["create-first-btn"]}
|
||||||
>
|
>
|
||||||
<PlusOutlined /> 创建第一个计划
|
<PlusOutlined /> 创建第一个计划
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -34,11 +34,12 @@ Route::group('v1/', function () {
|
|||||||
Route::get('list', 'app\chukebao\controller\AccountsController@getList'); // 获取账号列表
|
Route::get('list', 'app\chukebao\controller\AccountsController@getList'); // 获取账号列表
|
||||||
});
|
});
|
||||||
|
|
||||||
//客服相关
|
//消息相关
|
||||||
Route::group('message/', function () {
|
Route::group('message/', function () {
|
||||||
Route::get('list', 'app\chukebao\controller\MessageController@getList'); // 获取好友列表
|
Route::get('list', 'app\chukebao\controller\MessageController@getList'); // 获取好友列表
|
||||||
Route::get('readMessage', 'app\chukebao\controller\MessageController@readMessage'); // 读取消息
|
Route::get('readMessage', 'app\chukebao\controller\MessageController@readMessage'); // 读取消息
|
||||||
Route::get('details', 'app\chukebao\controller\MessageController@details'); // 消息详情
|
Route::get('details', 'app\chukebao\controller\MessageController@details'); // 消息详情
|
||||||
|
Route::get('getMessageStatus', 'app\chukebao\controller\MessageController@getMessageStatus'); // 获取单条消息发送状态
|
||||||
});
|
});
|
||||||
|
|
||||||
//微信分组
|
//微信分组
|
||||||
|
|||||||
@@ -196,6 +196,110 @@ class MessageController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单条消息发送状态(带轮询功能)
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function getMessageStatus()
|
||||||
|
{
|
||||||
|
$messageId = $this->request->param('messageId', 0);
|
||||||
|
$wechatAccountId = $this->request->param('wechatAccountId', '');
|
||||||
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
|
$wechatFriendId = $this->request->param('wechatFriendId', '');
|
||||||
|
$wechatChatroomId = $this->request->param('wechatChatroomId', '');
|
||||||
|
|
||||||
|
if (empty($accountId)) {
|
||||||
|
return ResponseHelper::error('请先登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($messageId)) {
|
||||||
|
return ResponseHelper::error('消息ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(empty($wechatFriendId) && empty($wechatChatroomId)) {
|
||||||
|
return ResponseHelper::error('消息类型不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询单条消息的基本信息(只需要发送状态相关字段)
|
||||||
|
$message = Db::table('s2_wechat_message')
|
||||||
|
->where('id', $messageId)
|
||||||
|
->field('id,wechatAccountId,wechatFriendId,wechatChatroomId,sendStatus')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (empty($message)) {
|
||||||
|
$message = [
|
||||||
|
'id' => $messageId,
|
||||||
|
'wechatAccountId' => $wechatAccountId,
|
||||||
|
'wechatFriendId' => $wechatFriendId,
|
||||||
|
'wechatChatroomId' => $wechatChatroomId,
|
||||||
|
'sendStatus' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sendStatus = isset($message['sendStatus']) ? (int)$message['sendStatus'] : 0;
|
||||||
|
$isUpdated = false;
|
||||||
|
$pollCount = 0;
|
||||||
|
$maxPollCount = 10; // 最多轮询10次
|
||||||
|
|
||||||
|
// 如果sendStatus不为0,开始轮询
|
||||||
|
if ($sendStatus != 0) {
|
||||||
|
$messageRequest = [
|
||||||
|
'id' => $message['id'],
|
||||||
|
'wechatAccountId' => !empty($wechatAccountId) ? $wechatAccountId : $message['wechatAccountId'],
|
||||||
|
'wechatFriendId' => !empty($message['wechatFriendId']) ? $message['wechatFriendId'] : '',
|
||||||
|
'wechatChatroomId' => !empty($message['wechatChatroomId']) ? $message['wechatChatroomId'] : '',
|
||||||
|
'from' => '',
|
||||||
|
'to' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
// 轮询逻辑:最多10次
|
||||||
|
while ($pollCount < $maxPollCount && $sendStatus != 0) {
|
||||||
|
$pollCount++;
|
||||||
|
|
||||||
|
// 请求线上接口获取最新状态
|
||||||
|
$newData = $this->fetchLatestMessageFromApi($messageRequest);
|
||||||
|
|
||||||
|
if (!empty($newData)) {
|
||||||
|
// 重新查询消息状态(可能已更新)
|
||||||
|
$updatedMessage = Db::table('s2_wechat_message')
|
||||||
|
->where('id', $messageId)
|
||||||
|
->field('sendStatus')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!empty($updatedMessage)) {
|
||||||
|
$newSendStatus = isset($updatedMessage['sendStatus']) ? (int)$updatedMessage['sendStatus'] : 0;
|
||||||
|
|
||||||
|
// 如果状态已更新为0(已发送),停止轮询
|
||||||
|
if ($newSendStatus == 0) {
|
||||||
|
$sendStatus = 0;
|
||||||
|
$isUpdated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果状态仍然是1,继续轮询(但需要等待一下,避免请求过快)
|
||||||
|
if ($newSendStatus != 0 && $pollCount < $maxPollCount) {
|
||||||
|
// 每次轮询间隔500毫秒(0.5秒)
|
||||||
|
usleep(500000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果请求失败,等待后继续尝试
|
||||||
|
if ($pollCount < $maxPollCount) {
|
||||||
|
usleep(500000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回发送状态信息
|
||||||
|
return ResponseHelper::success([
|
||||||
|
'messageId' => $messageId,
|
||||||
|
'sendStatus' => $sendStatus,
|
||||||
|
'statusText' => $sendStatus == 0 ? '已发送' : '发送中'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function details()
|
public function details()
|
||||||
{
|
{
|
||||||
$wechatFriendId = $this->request->param('wechatFriendId', '');
|
$wechatFriendId = $this->request->param('wechatFriendId', '');
|
||||||
@@ -257,6 +361,12 @@ class MessageController extends BaseController
|
|||||||
return ResponseHelper::success(['total' => $total, 'list' => $list]);
|
return ResponseHelper::success(['total' => $total, 'list' => $list]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从线上接口获取最新消息
|
* 从线上接口获取最新消息
|
||||||
* @param array $messageRequest 消息项(包含wechatAccountId、wechatFriendId或wechatChatroomId、id等)
|
* @param array $messageRequest 消息项(包含wechatAccountId、wechatFriendId或wechatChatroomId、id等)
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class TokensRecordController extends BaseController
|
|||||||
$limit = $this->request->param('limit', 10);
|
$limit = $this->request->param('limit', 10);
|
||||||
$type = $this->request->param('type', '');
|
$type = $this->request->param('type', '');
|
||||||
$form = $this->request->param('form', '');
|
$form = $this->request->param('form', '');
|
||||||
|
$startTime = $this->request->param('startTime', '');
|
||||||
|
$endTime = $this->request->param('endTime', '');
|
||||||
$userId = $this->getUserInfo('id');
|
$userId = $this->getUserInfo('id');
|
||||||
$companyId = $this->getUserInfo('companyId');
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
@@ -32,6 +34,26 @@ class TokensRecordController extends BaseController
|
|||||||
$where[] = ['form','=',$form];
|
$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);
|
$query = TokensRecord::where($where);
|
||||||
$total = $query->count();
|
$total = $query->count();
|
||||||
|
|||||||
@@ -495,7 +495,7 @@ class PaymentService
|
|||||||
switch ($order['orderType']) {
|
switch ($order['orderType']) {
|
||||||
case 1:
|
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);
|
$goodsSpecs = json_decode($order->goodsSpecs, true);
|
||||||
if (!empty($token)) {
|
if (!empty($token)) {
|
||||||
$token->tokens = $token->tokens + $goodsSpecs['tokens'];
|
$token->tokens = $token->tokens + $goodsSpecs['tokens'];
|
||||||
@@ -504,6 +504,7 @@ class PaymentService
|
|||||||
$newTokens = $token->tokens;
|
$newTokens = $token->tokens;
|
||||||
} else {
|
} else {
|
||||||
$tokensCompany = new TokensCompany();
|
$tokensCompany = new TokensCompany();
|
||||||
|
$tokensCompany->userId = $order->userId;
|
||||||
$tokensCompany->companyId = $order->companyId;
|
$tokensCompany->companyId = $order->companyId;
|
||||||
$tokensCompany->tokens = $goodsSpecs['tokens'];
|
$tokensCompany->tokens = $goodsSpecs['tokens'];
|
||||||
$tokensCompany->createTime = time();
|
$tokensCompany->createTime = time();
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class TokensController extends BaseController
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
//获取配置的tokens比例
|
//获取配置的tokens比例
|
||||||
$tokens_multiple = Env::get('payment.tokens_multiple', 28);
|
$tokens_multiple = Env::get('payment.tokens_multiple', 20);
|
||||||
$specs = [
|
$specs = [
|
||||||
'id' => 0,
|
'id' => 0,
|
||||||
'name' => '自定义购买算力',
|
'name' => '自定义购买算力',
|
||||||
@@ -119,7 +119,7 @@ class TokensController extends BaseController
|
|||||||
return ResponseHelper::success($order, '订单已支付');
|
return ResponseHelper::success($order, '订单已支付');
|
||||||
} else {
|
} else {
|
||||||
$errorMsg = !empty($order['payInfo']) ? $order['payInfo'] : '订单未支付';
|
$errorMsg = !empty($order['payInfo']) ? $order['payInfo'] : '订单未支付';
|
||||||
return ResponseHelper::success($order,$errorMsg,400);
|
return ResponseHelper::success($order,$errorMsg);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return ResponseHelper::success($order, '订单已支付');
|
return ResponseHelper::success($order, '订单已支付');
|
||||||
@@ -140,6 +140,7 @@ class TokensController extends BaseController
|
|||||||
$status = $this->request->param('status', ''); // 订单状态筛选
|
$status = $this->request->param('status', ''); // 订单状态筛选
|
||||||
$keyword = $this->request->param('keyword', ''); // 关键词搜索(订单号)
|
$keyword = $this->request->param('keyword', ''); // 关键词搜索(订单号)
|
||||||
$orderType = $this->request->param('orderType', ''); // 订单类型筛选
|
$orderType = $this->request->param('orderType', ''); // 订单类型筛选
|
||||||
|
$payType = $this->request->param('payType', ''); // 支付类型筛选
|
||||||
$startTime = $this->request->param('startTime', ''); // 开始时间
|
$startTime = $this->request->param('startTime', ''); // 开始时间
|
||||||
$endTime = $this->request->param('endTime', ''); // 结束时间
|
$endTime = $this->request->param('endTime', ''); // 结束时间
|
||||||
|
|
||||||
@@ -166,6 +167,11 @@ class TokensController extends BaseController
|
|||||||
$where[] = ['orderType', '=', $orderType];
|
$where[] = ['orderType', '=', $orderType];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 支付类型筛选
|
||||||
|
if($payType !== '') {
|
||||||
|
$where[] = ['payType', '=', $payType];
|
||||||
|
}
|
||||||
|
|
||||||
// 时间范围筛选
|
// 时间范围筛选
|
||||||
if (!empty($startTime)) {
|
if (!empty($startTime)) {
|
||||||
$where[] = ['createTime', '>=', strtotime($startTime)];
|
$where[] = ['createTime', '>=', strtotime($startTime)];
|
||||||
@@ -300,16 +306,63 @@ class TokensController extends BaseController
|
|||||||
])->sum('tokens');
|
])->sum('tokens');
|
||||||
$totalRecharged = intval($totalRecharged);
|
$totalRecharged = intval($totalRecharged);
|
||||||
|
|
||||||
|
// 计算预计可用天数(基于过去一个月的平均消耗)
|
||||||
|
$estimatedDays = $this->calculateEstimatedDays($companyId, $remainingTokens);
|
||||||
|
|
||||||
return ResponseHelper::success([
|
return ResponseHelper::success([
|
||||||
'totalTokens' => $totalRecharged, // 总算力(累计充值)
|
'totalTokens' => $totalRecharged, // 总算力(累计充值)
|
||||||
'todayUsed' => $todayUsed, // 今日使用
|
'todayUsed' => $todayUsed, // 今日使用
|
||||||
'monthUsed' => $monthUsed, // 本月使用
|
'monthUsed' => $monthUsed, // 本月使用
|
||||||
'remainingTokens' => $remainingTokens, // 剩余算力
|
'remainingTokens' => $remainingTokens, // 剩余算力
|
||||||
'totalConsumed' => $totalConsumed, // 累计消费
|
'totalConsumed' => $totalConsumed, // 累计消费
|
||||||
|
'estimatedDays' => $estimatedDays, // 预计可用天数
|
||||||
], '获取成功');
|
], '获取成功');
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return ResponseHelper::error('获取算力统计失败:' . $e->getMessage());
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user