代码提交

This commit is contained in:
wong
2026-01-06 10:58:29 +08:00
parent efcbb06eb2
commit ea2dd8cab2
23 changed files with 1848 additions and 722 deletions

View File

@@ -9,6 +9,7 @@
"antd-mobile": "^5.39.1",
"antd-mobile-icons": "^0.3.0",
"axios": "^1.6.7",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
@@ -21,6 +22,7 @@
"zustand": "^5.0.6"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/node": "^24.0.14",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",

View File

@@ -23,6 +23,9 @@ importers:
axios:
specifier: ^1.6.7
version: 1.11.0
crypto-js:
specifier: ^4.2.0
version: 4.2.0
dayjs:
specifier: ^1.11.13
version: 1.11.13
@@ -54,6 +57,9 @@ importers:
specifier: ^5.0.6
version: 5.0.7(@types/react@19.1.10)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1))
devDependencies:
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/node':
specifier: ^24.0.14
version: 24.2.1
@@ -489,36 +495,42 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -669,56 +681,67 @@ packages:
resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.46.2':
resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.46.2':
resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.46.2':
resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.46.2':
resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.46.2':
resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.46.2':
resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.46.2':
resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.46.2':
resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.46.2':
resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.46.2':
resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.46.2':
resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==}
@@ -747,6 +770,9 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/crypto-js@4.2.2':
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -1016,6 +1042,9 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@@ -2958,6 +2987,8 @@ snapshots:
dependencies:
'@babel/types': 7.28.2
'@types/crypto-js@4.2.2': {}
'@types/estree@1.0.8': {}
'@types/node@24.2.1':
@@ -3357,6 +3388,8 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
crypto-js@4.2.0: {}
csstype@3.1.3: {}
data-view-buffer@1.0.2:

View File

@@ -0,0 +1,89 @@
import axios from "axios";
import { Toast } from "antd-mobile";
import { generateSign } from "./utils/sign";
// API配置
const API_BASE_URL = "https://ckbapi.quwanzhi.com/v1/api";
const API_KEY = "v3pzy-zcfkg-96jio-7xgh6-14kio";
export interface SubmitLeadParams {
phone: string;
name: string;
source: string;
remark?: string;
wechatId?: string;
tags?: string;
siteTags?: string;
}
export interface SubmitLeadResponse {
code: number;
message: string;
data: string | null;
}
/**
* 提交线索到存客宝
*/
export async function submitLead(
params: SubmitLeadParams,
): Promise<SubmitLeadResponse> {
try {
// 生成时间戳(秒级)
const timestamp = Math.floor(Date.now() / 1000);
// 构建请求参数
const requestParams: Record<string, any> = {
apiKey: API_KEY,
timestamp,
phone: params.phone,
name: params.name,
source: params.source,
};
// 添加可选字段(只添加非空值)
if (params.remark) {
requestParams.remark = params.remark;
}
if (params.wechatId) {
requestParams.wechatId = params.wechatId;
}
if (params.tags) {
requestParams.tags = params.tags;
}
if (params.siteTags) {
requestParams.siteTags = params.siteTags;
}
// 生成签名
const sign = generateSign(requestParams, API_KEY);
requestParams.sign = sign;
// 发送请求
const response = await axios.post<SubmitLeadResponse>(
`${API_BASE_URL}/scenarios`,
requestParams,
{
headers: {
"Content-Type": "application/json",
},
timeout: 20000,
},
);
const result = response.data;
// 处理响应
if (result.code === 200) {
return result;
} else {
throw new Error(result.message || "提交失败");
}
} catch (error: any) {
const errorMessage =
error.response?.data?.message ||
error.message ||
"网络请求失败,请稍后重试";
throw new Error(errorMessage);
}
}

View File

@@ -0,0 +1,47 @@
.modalContainer {
background: #fff;
min-height: 400px;
}
.modalHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
position: sticky;
top: 0;
background: #fff;
z-index: 1;
}
.modalTitle {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.closeIcon {
font-size: 20px;
color: #666;
cursor: pointer;
padding: 4px;
transition: color 0.2s;
&:hover {
color: #333;
}
}
.modalContent {
padding: 20px;
}
.formFooter {
display: flex;
gap: 12px;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}

View File

@@ -0,0 +1,173 @@
import React, { useState } from "react";
import { Popup, Form, Input, TextArea, Button, Toast } from "antd-mobile";
import { CloseOutlined } from "@ant-design/icons";
import styles from "./TestFormModal.module.scss";
import { submitLead } from "../api";
interface TestFormModalProps {
visible: boolean;
onClose: () => void;
onSubmit?: (values: TestFormValues) => void;
}
export interface TestFormValues {
phone: string;
name: string;
source: string;
remark: string;
}
const TestFormModal: React.FC<TestFormModalProps> = ({
visible,
onClose,
onSubmit,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setLoading(true);
// 调用API提交数据
const result = await submitLead({
phone: values.phone,
name: values.name,
source: values.source,
remark: values.remark || undefined,
});
// 调用提交回调(如果提供)
if (onSubmit) {
onSubmit(values as TestFormValues);
}
Toast.show({
content: result.message || "提交成功",
icon: "success",
});
// 重置表单并关闭弹框
form.resetFields();
onClose();
} catch (error: any) {
console.error("提交失败:", error);
Toast.show({
content: error.message || "提交失败,请稍后重试",
icon: "fail",
});
} finally {
setLoading(false);
}
};
const handleClose = () => {
form.resetFields();
onClose();
};
return (
<Popup
visible={visible}
onMaskClick={handleClose}
position="bottom"
bodyStyle={{
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
maxHeight: "90vh",
overflowY: "auto",
}}
>
<div className={styles.modalContainer}>
{/* 头部 */}
<div className={styles.modalHeader}>
<h3 className={styles.modalTitle}></h3>
<CloseOutlined
className={styles.closeIcon}
onClick={handleClose}
/>
</div>
{/* 表单内容 */}
<div className={styles.modalContent}>
<Form
form={form}
layout="vertical"
footer={
<div className={styles.formFooter}>
<Button
onClick={handleClose}
style={{ marginRight: 12 }}
disabled={loading}
>
</Button>
<Button
color="primary"
onClick={handleSubmit}
loading={loading}
block
>
</Button>
</div>
}
>
<Form.Item
label="手机号"
name="phone"
rules={[
{ required: true, message: "请输入手机号" },
{
pattern: /^1[3-9]\d{9}$/,
message: "请输入正确的手机号",
},
]}
>
<Input
placeholder="请输入手机号"
type="tel"
maxLength={11}
/>
</Form.Item>
<Form.Item
label="姓名"
name="name"
rules={[
{ required: true, message: "请输入姓名" },
{ max: 20, message: "姓名不能超过20个字符" },
]}
>
<Input placeholder="请输入姓名" maxLength={20} />
</Form.Item>
<Form.Item
label="来源"
name="source"
rules={[{ required: true, message: "请输入来源" }]}
>
<Input placeholder="请输入来源" maxLength={50} />
</Form.Item>
<Form.Item
label="备注"
name="remark"
rules={[{ max: 200, message: "备注不能超过200个字符" }]}
>
<TextArea
placeholder="请输入备注(选填)"
showCount
maxLength={200}
rows={3}
/>
</Form.Item>
</Form>
</div>
</div>
</Popup>
);
};
export default TestFormModal;

View File

@@ -1,19 +1,28 @@
import React from "react";
import React, { useState } from "react";
import { Card, Button, Space, Typography, Tag } from "antd";
import {
MessageOutlined,
SelectOutlined,
UploadOutlined,
FormOutlined,
} from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
import { isDevelopment } from "@/utils/env";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import TestFormModal, { TestFormValues } from "./components/TestFormModal";
const { Title, Text } = Typography;
const TestIndex: React.FC = () => {
const navigate = useNavigate();
const [testFormVisible, setTestFormVisible] = useState(false);
const handleTestFormSubmit = (values: TestFormValues) => {
// API调用已在TestFormModal内部完成
// 这里可以添加额外的处理逻辑,如日志记录、数据分析等
console.log("测试表单提交成功,数据:", values);
};
return (
<Layout header={<NavCommon title="测试页面" />}>
@@ -60,11 +69,31 @@ const TestIndex: React.FC = () => {
</Space>
</Card>
<Card title="功能测试" size="small">
<Space direction="vertical" style={{ width: "100%" }}>
<Button
icon={<FormOutlined />}
size="large"
block
onClick={() => setTestFormVisible(true)}
>
</Button>
</Space>
</Card>
<Card title="说明" size="small">
<Text>便</Text>
</Card>
</Space>
</div>
{/* 测试表单弹框 */}
<TestFormModal
visible={testFormVisible}
onClose={() => setTestFormVisible(false)}
onSubmit={handleTestFormSubmit}
/>
</Layout>
);
};

View File

@@ -0,0 +1,54 @@
/**
* 签名生成工具
* 根据API文档的签名规则生成MD5签名
*/
import CryptoJS from "crypto-js";
/**
* 生成MD5哈希值
*/
function md5(text: string): string {
return CryptoJS.MD5(text).toString();
}
/**
* 生成签名
* 根据API文档的签名规则生成MD5签名
*/
export function generateSign(
params: Record<string, any>,
apiKey: string,
): string {
// 第一步:移除 sign、apiKey、portrait
const filteredParams: Record<string, any> = { ...params };
delete filteredParams.sign;
delete filteredParams.apiKey;
delete filteredParams.portrait;
// 第二步移除空值null 和空字符串)
const nonEmptyParams: Record<string, any> = {};
for (const key in filteredParams) {
const value = filteredParams[key];
if (value !== null && value !== "" && value !== undefined) {
nonEmptyParams[key] = value;
}
}
// 第三步:按参数名升序排序
const sortedKeys = Object.keys(nonEmptyParams).sort();
// 第四步:拼接参数值
let stringToSign = "";
for (const key of sortedKeys) {
stringToSign += String(nonEmptyParams[key]);
}
// 第五步第一次MD5
const firstMd5 = md5(stringToSign);
// 第六步拼接apiKey后第二次MD5
const finalSign = md5(firstMd5 + apiKey);
return finalSign;
}

View File

@@ -14,6 +14,8 @@ import {
Select,
Radio,
} from "antd";
const { TextArea } = Input;
import { fetchSocialMediaList, fetchPromotionSiteList } from "../index.api";
interface BasicSettingsProps {
@@ -26,6 +28,19 @@ interface BasicSettingsProps {
isLoop: number; // 0: 否, 1: 是
pushType: number; // 0: 定时推送, 1: 立即推送
status: number; // 0: 否, 1: 是
isRandomTemplate?: number; // 是否随机模板0=否1=是
postPushTags?: string[]; // 推送后标签数组
targetType?: number; // 1=群推送2=好友推送
groupPushSubType?: number; // 1=群群发2=群公告
// 好友推送间隔设置
friendIntervalMin?: number;
friendIntervalMax?: number;
messageIntervalMin?: number;
messageIntervalMax?: number;
// 群公告相关
announcementContent?: string;
enableAiRewrite?: number;
aiRewritePrompt?: string;
socialMediaId?: string;
promotionSiteId?: string;
};
@@ -51,6 +66,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
isLoop: 0, // 0: 否, 1: 是
pushType: 0, // 0: 定时推送, 1: 立即推送
status: 0, // 0: 否, 1: 是
targetType: 1, // 默认1=群推送
groupPushSubType: 1, // 默认1=群群发
socialMediaId: undefined,
promotionSiteId: undefined,
},
@@ -69,6 +86,29 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
forceUpdate({});
}, []);
// 监听 defaultValues 变化,更新表单值(用于编辑模式的数据回填)
useEffect(() => {
if (defaultValues) {
form.setFieldsValue(defaultValues);
forceUpdate({}); // 强制更新以刷新按钮状态
// 如果有社交媒体ID加载对应的推广站点列表
if (defaultValues.socialMediaId && defaultValues.socialMediaId !== "") {
const socialMediaIdNum = Number(defaultValues.socialMediaId);
if (!isNaN(socialMediaIdNum)) {
setLoadingPromotionSite(true);
fetchPromotionSiteList(socialMediaIdNum)
.then(res => {
setPromotionSiteList(res);
})
.finally(() => {
setLoadingPromotionSite(false);
});
}
}
}
}, [defaultValues, form]);
// 组件挂载时获取社交媒体列表
useEffect(() => {
setLoadingSocialMedia(true);
@@ -144,6 +184,25 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
>
<Input placeholder="请输入任务名称" />
</Form.Item>
{/* 推送目标类型 - 暂时隐藏,但保留默认值 */}
<Form.Item
name="targetType"
hidden
initialValue={1}
>
<Input type="hidden" />
</Form.Item>
{/* 群推送子类型 - 暂时隐藏,但保留默认值 */}
<Form.Item
name="groupPushSubType"
hidden
initialValue={1}
>
<Input type="hidden" />
</Form.Item>
{/* 推送类型 */}
<Form.Item
label="推送类型"
@@ -190,96 +249,202 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
}}
</Form.Item>
{/* 每日推送 */}
{/* 每日推送 - 群公告时隐藏 */}
<Form.Item
label="每日推送"
name="maxPerDay"
rules={[
{ required: true, message: "请输入每日推送数量" },
{
type: "number",
min: 1,
max: 100,
message: "每日推送数量在1-100之间",
},
]}
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.targetType !== currentValues.targetType ||
prevValues.groupPushSubType !== currentValues.groupPushSubType
}
>
<InputNumber
min={1}
max={100}
style={{ width: 120 }}
addonAfter="条内容"
/>
{({ getFieldValue }) => {
const isGroupAnnouncement = getFieldValue("targetType") === 1 && getFieldValue("groupPushSubType") === 2;
return !isGroupAnnouncement ? (
<Form.Item
label="每日推送"
name="maxPerDay"
rules={[
{ required: true, message: "请输入每日推送数量" },
{
type: "number",
min: 1,
max: 100,
message: "每日推送数量在1-100之间",
},
]}
>
<InputNumber
min={1}
max={100}
style={{ width: 120 }}
addonAfter="条内容"
/>
</Form.Item>
) : null;
}}
</Form.Item>
{/* 推送顺序 */}
{/* 推送顺序 - 群公告时隐藏 */}
<Form.Item
label="推送顺序"
name="pushOrder"
rules={[{ required: true, message: "请选择推送顺序" }]}
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.targetType !== currentValues.targetType ||
prevValues.groupPushSubType !== currentValues.groupPushSubType
}
>
<div style={{ display: "flex" }}>
<Button
type={
form.getFieldValue("pushOrder") == 1 ? "primary" : "default"
}
style={{ borderRadius: "6px 0 0 6px" }}
onClick={() => handlePushOrderChange(1)}
>
</Button>
<Button
type={
form.getFieldValue("pushOrder") == 2 ? "primary" : "default"
}
style={{ borderRadius: "0 6px 6px 0", marginLeft: -1 }}
onClick={() => handlePushOrderChange(2)}
>
</Button>
</div>
{({ getFieldValue }) => {
const isGroupAnnouncement = getFieldValue("targetType") === 1 && getFieldValue("groupPushSubType") === 2;
return !isGroupAnnouncement ? (
<Form.Item
label="推送顺序"
name="pushOrder"
rules={[{ required: true, message: "请选择推送顺序" }]}
>
<div style={{ display: "flex" }}>
<Button
type={
form.getFieldValue("pushOrder") == 1 ? "primary" : "default"
}
style={{ borderRadius: "6px 0 0 6px" }}
onClick={() => handlePushOrderChange(1)}
>
</Button>
<Button
type={
form.getFieldValue("pushOrder") == 2 ? "primary" : "default"
}
style={{ borderRadius: "0 6px 6px 0", marginLeft: -1 }}
onClick={() => handlePushOrderChange(2)}
>
</Button>
</div>
</Form.Item>
) : null;
}}
</Form.Item>
{/* 京东联盟 */}
<Form.Item label="京东联盟" style={{ marginBottom: 16 }}>
<div style={{ display: "flex", gap: 12, alignItems: "flex-end" }}>
<Form.Item name="socialMediaId" noStyle>
<Select
placeholder="请选择社交媒体"
style={{ width: 200 }}
loading={loadingSocialMedia}
onChange={handleSocialMediaChange}
options={socialMediaList.map(item => ({
label: item.name,
value: item.id,
}))}
/>
</Form.Item>
{/* 京东联盟 - 仅群推送显示,群公告时隐藏 */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.targetType !== currentValues.targetType ||
prevValues.groupPushSubType !== currentValues.groupPushSubType
}
>
{({ getFieldValue }) => {
const isGroupAnnouncement = getFieldValue("targetType") === 1 && getFieldValue("groupPushSubType") === 2;
return getFieldValue("targetType") === 1 && !isGroupAnnouncement ? (
<Form.Item label="京东联盟" style={{ marginBottom: 16 }}>
<div style={{ display: "flex", gap: 12, alignItems: "flex-end" }}>
<Form.Item name="socialMediaId" noStyle>
<Select
placeholder="请选择社交媒体"
style={{ width: 200 }}
loading={loadingSocialMedia}
onChange={handleSocialMediaChange}
options={socialMediaList.map(item => ({
label: item.name,
value: item.id,
}))}
/>
</Form.Item>
<Form.Item name="promotionSiteId" noStyle>
<Select
placeholder="请选择推广站点"
style={{ width: 200 }}
loading={loadingPromotionSite}
disabled={!form.getFieldValue("socialMediaId")}
options={promotionSiteList.map(item => ({
label: item.name,
value: item.id,
}))}
/>
</Form.Item>
</div>
<Form.Item name="promotionSiteId" noStyle>
<Select
placeholder="请选择推广站点"
style={{ width: 200 }}
loading={loadingPromotionSite}
disabled={!form.getFieldValue("socialMediaId")}
options={promotionSiteList.map(item => ({
label: item.name,
value: item.id,
}))}
/>
</Form.Item>
</div>
</Form.Item>
) : null;
}}
</Form.Item>
{/* 是否循环推送 */}
{/* 是否随机模板 - 群公告时隐藏 */}
<Form.Item
label="是否循环推送"
name="isLoop"
valuePropName="checked"
getValueFromEvent={checked => (checked ? 1 : 0)}
getValueProps={value => ({ checked: value === 1 })}
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.targetType !== currentValues.targetType ||
prevValues.groupPushSubType !== currentValues.groupPushSubType
}
>
<Switch />
{({ getFieldValue }) => {
const isGroupAnnouncement = getFieldValue("targetType") === 1 && getFieldValue("groupPushSubType") === 2;
return !isGroupAnnouncement ? (
<Form.Item
label="是否随机模板"
name="isRandomTemplate"
valuePropName="checked"
getValueFromEvent={checked => (checked ? 1 : 0)}
getValueProps={value => ({ checked: value === 1 })}
>
<Switch />
</Form.Item>
) : null;
}}
</Form.Item>
{/* 推送后标签 - 仅好友推送显示 */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.targetType !== currentValues.targetType
}
>
{({ getFieldValue }) => {
return getFieldValue("targetType") === 2 ? (
<Form.Item
label="推送后标签"
name="postPushTags"
tooltip="推送后自动添加的标签,多个标签用逗号分隔"
>
<Input
placeholder="请输入标签,多个用逗号分隔"
onChange={e => {
const tags = e.target.value
.split(",")
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
form.setFieldValue("postPushTags", tags);
}}
/>
</Form.Item>
) : null;
}}
</Form.Item>
{/* 是否循环推送 - 群推送和好友推送都显示,群公告时隐藏,好友推送默认为否 */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.targetType !== currentValues.targetType ||
prevValues.groupPushSubType !== currentValues.groupPushSubType
}
>
{({ getFieldValue }) => {
const isGroupAnnouncement = getFieldValue("targetType") === 1 && getFieldValue("groupPushSubType") === 2;
return !isGroupAnnouncement ? (
<Form.Item
label="是否循环推送"
name="isLoop"
valuePropName="checked"
getValueFromEvent={checked => (checked ? 1 : 0)}
getValueProps={value => ({ checked: value === 1 })}
initialValue={0} // 默认为否
>
<Switch />
</Form.Item>
) : null;
}}
</Form.Item>
{/* 是否启用 */}
@@ -293,6 +458,139 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
<Switch />
</Form.Item>
{/* 推送间隔设置 - 仅好友推送显示 */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.targetType !== currentValues.targetType
}
>
{({ getFieldValue }) => {
return getFieldValue("targetType") === 2 ? (
<>
<Form.Item label="目标间间隔(秒)">
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<Form.Item
name="friendIntervalMin"
noStyle
rules={[{ required: true, message: "请输入最小间隔" }]}
>
<InputNumber
min={1}
placeholder="最小"
style={{ width: 100 }}
/>
</Form.Item>
<span style={{ color: "#888" }}></span>
<Form.Item
name="friendIntervalMax"
noStyle
rules={[{ required: true, message: "请输入最大间隔" }]}
>
<InputNumber
min={1}
placeholder="最大"
style={{ width: 100 }}
/>
</Form.Item>
</div>
</Form.Item>
<Form.Item label="消息间间隔(秒)">
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<Form.Item
name="messageIntervalMin"
noStyle
rules={[{ required: true, message: "请输入最小间隔" }]}
>
<InputNumber
min={1}
placeholder="最小"
style={{ width: 100 }}
/>
</Form.Item>
<span style={{ color: "#888" }}></span>
<Form.Item
name="messageIntervalMax"
noStyle
rules={[{ required: true, message: "请输入最大间隔" }]}
>
<InputNumber
min={1}
placeholder="最大"
style={{ width: 100 }}
/>
</Form.Item>
</div>
</Form.Item>
</>
) : null;
}}
</Form.Item>
{/* 群公告设置 - 仅当targetType=1且groupPushSubType=2时显示 */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.targetType !== currentValues.targetType ||
prevValues.groupPushSubType !== currentValues.groupPushSubType
}
>
{({ getFieldValue }) => {
return getFieldValue("targetType") === 1 &&
getFieldValue("groupPushSubType") === 2 ? (
<>
<Form.Item
label="群公告内容"
name="announcementContent"
rules={[
{ required: true, message: "请输入群公告内容" },
{ min: 1, max: 500, message: "群公告内容长度在1-500个字符之间" },
]}
>
<TextArea
rows={4}
placeholder="请输入群公告内容"
maxLength={500}
showCount
/>
</Form.Item>
<Form.Item
label="是否启用AI改写"
name="enableAiRewrite"
valuePropName="checked"
getValueFromEvent={checked => (checked ? 1 : 0)}
getValueProps={value => ({ checked: value === 1 })}
>
<Switch />
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.enableAiRewrite !== currentValues.enableAiRewrite
}
>
{({ getFieldValue }) => {
return getFieldValue("enableAiRewrite") === 1 ? (
<Form.Item
label="AI改写提示词"
name="aiRewritePrompt"
rules={[
{ required: true, message: "请输入AI改写提示词" },
]}
>
<TextArea
rows={3}
placeholder="请输入AI改写提示词"
/>
</Form.Item>
) : null;
}}
</Form.Item>
</>
) : null;
}}
</Form.Item>
{/* 推送类型提示 */}
<Form.Item
noStyle

View File

@@ -0,0 +1,94 @@
import React, { useImperativeHandle, forwardRef } from "react";
import { Form, Card } from "antd";
import DeviceSelection from "@/components/DeviceSelection";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
interface DeviceSelectorProps {
selectedDevices: DeviceSelectionItem[];
onPrevious: () => void;
onNext: (data: {
deviceGroups: string[];
deviceGroupsOptions: DeviceSelectionItem[];
}) => void;
}
export interface DeviceSelectorRef {
validate: () => Promise<boolean>;
getValues: () => any;
}
const DeviceSelector = forwardRef<DeviceSelectorRef, DeviceSelectorProps>(
({ selectedDevices, onNext }, ref) => {
const [form] = Form.useForm();
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
try {
form.setFieldsValue({
deviceGroups: selectedDevices.map(item => String(item.id)),
});
await form.validateFields();
return true;
} catch (error) {
console.log("DeviceSelector 表单验证失败:", error);
return false;
}
},
getValues: () => {
return form.getFieldsValue();
},
}));
// 设备选择
const handleDeviceSelect = (deviceGroupsOptions: DeviceSelectionItem[]) => {
const deviceGroups = deviceGroupsOptions.map(item => String(item.id));
form.setFieldValue("deviceGroups", deviceGroups);
onNext({ deviceGroups, deviceGroupsOptions });
};
return (
<Card>
<Form
form={form}
layout="vertical"
initialValues={{ devices: selectedDevices }}
>
<div style={{ marginBottom: 20 }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
</h2>
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
使
</p>
</div>
<Form.Item
name="deviceGroups"
rules={[
{
required: true,
type: "array",
min: 1,
message: "请选择至少一个设备",
},
]}
>
<DeviceSelection
selectedOptions={selectedDevices}
onSelect={handleDeviceSelect}
placeholder="选择设备"
readonly={false}
showSelectedList={true}
selectedListMaxHeight={300}
/>
</Form.Item>
</Form>
</Card>
);
},
);
DeviceSelector.displayName = "DeviceSelector";
export default DeviceSelector;

View File

@@ -1,14 +1,25 @@
import React, { useImperativeHandle, forwardRef } from "react";
import React, { useImperativeHandle, forwardRef, useState } from "react";
import { Form, Card } from "antd";
import GroupSelection from "@/components/GroupSelection";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
import FriendSelection from "@/components/FriendSelection";
import { FriendSelectionItem } from "@/components/FriendSelection/data";
import PoolSelection from "@/components/PoolSelection";
import { PoolSelectionItem } from "@/components/PoolSelection/data";
interface GroupSelectorProps {
selectedGroups: GroupSelectionItem[];
targetType: number; // 1=群推送2=好友推送
selectedFriends?: any[];
selectedPools?: any[];
onPrevious: () => void;
onNext: (data: {
wechatGroups: string[];
wechatGroupsOptions: GroupSelectionItem[];
wechatGroups?: string[];
wechatGroupsOptions?: GroupSelectionItem[];
wechatFriends?: string[];
wechatFriendsOptions?: any[];
poolGroups?: string[];
poolGroupsOptions?: any[];
}) => void;
}
@@ -18,17 +29,44 @@ export interface GroupSelectorRef {
}
const GroupSelector = forwardRef<GroupSelectorRef, GroupSelectorProps>(
({ selectedGroups, onNext }, ref) => {
({ selectedGroups, targetType, selectedFriends = [], selectedPools = [], onNext }, ref) => {
const [form] = Form.useForm();
const [friendsOptions, setFriendsOptions] = useState<FriendSelectionItem[]>(selectedFriends);
const [poolsOptions, setPoolsOptions] = useState<PoolSelectionItem[]>(selectedPools);
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
try {
form.setFieldsValue({
wechatGroups: selectedGroups.map(item => item.id),
});
await form.validateFields();
if (targetType === 1) {
// 群推送:必须选择群组
form.setFieldsValue({
wechatGroups: selectedGroups.map(item => item.id),
});
await form.validateFields(["wechatGroups"]);
} else {
// 好友推送wechatFriends可选但如果为空则必须选择流量池
const friends = friendsOptions.map(item => String(item.id));
const pools = poolsOptions.map(item => String(item.id));
form.setFieldsValue({
wechatFriends: friends,
poolGroups: pools,
});
// 如果好友为空,则流量池必填
if (friends.length === 0 && pools.length === 0) {
form.setFields([
{
name: "poolGroups",
errors: ["好友为空时,必须选择流量池"],
},
]);
throw new Error("好友为空时,必须选择流量池");
}
await form.validateFields(["wechatFriends", "poolGroups"]);
}
return true;
} catch (error) {
console.log("GroupSelector 表单验证失败:", error);
@@ -40,50 +78,138 @@ const GroupSelector = forwardRef<GroupSelectorRef, GroupSelectorProps>(
},
}));
// 群组选择
// 群组选择targetType=1
const handleGroupSelect = (wechatGroupsOptions: GroupSelectionItem[]) => {
const wechatGroups = wechatGroupsOptions.map(item => item.id);
form.setFieldValue("wechatGroups", wechatGroups);
onNext({ wechatGroups, wechatGroupsOptions });
};
// 好友选择targetType=2
const handleFriendSelect = (friendsOptions: FriendSelectionItem[]) => {
setFriendsOptions(friendsOptions);
const wechatFriends = friendsOptions.map(item => String(item.id));
form.setFieldValue("wechatFriends", wechatFriends);
onNext({
wechatFriends,
wechatFriendsOptions: friendsOptions,
poolGroups: poolsOptions.map(p => String(p.id)),
poolGroupsOptions: poolsOptions,
});
};
// 流量池选择targetType=2
const handlePoolSelect = (poolsOptions: PoolSelectionItem[]) => {
setPoolsOptions(poolsOptions);
const poolGroups = poolsOptions.map(item => String(item.id));
form.setFieldValue("poolGroups", poolGroups);
onNext({
wechatFriends: friendsOptions.map(f => String(f.id)),
wechatFriendsOptions: friendsOptions,
poolGroups,
poolGroupsOptions: poolsOptions,
});
};
return (
<Card>
<Form
form={form}
layout="vertical"
initialValues={{ groups: selectedGroups }}
initialValues={{
groups: selectedGroups,
friends: friendsOptions,
pools: poolsOptions,
}}
>
<div style={{ marginBottom: 20 }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
</h2>
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
</p>
</div>
{targetType === 1 ? (
// 群推送模式
<>
<div style={{ marginBottom: 20 }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
</h2>
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
</p>
</div>
<Form.Item
name="wechatGroups"
rules={[
{
required: true,
type: "array",
min: 1,
message: "请选择至少一个群组",
},
{ type: "array", max: 50, message: "最多只能选择50个群组" },
]}
>
<GroupSelection
selectedOptions={selectedGroups}
onSelect={handleGroupSelect}
placeholder="选择要推送的群组"
readonly={false}
showSelectedList={true}
selectedListMaxHeight={300}
/>
</Form.Item>
<Form.Item
name="wechatGroups"
rules={[
{
required: true,
type: "array",
min: 1,
message: "请选择至少一个群组",
},
{ type: "array", max: 50, message: "最多只能选择50个群组" },
]}
>
<GroupSelection
selectedOptions={selectedGroups}
onSelect={handleGroupSelect}
placeholder="选择要推送的群组"
readonly={false}
showSelectedList={true}
selectedListMaxHeight={300}
/>
</Form.Item>
</>
) : (
// 好友推送模式
<>
<div style={{ marginBottom: 20 }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
</h2>
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
</p>
</div>
{/* 好友选择(可选) */}
<Form.Item
name="wechatFriends"
label="选择好友(可选)"
>
<FriendSelection
selectedOptions={friendsOptions}
onSelect={handleFriendSelect}
placeholder="选择要推送的好友"
readonly={false}
showSelectedList={true}
selectedListMaxHeight={300}
/>
</Form.Item>
{/* 流量池选择(当好友为空时必选) */}
<Form.Item
name="poolGroups"
label="选择流量池"
rules={[
({ getFieldValue }) => ({
validator: (_, value) => {
const friends = getFieldValue("wechatFriends") || [];
if (friends.length === 0 && (!value || value.length === 0)) {
return Promise.reject(new Error("好友为空时,必须选择流量池"));
}
return Promise.resolve();
},
}),
]}
>
<PoolSelection
selectedOptions={poolsOptions}
onSelect={handlePoolSelect}
placeholder="选择流量池"
readonly={false}
showSelectedList={true}
selectedListMaxHeight={300}
/>
</Form.Item>
</>
)}
</Form>
</Card>
);

View File

@@ -1,15 +1,18 @@
import request from "@/api/request";
export function createGroupPushTask(data) {
return request("/v1/workbench/create", { ...data, type: 3 }, "POST");
}
// 获取自动点赞任务详情
export function fetchGroupPushTaskDetail(id: string) {
return request("/v1/workbench/detail", { id }, "GET");
// 创建群发工作台
export function createGroupPushTask(data: any) {
return request("/v1/workbench/create", data, "POST");
}
export function updateGroupPushTask(data) {
return request("/v1/workbench/update", { ...data, type: 3 }, "POST");
// 更新群发工作台
export function updateGroupPushTask(data: any) {
return request("/v1/workbench/update", data, "POST");
}
// 获取群发工作台详情
export function fetchGroupPushTaskDetail(id: string) {
return request("/v1/workbench/detail", { id }, "GET");
}
// 获取京东社交媒体列表
export const fetchSocialMediaList = async () => {

View File

@@ -27,9 +27,33 @@ export interface FormData {
pushOrder: number; // 1: 按最早, 2: 按最新
isLoop: number; // 0: 否, 1: 是
pushType: number; // 0: 定时推送, 1: 立即推送
status: number; // 0: 否, 1: 是
status: number; // 0: 否, 1: 是(同时作为是否自动启动)
isRandomTemplate?: number; // 是否随机模板0=否1=是
postPushTags?: string[]; // 推送后标签数组
contentGroups: string[];
wechatGroups: string[];
// 推送目标类型1=群推送2=好友推送
targetType: number; // 默认1
// 群推送子类型1=群群发2=群公告仅当targetType=1时有效
groupPushSubType?: number; // 默认1
// 好友推送相关
wechatFriends?: string[]; // 当targetType=2时可选可以为空
wechatFriendsOptions?: any[]; // 好友选项列表
// 流量池当wechatFriends为空时必须选择
poolGroups?: string[]; // 流量池ID列表
poolGroupsOptions?: any[]; // 流量池选项列表
// 好友推送间隔设置
friendIntervalMin?: number; // 目标间最小间隔(秒)
friendIntervalMax?: number; // 目标间最大间隔(秒)
messageIntervalMin?: number; // 消息间最小间隔(秒)
messageIntervalMax?: number; // 消息间最大间隔(秒)
// 群公告相关仅当targetType=1且groupPushSubType=2时
announcementContent?: string; // 群公告内容
enableAiRewrite?: number; // 是否启用AI改写0=否1=是
aiRewritePrompt?: string; // AI改写提示词
// 设备选择
deviceGroups?: string[]; // 设备ID列表
deviceGroupsOptions?: any[]; // 设备选项列表
// 京东联盟相关字段
socialMediaId?: string;
promotionSiteId?: string;

View File

@@ -6,6 +6,7 @@ import { createGroupPushTask, fetchGroupPushTaskDetail } from "./index.api";
import Layout from "@/components/Layout/Layout";
import StepIndicator from "@/components/StepIndicator";
import BasicSettings, { BasicSettingsRef } from "./components/BasicSettings";
import DeviceSelector, { DeviceSelectorRef } from "./components/DeviceSelector";
import GroupSelector, { GroupSelectorRef } from "./components/GroupSelector";
import ContentSelector, {
ContentSelectorRef,
@@ -14,17 +15,49 @@ import type { FormData } from "./index.data";
import NavCommon from "@/components/NavCommon";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
import { ContentItem } from "@/components/ContentSelection/data";
const steps = [
{ id: 1, title: "步骤 1", subtitle: "基础设置" },
{ id: 2, title: "步骤 2", subtitle: "选择社群" },
{ id: 3, title: "步骤 3", subtitle: "选择内容库" },
];
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
// 根据targetType和groupPushSubType动态生成步骤
const getSteps = (targetType: number, groupPushSubType?: number) => {
const baseSteps = [
{ id: 1, title: "步骤 1", subtitle: "基础设置" },
{ id: 2, title: "步骤 2", subtitle: "选择设备" },
];
if (targetType === 2) {
// 好友推送:选择好友
return [
...baseSteps,
{ id: 3, title: "步骤 3", subtitle: "选择好友" },
{ id: 4, title: "步骤 4", subtitle: "选择内容库" },
];
} else {
// 群推送:选择社群
const steps = [
...baseSteps,
{ id: 3, title: "步骤 3", subtitle: "选择社群" },
];
// 群公告时不显示内容库步骤
if (groupPushSubType !== 2) {
steps.push({ id: 4, title: "步骤 4", subtitle: "选择内容库" });
}
return steps;
}
};
const NewGroupPush: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [currentStep, setCurrentStep] = useState(1);
const [loading, setLoading] = useState(false);
// 从 URL 参数获取推送类型
const urlParams = new URLSearchParams(window.location.search);
const urlTargetType = urlParams.get("targetType");
const urlGroupPushSubType = urlParams.get("groupPushSubType");
const [deviceGroupsOptions, setDeviceGroupsOptions] = useState<
DeviceSelectionItem[]
>([]);
const [wechatGroupsOptions, setWechatGroupsOptions] = useState<
GroupSelectionItem[]
>([]);
@@ -34,43 +67,170 @@ const NewGroupPush: React.FC = () => {
const [formData, setFormData] = useState<FormData>({
name: "",
startTime: "06:00", // 允许推送的开始时间
startTime: "09:00", // 允许推送的开始时间
dailyPushCount: 0, // 每日已推送次数
endTime: "23:59", // 允许推送的结束时间
endTime: "21:00", // 允许推送的结束时间
maxPerDay: 20,
pushOrder: 2, // 2: 按最
pushOrder: 1, // 1: 按最
isLoop: 0, // 0: 否, 1: 是
pushType: 0, // 0: 定时推送, 1: 立即推送
status: 0, // 0: 否, 1: 是
status: 0, // 0: 否, 1: 是(同时作为是否自动启动)
isRandomTemplate: 0, // 是否随机模板0=否1=是
postPushTags: [], // 推送后标签数组
wechatGroups: [],
contentGroups: [],
targetType: urlTargetType ? Number(urlTargetType) : 1, // 从URL参数获取默认1=群推送
groupPushSubType: urlGroupPushSubType ? Number(urlGroupPushSubType) : 1, // 从URL参数获取默认1=群群发
wechatFriends: [],
wechatFriendsOptions: [],
poolGroups: [],
poolGroupsOptions: [],
deviceGroups: [],
// 好友推送间隔设置
friendIntervalMin: 10, // 目标间最小间隔(秒)
friendIntervalMax: 20, // 目标间最大间隔(秒)
messageIntervalMin: 1, // 消息间最小间隔(秒)
messageIntervalMax: 12, // 消息间最大间隔(秒)
// 群公告相关
announcementContent: "",
enableAiRewrite: 0,
aiRewritePrompt: "",
});
const [isEditMode, setIsEditMode] = useState(false);
// 创建子组件的ref
const basicSettingsRef = useRef<BasicSettingsRef>(null);
const deviceSelectorRef = useRef<DeviceSelectorRef>(null);
const groupSelectorRef = useRef<GroupSelectorRef>(null);
const contentSelectorRef = useRef<ContentSelectorRef>(null);
useEffect(() => {
if (!id) return;
setIsEditMode(true);
// 加载编辑数据
const loadEditData = async () => {
try {
const res = await fetchGroupPushTaskDetail(id);
const data = res?.data || res;
const config = data?.config || {};
// 回填表单数据(支持接口字段名和数据库字段名的映射)
// 数据库字段groups, friends, trafficPools, contentLibraries, ownerWechatIds, devices
// 接口字段wechatGroups, wechatFriends, trafficPools, contentGroups, ownerWechatIds
const groups = config.groups || config.wechatGroups || [];
const friends = config.friends || config.wechatFriends || [];
const trafficPools = config.trafficPools || config.poolGroups || [];
const contentLibraries = config.contentLibraries || config.contentGroups || [];
const ownerWechatIds = config.ownerWechatIds || config.deviceGroups || [];
const devices = config.devices || [];
setFormData(prev => ({
...prev,
name: data.name || "",
status: data.status ?? config.status ?? config.autoStart ?? 0, // status 和 autoStart 合并为 status
targetType: config.targetType ?? 1,
groupPushSubType: config.groupPushSubType ?? 1,
pushType: config.pushType ?? 0, // 0=定时推送1=立即推送
startTime: config.startTime || "09:00",
endTime: config.endTime || "21:00",
maxPerDay: config.maxPerDay || 20,
pushOrder: config.pushOrder || 1,
isLoop: config.isLoop ?? 0,
isRandomTemplate: config.isRandomTemplate ?? 0,
postPushTags: config.postPushTags || [],
// 支持数据库字段名和接口字段名的映射
deviceGroups: [...ownerWechatIds, ...devices].map((id: any) => String(id)),
wechatGroups: groups.map((id: any) => String(id)),
wechatFriends: friends.map((id: any) => String(id)),
poolGroups: trafficPools.map((id: any) => String(id)),
contentGroups: contentLibraries.map((id: any) => String(id)),
friendIntervalMin: config.friendIntervalMin || 10,
friendIntervalMax: config.friendIntervalMax || 20,
messageIntervalMin: config.messageIntervalMin || 1,
messageIntervalMax: config.messageIntervalMax || 12,
announcementContent: config.announcementContent || "",
enableAiRewrite: config.enableAiRewrite ?? 0,
aiRewritePrompt: config.aiRewritePrompt || "",
socialMediaId: config.socialMediaId || "",
promotionSiteId: config.promotionSiteId || "",
}));
// 回填选项数据(支持多种字段名)
// 设备选项:支持 deviceGroupsOptions, devicesOptions, ownerWechatOptions
if (config.deviceGroupsOptions || config.devicesOptions || config.ownerWechatOptions) {
setDeviceGroupsOptions(
config.deviceGroupsOptions ||
config.devicesOptions ||
config.ownerWechatOptions ||
[]
);
}
// 群组选项:支持 wechatGroupsOptions, groupsOptions
if (config.wechatGroupsOptions || config.groupsOptions) {
setWechatGroupsOptions(config.wechatGroupsOptions || config.groupsOptions || []);
}
// 内容库选项:支持 contentGroupsOptions, contentLibrariesOptions
if (config.contentGroupsOptions || config.contentLibrariesOptions) {
setContentGroupsOptions(config.contentGroupsOptions || config.contentLibrariesOptions || []);
}
// 好友选项:支持 wechatFriendsOptions, friendsOptions
if (config.wechatFriendsOptions || config.friendsOptions) {
setFormData(prev => ({
...prev,
wechatFriendsOptions: config.wechatFriendsOptions || config.friendsOptions || []
}));
}
// 流量池选项:支持 poolGroupsOptions, trafficPoolsOptions
if (config.poolGroupsOptions || config.trafficPoolsOptions) {
setFormData(prev => ({
...prev,
poolGroupsOptions: config.poolGroupsOptions || config.trafficPoolsOptions || []
}));
}
} catch (error) {
console.error("加载编辑数据失败:", error);
Toast.show({ content: "加载数据失败", position: "top" });
}
};
loadEditData();
}, [id]);
const handleBasicSettingsChange = (values: Partial<FormData>) => {
setFormData(prev => ({ ...prev, ...values }));
};
//群组选择
const handleGroupsChange = (data: {
wechatGroups: string[];
wechatGroupsOptions: GroupSelectionItem[];
//设备选择
const handleDevicesChange = (data: {
deviceGroups: string[];
deviceGroupsOptions: DeviceSelectionItem[];
}) => {
setFormData(prev => ({
...prev,
wechatGroups: data.wechatGroups,
deviceGroups: data.deviceGroups,
}));
setWechatGroupsOptions(data.wechatGroupsOptions);
setDeviceGroupsOptions(data.deviceGroupsOptions);
};
//群组选择当targetType=1时或好友/流量池选择当targetType=2时
const handleGroupsChange = (data: {
wechatGroups?: string[];
wechatGroupsOptions?: GroupSelectionItem[];
wechatFriends?: string[];
wechatFriendsOptions?: any[];
poolGroups?: string[];
poolGroupsOptions?: any[];
}) => {
setFormData(prev => ({
...prev,
wechatGroups: data.wechatGroups || [],
wechatFriends: data.wechatFriends || [],
poolGroups: data.poolGroups || [],
wechatFriendsOptions: data.wechatFriendsOptions || [],
poolGroupsOptions: data.poolGroupsOptions || [],
}));
if (data.wechatGroupsOptions) {
setWechatGroupsOptions(data.wechatGroupsOptions);
}
};
//内容库选择
const handleLibrariesChange = (data: {
@@ -83,57 +243,89 @@ const NewGroupPush: React.FC = () => {
const handleSave = async () => {
try {
// 调用 ContentSelector 的表单校验
const isValid = (await contentSelectorRef.current?.validate()) || false;
if (!isValid) return;
// 群公告时不验证内容库选择器(因为步骤被隐藏了)
const isGroupAnnouncement = formData.targetType === 1 && formData.groupPushSubType === 2;
if (!isGroupAnnouncement) {
// 调用 ContentSelector 的表单校验
const isValid = (await contentSelectorRef.current?.validate()) || false;
if (!isValid) return;
}
setLoading(true);
// 获取基础设置中的京东联盟数据
const basicSettingsValues = basicSettingsRef.current?.getValues() || {};
// 构建 API 请求数据
const apiData = {
// 构建 API 请求数据(根据接口文档)
const apiData: any = {
name: formData.name,
startTime: formData.startTime, // 允许推送的开始时间
endTime: formData.endTime, // 允许推送的结束时间
maxPerDay: formData.maxPerDay,
pushOrder: formData.pushOrder,
isLoop: formData.isLoop, // 0: 否, 1: 是
pushType: formData.pushType, // 0: 定时推送, 1: 立即推送
status: formData.status, // 0: 否, 1: 是
wechatGroups: formData.wechatGroups,
contentGroups: formData.contentGroups,
// 京东联盟数据从基础设置中获取
socialMediaId: basicSettingsValues.socialMediaId,
promotionSiteId: basicSettingsValues.promotionSiteId,
pushMode:
formData.pushType === 1
? ("immediate" as const)
: ("scheduled" as const),
messageType: "text" as const,
messageContent: "",
targetTags: [],
pushInterval: 60,
type: 3, // 群发工作台类型固定为3
status: formData.status, // 0: 否, 1: 是(同时作为是否自动启动)
targetType: formData.targetType, // 1=群推送2=好友推送
pushType: formData.pushType ?? 0, // 推送方式0=定时推送1=立即推送
// 设备参数参考自动建群的方式使用deviceGroups
deviceGroups: formData.deviceGroups?.map(id => Number(id)) || [], // 设备ID数组
ownerWechatIds: formData.deviceGroups?.map(id => Number(id)) || [], // 设备ID数组兼容字段
startTime: formData.startTime || "09:00", // 允许推送的开始时间
endTime: formData.endTime || "21:00", // 允许推送的结束时间
maxPerDay: formData.maxPerDay || 0,
pushOrder: formData.pushOrder || 1,
isRandomTemplate: formData.isRandomTemplate || 0,
socialMediaId: basicSettingsValues.socialMediaId || "",
promotionSiteId: basicSettingsValues.promotionSiteId || "",
};
// 打印API请求数据用于调试
console.log("发送到API的数据:", apiData);
// 群推送targetType = 1
if (formData.targetType === 1) {
apiData.groupPushSubType = formData.groupPushSubType || 1;
apiData.wechatGroups = formData.wechatGroups.map(id => Number(id)); // 群ID数组
apiData.isLoop = formData.isLoop || 0; // 群推送和好友推送都有循环推送
// 群推送不打标签不传递postPushTags
// 群推送不传递间隔参数
// 群公告不传递内容库,群群发才传递
if (formData.groupPushSubType !== 2) {
apiData.contentGroups = formData.contentGroups.map(id => Number(id)); // 内容库ID数组
}
// 群公告groupPushSubType = 2
if (formData.groupPushSubType === 2) {
apiData.announcementContent = formData.announcementContent || "";
apiData.enableAiRewrite = formData.enableAiRewrite || 0;
if (formData.enableAiRewrite === 1) {
apiData.aiRewritePrompt = formData.aiRewritePrompt || "";
}
}
} else {
// 好友推送targetType = 2
apiData.wechatFriends = (formData.wechatFriends || []).map(id => Number(id));
apiData.trafficPools = (formData.poolGroups || []).map(id => Number(id)); // 流量池ID数组
apiData.isLoop = formData.isLoop || 0; // 好友推送默认为否0
// 好友推送可以打标签
apiData.postPushTags = formData.postPushTags || [];
// 好友推送需要传递间隔参数
apiData.friendIntervalMin = formData.friendIntervalMin || 10;
apiData.friendIntervalMax = formData.friendIntervalMax || 20;
apiData.messageIntervalMin = formData.messageIntervalMin || 1;
apiData.messageIntervalMax = formData.messageIntervalMax || 12;
// 好友推送需要传递内容库
apiData.contentGroups = formData.contentGroups.map(id => Number(id)); // 内容库ID数组
}
// 更新时需要传递id
if (id) {
apiData.id = Number(id);
}
// 调用创建或更新 API
if (id) {
// 更新逻辑将在这里实现
const { updateGroupPushTask } = await import("./index.api");
await updateGroupPushTask(apiData);
Toast.show({ content: "更新成功", position: "top" });
navigate("/workspace/group-push");
} else {
createGroupPushTask(apiData)
.then(() => {
Toast.show({ content: "创建成功", position: "top" });
navigate("/workspace/group-push");
})
.catch(() => {
Toast.show({ content: "创建失败,请稍后重试", position: "top" });
});
await createGroupPushTask(apiData);
Toast.show({ content: "创建成功", position: "top" });
navigate("/workspace/group-push");
}
} catch (error) {
Toast.show({ content: "保存失败,请稍后重试", position: "top" });
@@ -149,7 +341,7 @@ const NewGroupPush: React.FC = () => {
};
const handleNext = async () => {
if (currentStep < 3) {
if (currentStep < 4) {
try {
let isValid = false;
@@ -167,10 +359,23 @@ const NewGroupPush: React.FC = () => {
break;
case 2:
// 调用 DeviceSelector 的表单校验
isValid = (await deviceSelectorRef.current?.validate()) || false;
if (isValid) {
setCurrentStep(3);
}
break;
case 3:
// 调用 GroupSelector 的表单校验
isValid = (await groupSelectorRef.current?.validate()) || false;
if (isValid) {
setCurrentStep(3);
// 群公告时不显示内容库步骤,直接保存
if (formData.targetType === 1 && formData.groupPushSubType === 2) {
await handleSave();
} else {
setCurrentStep(4);
}
}
break;
@@ -191,15 +396,20 @@ const NewGroupPush: React.FC = () => {
</Button>
)}
{currentStep === 3 ? (
<Button size="large" type="primary" onClick={handleSave}>
</Button>
) : (
<Button size="large" type="primary" onClick={handleNext}>
</Button>
)}
{(() => {
// 群公告时步骤3就是最后一步
const isGroupAnnouncement = formData.targetType === 1 && formData.groupPushSubType === 2;
const isLastStep = isGroupAnnouncement ? currentStep === 3 : currentStep === 4;
return isLastStep ? (
<Button size="large" type="primary" onClick={handleSave}>
</Button>
) : (
<Button size="large" type="primary" onClick={handleNext}>
</Button>
);
})()}
</div>
);
};
@@ -211,7 +421,7 @@ const NewGroupPush: React.FC = () => {
>
<div style={{ padding: 12 }}>
<div style={{ marginBottom: 12 }}>
<StepIndicator currentStep={currentStep} steps={steps} />
<StepIndicator currentStep={currentStep} steps={getSteps(formData.targetType, formData.groupPushSubType)} />
</div>
<div>
{currentStep === 1 && (
@@ -226,6 +436,17 @@ const NewGroupPush: React.FC = () => {
isLoop: formData.isLoop,
status: formData.status,
pushType: formData.pushType,
targetType: formData.targetType,
groupPushSubType: formData.groupPushSubType,
isRandomTemplate: formData.isRandomTemplate,
postPushTags: formData.postPushTags,
friendIntervalMin: formData.friendIntervalMin,
friendIntervalMax: formData.friendIntervalMax,
messageIntervalMin: formData.messageIntervalMin,
messageIntervalMax: formData.messageIntervalMax,
announcementContent: formData.announcementContent,
enableAiRewrite: formData.enableAiRewrite,
aiRewritePrompt: formData.aiRewritePrompt,
}}
onNext={handleBasicSettingsChange}
onSave={handleSave}
@@ -233,18 +454,37 @@ const NewGroupPush: React.FC = () => {
/>
)}
{currentStep === 2 && (
<GroupSelector
ref={groupSelectorRef}
selectedGroups={wechatGroupsOptions}
<DeviceSelector
ref={deviceSelectorRef}
selectedDevices={deviceGroupsOptions}
onPrevious={() => setCurrentStep(1)}
onNext={handleGroupsChange}
onNext={handleDevicesChange}
/>
)}
{currentStep === 3 && (
<GroupSelector
ref={groupSelectorRef}
selectedGroups={wechatGroupsOptions}
targetType={formData.targetType}
selectedFriends={formData.wechatFriendsOptions || []}
selectedPools={formData.poolGroupsOptions || []}
onPrevious={() => setCurrentStep(2)}
onNext={handleGroupsChange}
/>
)}
{currentStep === 4 && formData.targetType === 1 && formData.groupPushSubType !== 2 && (
<ContentSelector
ref={contentSelectorRef}
selectedOptions={contentGroupsOptions}
onPrevious={() => setCurrentStep(2)}
onPrevious={() => setCurrentStep(3)}
onNext={handleLibrariesChange}
/>
)}
{currentStep === 4 && formData.targetType === 2 && (
<ContentSelector
ref={contentSelectorRef}
selectedOptions={contentGroupsOptions}
onPrevious={() => setCurrentStep(3)}
onNext={handleLibrariesChange}
/>
)}

View File

@@ -138,7 +138,12 @@ const GroupPush: React.FC = () => {
};
const handleCreateNew = () => {
navigate("/workspace/group-push/new");
// 直接跳转到群消息推送targetType=1, groupPushSubType=1
const params = new URLSearchParams({
targetType: "1",
groupPushSubType: "1",
});
navigate(`/workspace/group-push/new?${params.toString()}`);
};
const filteredTasks = tasks.filter(task =>
@@ -275,6 +280,7 @@ const GroupPush: React.FC = () => {
)}
</div>
</div>
</Layout>
);
};

View File

@@ -106,3 +106,4 @@ class WorkbenchAutoLikeController extends Controller
}
}

View File

@@ -1896,11 +1896,35 @@ class WorkbenchController extends Controller
throw new \Exception('消息间最小间隔不能大于最大间隔');
}
$contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : [];
$contentGroupsParam = $this->getParamValue($param, 'contentGroups', null);
$contentGroups = $contentGroupsParam !== null
? $this->extractIdList($contentGroupsParam, '内容库参数格式错误')
: $contentGroupsExisting;
// 群公告groupPushSubType = 2contentGroups 可以为空,不需要验证
if ($targetType === 1 && $groupPushSubType === 2) {
// 群公告可以不传内容库,允许为空,不进行任何验证
$contentGroupsParam = $this->getParamValue($param, 'contentGroups', null);
if ($contentGroupsParam !== null) {
// 如果传入了参数,尝试解析,但不验证格式和是否为空
try {
$contentGroups = $this->extractIdList($contentGroupsParam, '内容库参数格式错误');
} catch (\Exception $e) {
// 群公告时忽略格式错误,使用空数组
$contentGroups = [];
}
} else {
// 如果没有传入参数,使用现有配置或空数组
$contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : [];
$contentGroups = $contentGroupsExisting;
}
} else {
// 其他情况,正常处理并验证
$contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : [];
$contentGroupsParam = $this->getParamValue($param, 'contentGroups', null);
$contentGroups = $contentGroupsParam !== null
? $this->extractIdList($contentGroupsParam, '内容库参数格式错误')
: $contentGroupsExisting;
// 其他情况,内容库为必填
if (empty($contentGroups)) {
throw new \Exception('请至少选择一个内容库');
}
}
$data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE);
$postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : [];

View File

@@ -18,454 +18,40 @@ class WorkbenchGroupPushController extends Controller
const TYPE_GROUP_PUSH = 3; // 群消息推送
/**
* 创建工作台
* 获取群发统计数据
* @return \think\response\Json
*/
public function create()
public function getGroupPushStats()
{
if (!$this->request->isPost()) {
return json(['code' => 400, 'msg' => '请求方式错误']);
}
$workbenchId = $this->request->param('workbenchId', 0);
$timeRange = $this->request->param('timeRange', '7'); // 默认最近7天
$contentLibraryIds = $this->request->param('contentLibraryIds', ''); // 话术组筛选
$userId = $this->request->userInfo['id'];
// 获取登录用户信息
$userInfo = request()->userInfo;
// 如果指定了工作台ID则验证权限
if (!empty($workbenchId)) {
$workbench = Workbench::where([
['id', '=', $workbenchId],
['userId', '=', $userId],
['type', '=', self::TYPE_GROUP_PUSH],
['isDel', '=', 0]
])->find();
// 获取请求参数
$param = $this->request->post();
// 根据业务默认值补全参数
if (
isset($param['type']) &&
intval($param['type']) === self::TYPE_GROUP_PUSH
) {
if (empty($param['startTime'])) {
$param['startTime'] = '09:00';
}
if (empty($param['endTime'])) {
$param['endTime'] = '21:00';
if (empty($workbench)) {
return json(['code' => 404, 'msg' => '工作台不存在']);
}
}
// 验证数据
$validate = new WorkbenchValidate;
if (!$validate->scene('create')->check($param)) {
return json(['code' => 400, 'msg' => $validate->getError()]);
}
Db::startTrans();
try {
// 创建工作台基本信息
$workbench = new Workbench;
$workbench->name = $param['name'];
$workbench->type = $param['type'];
$workbench->status = !empty($param['status']) ? 1 : 0;
$workbench->autoStart = !empty($param['autoStart']) ? 1 : 0;
$workbench->userId = $userInfo['id'];
$workbench->companyId = $userInfo['companyId'];
$workbench->createTime = time();
$workbench->updateTime = time();
$workbench->save();
// 根据类型创建对应的配置
switch ($param['type']) {
case self::TYPE_AUTO_LIKE: // 自动点赞
$config = new WorkbenchAutoLike;
$config->workbenchId = $workbench->id;
$config->interval = $param['interval'];
$config->maxLikes = $param['maxLikes'];
$config->startTime = $param['startTime'];
$config->endTime = $param['endTime'];
$config->contentTypes = json_encode($param['contentTypes']);
$config->devices = json_encode($param['deviceGroups']);
$config->friends = json_encode($param['wechatFriends']);
// $config->targetGroups = json_encode($param['targetGroups']);
// $config->tagOperator = $param['tagOperator'];
$config->friendMaxLikes = $param['friendMaxLikes'];
$config->friendTags = $param['friendTags'];
$config->enableFriendTags = $param['enableFriendTags'];
$config->createTime = time();
$config->updateTime = time();
$config->save();
break;
case self::TYPE_MOMENTS_SYNC: // 朋友圈同步
$config = new WorkbenchMomentsSync;
$config->workbenchId = $workbench->id;
$config->syncInterval = $param['syncInterval'];
$config->syncCount = $param['syncCount'];
$config->syncType = $param['syncType'];
$config->startTime = $param['startTime'];
$config->endTime = $param['endTime'];
$config->accountType = $param['accountType'];
$config->devices = json_encode($param['deviceGroups']);
$config->contentLibraries = json_encode($param['contentGroups'] ?? []);
$config->createTime = time();
$config->updateTime = time();
$config->save();
break;
case self::TYPE_GROUP_PUSH: // 群消息推送
$ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? []);
$groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds);
$groupPushData['workbenchId'] = $workbench->id;
$groupPushData['createTime'] = time();
$groupPushData['updateTime'] = time();
$config = new WorkbenchGroupPush;
$config->save($groupPushData);
break;
case self::TYPE_GROUP_CREATE: // 自动建群
$config = new WorkbenchGroupCreate;
$config->workbenchId = $workbench->id;
$config->planType = !empty($param['planType']) ? $param['planType'] : 0;
$config->executorId = !empty($param['executorId']) ? $param['executorId'] : 0;
$config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE);
$config->startTime = $param['startTime'] ?? '';
$config->endTime = $param['endTime'] ?? '';
$config->groupSizeMin = intval($param['groupSizeMin'] ?? 3);
$config->groupSizeMax = intval($param['groupSizeMax'] ?? 38);
$config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20);
$config->groupNameTemplate = $param['groupNameTemplate'] ?? '';
$config->groupDescription = $param['groupDescription'] ?? '';
$config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE);
$config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE);
// 处理群管理员如果启用了群管理员且有指定管理员则保存到admins字段
$admins = [];
if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) {
// 如果groupAdminWechatId是数组取第一个如果是单个值直接使用
$adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId'];
// 如果是好友ID直接添加到admins如果是wechatId需要转换为好友ID
if (is_numeric($adminWechatId)) {
$admins[] = intval($adminWechatId);
} else {
// 如果是wechatId字符串需要查询对应的好友ID
$friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find();
if ($friend) {
$admins[] = intval($friend['id']);
}
}
}
// 如果传入了admins参数优先使用兼容旧逻辑
if (!empty($param['admins']) && is_array($param['admins'])) {
$admins = array_merge($admins, $param['admins']);
}
$config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE);
$config->fixedWechatIds = json_encode($param['fixedWechatIds'] ?? [], JSON_UNESCAPED_UNICODE);
$config->createTime = time();
$config->updateTime = time();
$config->save();
break;
case self::TYPE_TRAFFIC_DISTRIBUTION: // 流量分发
$config = new WorkbenchTrafficConfig;
$config->workbenchId = $workbench->id;
$config->distributeType = $param['distributeType'];
$config->maxPerDay = $param['maxPerDay'];
$config->timeType = $param['timeType'];
$config->startTime = $param['startTime'];
$config->endTime = $param['endTime'];
$config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
$config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE);
$config->account = json_encode($param['accountGroups'], JSON_UNESCAPED_UNICODE);
$config->createTime = time();
$config->updateTime = time();
$config->save();
break;
case self::TYPE_IMPORT_CONTACT: //联系人导入
$config = new WorkbenchImportContact;
$config->workbenchId = $workbench->id;
$config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
$config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE);
$config->num = $param['num'];
$config->clearContact = $param['clearContact'];
$config->remark = $param['remark'];
$config->startTime = $param['startTime'];
$config->endTime = $param['endTime'];
$config->createTime = time();
$config->save();
break;
}
Db::commit();
return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $workbench->id]]);
} catch (\Exception $e) {
Db::rollback();
return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]);
}
}
/**
* 获取工作台列表
* @return \think\response\Json
*/
public function getList()
{
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$type = $this->request->param('type', '');
$keyword = $this->request->param('keyword', '');
// 计算时间范围
$days = intval($timeRange);
$startTime = strtotime(date('Y-m-d 00:00:00', strtotime("-{$days} days")));
$endTime = time();
// 构建查询条件
$where = [
['companyId', '=', $this->request->userInfo['companyId']],
['isDel', '=', 0]
['wgpi.createTime', '>=', $startTime],
['wgpi.createTime', '<=', $endTime]
];
if (empty($this->request->userInfo['isAdmin'])) {
$where[] = ['userId', '=', $this->request->userInfo['id']];
}
// 添加类型筛选
if ($type !== '') {
$where[] = ['type', '=', $type];
}
// 添加名称模糊搜索
if ($keyword !== '') {
$where[] = ['name', 'like', '%' . $keyword . '%'];
}
// 定义关联关系
$with = [
'autoLike' => function ($query) {
$query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends');
},
'momentsSync' => function ($query) {
$query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries');
},
'trafficConfig' => function ($query) {
$query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account');
},
'groupPush' => function ($query) {
$query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt');
},
'groupCreate' => function ($query) {
$query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins');
},
'importContact' => function ($query) {
$query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime');
},
'user' => function ($query) {
$query->field('id,username');
}
];
$list = Workbench::where($where)
->with($with)
->field('id,companyId,name,type,status,autoStart,userId,createTime,updateTime')
->order('id', 'desc')
->page($page, $limit)
->select()
->each(function ($item) {
// 处理配置信息
switch ($item->type) {
case self::TYPE_AUTO_LIKE:
if (!empty($item->autoLike)) {
$item->config = $item->autoLike;
$item->config->devices = json_decode($item->config->devices, true);
$item->config->contentTypes = json_decode($item->config->contentTypes, true);
$item->config->friends = json_decode($item->config->friends, true);
// 添加今日点赞数
$startTime = strtotime(date('Y-m-d') . ' 00:00:00');
$endTime = strtotime(date('Y-m-d') . ' 23:59:59');
$todayLikeCount = Db::name('workbench_auto_like_item')
->where('workbenchId', $item->id)
->whereTime('createTime', 'between', [$startTime, $endTime])
->count();
// 添加总点赞数
$totalLikeCount = Db::name('workbench_auto_like_item')
->where('workbenchId', $item->id)
->count();
$item->config->todayLikeCount = $todayLikeCount;
$item->config->totalLikeCount = $totalLikeCount;
}
unset($item->autoLike, $item->auto_like);
break;
case self::TYPE_MOMENTS_SYNC:
if (!empty($item->momentsSync)) {
$item->config = $item->momentsSync;
$item->config->devices = json_decode($item->config->devices, true);
$item->config->contentGroups = json_decode($item->config->contentLibraries, true);
//同步记录
$sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->count();
$item->syncCount = $sendNum;
$lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->order('id DESC')->value('createTime');
$item->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--';
// 获取内容库名称
if (!empty($item->config->contentGroups)) {
$libraryNames = ContentLibrary::where('id', 'in', $item->config->contentGroups)->select();
$item->config->contentGroupsOptions = $libraryNames;
} else {
$item->config->contentGroupsOptions = [];
}
}
unset($item->momentsSync, $item->moments_sync, $item->config->contentLibraries);
break;
case self::TYPE_GROUP_PUSH:
if (!empty($item->groupPush)) {
$item->config = $item->groupPush;
$item->config->pushType = $item->config->pushType;
$item->config->targetType = isset($item->config->targetType) ? intval($item->config->targetType) : 1; // 默认1=群推送
$item->config->groupPushSubType = isset($item->config->groupPushSubType) ? intval($item->config->groupPushSubType) : 1; // 默认1=群群发
$item->config->startTime = $item->config->startTime;
$item->config->endTime = $item->config->endTime;
$item->config->maxPerDay = $item->config->maxPerDay;
$item->config->pushOrder = $item->config->pushOrder;
$item->config->isLoop = $item->config->isLoop;
$item->config->status = $item->config->status;
$item->config->ownerWechatIds = json_decode($item->config->ownerWechatIds ?? '[]', true) ?: [];
// 根据targetType解析不同的数据
if ($item->config->targetType == 1) {
// 群推送
$item->config->wechatGroups = json_decode($item->config->groups, true) ?: [];
$item->config->wechatFriends = [];
// 群推送不需要devices字段
// 群公告相关字段
if ($item->config->groupPushSubType == 2) {
$item->config->announcementContent = isset($item->config->announcementContent) ? $item->config->announcementContent : '';
$item->config->enableAiRewrite = isset($item->config->enableAiRewrite) ? intval($item->config->enableAiRewrite) : 0;
$item->config->aiRewritePrompt = isset($item->config->aiRewritePrompt) ? $item->config->aiRewritePrompt : '';
}
$item->config->trafficPools = [];
} else {
// 好友推送
$item->config->wechatFriends = json_decode($item->config->friends, true) ?: [];
$item->config->wechatGroups = [];
$item->config->trafficPools = json_decode($item->config->trafficPools ?? '[]', true) ?: [];
}
$item->config->contentLibraries = json_decode($item->config->contentLibraries, true);
$item->config->postPushTags = json_decode($item->config->postPushTags ?? '[]', true) ?: [];
$item->config->lastPushTime = '';
if (!empty($item->config->ownerWechatIds)) {
$ownerWechatOptions = Db::name('wechat_account')
->whereIn('id', $item->config->ownerWechatIds)
->field('id,wechatId,nickName,avatar,alias')
->select();
$item->config->ownerWechatOptions = $ownerWechatOptions;
} else {
$item->config->ownerWechatOptions = [];
}
}
unset($item->groupPush, $item->group_push);
break;
case self::TYPE_GROUP_CREATE:
if (!empty($item->groupCreate)) {
$item->config = $item->groupCreate;
$item->config->devices = json_decode($item->config->devices, true);
$item->config->poolGroups = json_decode($item->config->poolGroups, true);
$item->config->wechatGroups = json_decode($item->config->wechatGroups, true);
$item->config->admins = json_decode($item->config->admins ?? '[]', true) ?: [];
// 处理群管理员相关字段
$item->config->groupAdminEnabled = !empty($item->config->admins) ? 1 : 0;
if (!empty($item->config->admins)) {
$adminOptions = Db::table('s2_wechat_friend')->alias('wf')
->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
->where('wf.id', 'in', $item->config->admins)
->order('wf.id', 'desc')
->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
->select();
$item->config->adminsOptions = $adminOptions;
// 如果有管理员设置groupAdminWechatId为第一个管理员的ID用于前端回显
$item->config->groupAdminWechatId = !empty($item->config->admins) ? $item->config->admins[0] : null;
} else {
$item->config->adminsOptions = [];
$item->config->groupAdminWechatId = null;
}
}
unset($item->groupCreate, $item->group_create);
break;
case self::TYPE_TRAFFIC_DISTRIBUTION:
if (!empty($item->trafficConfig)) {
$item->config = $item->trafficConfig;
$item->config->devices = json_decode($item->config->devices, true);
$item->config->poolGroups = json_decode($item->config->pools, true);
$item->config->account = json_decode($item->config->account, true);
$config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find();
$item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
//统计
$labels = $item->config->poolGroups;
$totalUsers = Db::table('s2_wechat_friend')->alias('wf')
->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
->where([
['wf.isDeleted', '=', 0],
['sa.departmentId', '=', $item->companyId]
])
->whereIn('wa.currentDeviceId', $item->config->devices);
if (!empty($labels) && count($labels) > 0) {
$totalUsers = $totalUsers->where(function ($q) use ($labels) {
foreach ($labels as $label) {
$q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')");
}
});
}
$totalUsers = $totalUsers->count();
$totalAccounts = count($item->config->account);
$dailyAverage = Db::name('workbench_traffic_config_item')
->where('workbenchId', $item->id)
->count();
$day = (time() - strtotime($item->createTime)) / 86400;
$day = intval($day);
if ($dailyAverage > 0 && $totalAccounts > 0 && $day > 0) {
$dailyAverage = $dailyAverage / $totalAccounts / $day;
}
$item->config->total = [
'dailyAverage' => intval($dailyAverage),
'totalAccounts' => $totalAccounts,
'deviceCount' => count($item->config->devices),
'poolCount' => !empty($item->config->poolGroups) ? count($item->config->poolGroups) : 'ALL',
'totalUsers' => $totalUsers >> 0
];
}
unset($item->trafficConfig, $item->traffic_config);
break;
case self::TYPE_IMPORT_CONTACT:
if (!empty($item->importContact)) {
$item->config = $item->importContact;
$item->config->devices = json_decode($item->config->devices, true);
$item->config->poolGroups = json_decode($item->config->pools, true);
}
unset($item->importContact, $item->import_contact);
break;
}
// 添加创建人名称
$item['creatorName'] = $item->user ? $item->user->username : '';
unset($item['user']); // 移除关联数据
return $item;
});
$total = Workbench::where($where)->count();
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'list' => $list,
'total' => $total,
'page' => $page,
'limit' => $limit
]
]);
}
/**
* 获取工作台详情
* @param int $id 工作台ID
* @return \think\response\Json
*/
public function detail()
{
$id = $this->request->param('id', '');
@@ -1001,7 +587,7 @@ class WorkbenchGroupPushController extends Controller
case self::TYPE_GROUP_PUSH:
$config = WorkbenchGroupPush::where('workbenchId', $param['id'])->find();
if ($config) {
$ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config);
$ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config, $param['deviceGroups'] ?? []);
$groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds, $config);
$groupPushData['updateTime'] = time();
$config->save($groupPushData);
@@ -1899,15 +1485,24 @@ class WorkbenchGroupPushController extends Controller
* 规范化客服微信ID列表
* @param mixed $ownerWechatIds
* @param WorkbenchGroupPush|null $originalConfig
* @param array $deviceGroups 设备ID数组
* @return array
* @throws \Exception
*/
private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array
private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null, array $deviceGroups = []): array
{
// 处理设备ID数组
$deviceGroupsList = $this->extractIdList($deviceGroups, '设备参数格式错误');
if ($ownerWechatIds === null) {
$existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : [];
if (empty($existing)) {
throw new \Exception('请至少选择一个客服微信');
// 如果原有配置为空且没有传设备ID则报错
if (empty($existing) && empty($deviceGroupsList)) {
throw new \Exception('请至少选择一个客服微信或设备');
}
// 如果原有配置为空但有设备ID返回空数组允许使用设备ID
if (empty($existing) && !empty($deviceGroupsList)) {
return [];
}
return $existing;
}
@@ -1917,8 +1512,9 @@ class WorkbenchGroupPushController extends Controller
}
$normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误');
if (empty($normalized)) {
throw new \Exception('请至少选择一个客服微信');
// 如果 ownerWechatIds 为空但传了设备ID则允许两个传一个即可
if (empty($normalized) && empty($deviceGroupsList)) {
throw new \Exception('请至少选择一个客服微信或设备');
}
return $normalized;
}
@@ -1960,6 +1556,14 @@ class WorkbenchGroupPushController extends Controller
'isRandomTemplate' => $this->toBoolInt($this->getParamValue($param, 'isRandomTemplate', $originalConfig->isRandomTemplate ?? 0)),
'ownerWechatIds' => json_encode($ownerWechatIds, JSON_UNESCAPED_UNICODE),
];
// 处理设备ID数组deviceGroups保存到 devices 字段
$deviceGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->devices ?? []) : [];
$deviceGroupsParam = $this->getParamValue($param, 'deviceGroups', null);
$deviceGroups = $deviceGroupsParam !== null
? $this->extractIdList($deviceGroupsParam, '设备参数格式错误')
: $deviceGroupsExisting;
$data['devices'] = json_encode($deviceGroups, JSON_UNESCAPED_UNICODE);
if ($data['friendIntervalMin'] > $data['friendIntervalMax']) {
throw new \Exception('目标间最小间隔不能大于最大间隔');
@@ -1968,11 +1572,35 @@ class WorkbenchGroupPushController extends Controller
throw new \Exception('消息间最小间隔不能大于最大间隔');
}
$contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : [];
$contentGroupsParam = $this->getParamValue($param, 'contentGroups', null);
$contentGroups = $contentGroupsParam !== null
? $this->extractIdList($contentGroupsParam, '内容库参数格式错误')
: $contentGroupsExisting;
// 群公告groupPushSubType = 2contentGroups 可以为空,不需要验证
if ($targetType === 1 && $groupPushSubType === 2) {
// 群公告可以不传内容库,允许为空,不进行任何验证
$contentGroupsParam = $this->getParamValue($param, 'contentGroups', null);
if ($contentGroupsParam !== null) {
// 如果传入了参数,尝试解析,但不验证格式和是否为空
try {
$contentGroups = $this->extractIdList($contentGroupsParam, '内容库参数格式错误');
} catch (\Exception $e) {
// 群公告时忽略格式错误,使用空数组
$contentGroups = [];
}
} else {
// 如果没有传入参数,使用现有配置或空数组
$contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : [];
$contentGroups = $contentGroupsExisting;
}
} else {
// 其他情况,正常处理并验证
$contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : [];
$contentGroupsParam = $this->getParamValue($param, 'contentGroups', null);
$contentGroups = $contentGroupsParam !== null
? $this->extractIdList($contentGroupsParam, '内容库参数格式错误')
: $contentGroupsExisting;
// 其他情况,内容库为必填
if (empty($contentGroups)) {
throw new \Exception('请至少选择一个内容库');
}
}
$data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE);
$postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : [];
@@ -2150,64 +1778,6 @@ class WorkbenchGroupPushController extends Controller
return false;
}
/**
* 获取通讯录导入记录列表
* @return \think\response\Json
*/
public function getImportContact()
{
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$workbenchId = $this->request->param('workbenchId', 0);
$where = [
['wici.workbenchId', '=', $workbenchId]
];
// 查询发布记录
$list = Db::name('workbench_import_contact_item')->alias('wici')
->join('traffic_pool tp', 'tp.id = wici.poolId', 'left')
->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left')
->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left')
->field([
'wici.id',
'wici.workbenchId',
'wici.createTime',
'tp.identifier',
'tp.mobile',
'tp.wechatId',
'tc.name',
'wa.nickName',
'wa.avatar',
'wa.alias',
])
->where($where)
->order('tc.name DESC,wici.createTime DESC')
->group('tp.identifier')
->page($page, $limit)
->select();
foreach ($list as &$item) {
$item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
}
// 获取总记录数
$total = Db::name('workbench_import_contact_item')->alias('wici')
->where($where)
->count();
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'list' => $list,
'total' => $total,
]
]);
}
/**
* 获取群发统计数据
* @return \think\response\Json
@@ -3116,15 +2686,302 @@ class WorkbenchGroupPushController extends Controller
}
/**
* 获取已创建的群列表(自动建群)
* @return \think\response\Json
* 规范化客服微信ID列表
* @param mixed $ownerWechatIds
* @param WorkbenchGroupPush|null $originalConfig
* @param array $deviceGroups 设备ID数组
* @return array
* @throws \Exception
*/
public function getCreatedGroupsList()
private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null, array $deviceGroups = []): array
{
$workbenchId = $this->request->param('workbenchId', 0);
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 100);
$keyword = $this->request->param('keyword', '');
// 处理设备ID数组
$deviceGroupsList = $this->extractIdList($deviceGroups, '设备参数格式错误');
if ($ownerWechatIds === null) {
$existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : [];
// 如果原有配置为空且没有传设备ID则报错
if (empty($existing) && empty($deviceGroupsList)) {
throw new \Exception('请至少选择一个客服微信或设备');
}
// 如果原有配置为空但有设备ID返回空数组允许使用设备ID
if (empty($existing) && !empty($deviceGroupsList)) {
return [];
}
return $existing;
}
if (!is_array($ownerWechatIds)) {
throw new \Exception('客服参数格式错误');
}
$normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误');
// 如果 ownerWechatIds 为空但传了设备ID则允许两个传一个即可
if (empty($normalized) && empty($deviceGroupsList)) {
throw new \Exception('请至少选择一个客服微信或设备');
}
return $normalized;
}
/**
* 构建群推送配置数据
* @param array $param
* @param array $ownerWechatIds
* @param WorkbenchGroupPush|null $originalConfig
* @return array
* @throws \Exception
*/
private function prepareGroupPushData(array $param, array $ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array
{
$targetTypeDefault = $originalConfig ? intval($originalConfig->targetType) : 1;
$targetType = intval($this->getParamValue($param, 'targetType', $targetTypeDefault)) ?: 1;
$groupPushSubTypeDefault = $originalConfig ? intval($originalConfig->groupPushSubType) : 1;
$groupPushSubType = intval($this->getParamValue($param, 'groupPushSubType', $groupPushSubTypeDefault)) ?: 1;
if (!in_array($groupPushSubType, [1, 2], true)) {
$groupPushSubType = 1;
}
$data = [
'pushType' => $this->toBoolInt($this->getParamValue($param, 'pushType', $originalConfig->pushType ?? 0)),
'targetType' => $targetType,
'startTime' => $this->getParamValue($param, 'startTime', $originalConfig->startTime ?? ''),
'endTime' => $this->getParamValue($param, 'endTime', $originalConfig->endTime ?? ''),
'maxPerDay' => intval($this->getParamValue($param, 'maxPerDay', $originalConfig->maxPerDay ?? 0)),
'pushOrder' => $this->getParamValue($param, 'pushOrder', $originalConfig->pushOrder ?? 1),
'groupPushSubType' => $groupPushSubType,
'status' => $this->toBoolInt($this->getParamValue($param, 'status', $originalConfig->status ?? 0)),
'socialMediaId' => $this->getParamValue($param, 'socialMediaId', $originalConfig->socialMediaId ?? ''),
'promotionSiteId' => $this->getParamValue($param, 'promotionSiteId', $originalConfig->promotionSiteId ?? ''),
'friendIntervalMin' => intval($this->getParamValue($param, 'friendIntervalMin', $originalConfig->friendIntervalMin ?? 10)),
'friendIntervalMax' => intval($this->getParamValue($param, 'friendIntervalMax', $originalConfig->friendIntervalMax ?? 20)),
'messageIntervalMin' => intval($this->getParamValue($param, 'messageIntervalMin', $originalConfig->messageIntervalMin ?? 1)),
'messageIntervalMax' => intval($this->getParamValue($param, 'messageIntervalMax', $originalConfig->messageIntervalMax ?? 12)),
'isRandomTemplate' => $this->toBoolInt($this->getParamValue($param, 'isRandomTemplate', $originalConfig->isRandomTemplate ?? 0)),
'ownerWechatIds' => json_encode($ownerWechatIds, JSON_UNESCAPED_UNICODE),
];
// 处理设备ID数组deviceGroups保存到 devices 字段
$deviceGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->devices ?? []) : [];
$deviceGroupsParam = $this->getParamValue($param, 'deviceGroups', null);
$deviceGroups = $deviceGroupsParam !== null
? $this->extractIdList($deviceGroupsParam, '设备参数格式错误')
: $deviceGroupsExisting;
$data['devices'] = json_encode($deviceGroups, JSON_UNESCAPED_UNICODE);
if ($data['friendIntervalMin'] > $data['friendIntervalMax']) {
throw new \Exception('目标间最小间隔不能大于最大间隔');
}
if ($data['messageIntervalMin'] > $data['messageIntervalMax']) {
throw new \Exception('消息间最小间隔不能大于最大间隔');
}
// 群公告groupPushSubType = 2contentGroups 可以为空,不需要验证
if ($targetType === 1 && $groupPushSubType === 2) {
// 群公告可以不传内容库,允许为空,不进行任何验证
$contentGroupsParam = $this->getParamValue($param, 'contentGroups', null);
if ($contentGroupsParam !== null) {
// 如果传入了参数,尝试解析,但不验证格式和是否为空
try {
$contentGroups = $this->extractIdList($contentGroupsParam, '内容库参数格式错误');
} catch (\Exception $e) {
// 群公告时忽略格式错误,使用空数组
$contentGroups = [];
}
} else {
// 如果没有传入参数,使用现有配置或空数组
$contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : [];
$contentGroups = $contentGroupsExisting;
}
} else {
// 其他情况,正常处理并验证
$contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : [];
$contentGroupsParam = $this->getParamValue($param, 'contentGroups', null);
$contentGroups = $contentGroupsParam !== null
? $this->extractIdList($contentGroupsParam, '内容库参数格式错误')
: $contentGroupsExisting;
// 其他情况,内容库为必填
if (empty($contentGroups)) {
throw new \Exception('请至少选择一个内容库');
}
}
$data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE);
$postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : [];
$postPushTagsParam = $this->getParamValue($param, 'postPushTags', null);
$postPushTags = $postPushTagsParam !== null
? $this->extractIdList($postPushTagsParam, '推送标签参数格式错误')
: $postPushTagsExisting;
$data['postPushTags'] = json_encode($postPushTags, JSON_UNESCAPED_UNICODE);
if ($targetType === 1) {
$data['isLoop'] = $this->toBoolInt($this->getParamValue($param, 'isLoop', $originalConfig->isLoop ?? 0));
$groupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->groups ?? []) : [];
$wechatGroups = array_key_exists('wechatGroups', $param)
? $this->extractIdList($param['wechatGroups'], '群参数格式错误')
: $groupsExisting;
if (empty($wechatGroups)) {
throw new \Exception('群推送必须选择微信群');
}
$data['groups'] = json_encode($wechatGroups, JSON_UNESCAPED_UNICODE);
$data['friends'] = json_encode([], JSON_UNESCAPED_UNICODE);
$data['trafficPools'] = json_encode([], JSON_UNESCAPED_UNICODE);
if ($groupPushSubType === 2) {
$announcementContent = $this->getParamValue($param, 'announcementContent', $originalConfig->announcementContent ?? '');
if (empty($announcementContent)) {
throw new \Exception('群公告必须输入公告内容');
}
$enableAiRewrite = $this->toBoolInt($this->getParamValue($param, 'enableAiRewrite', $originalConfig->enableAiRewrite ?? 0));
$aiRewritePrompt = trim((string)$this->getParamValue($param, 'aiRewritePrompt', $originalConfig->aiRewritePrompt ?? ''));
if ($enableAiRewrite === 1 && $aiRewritePrompt === '') {
throw new \Exception('启用AI智能话术改写时必须输入改写提示词');
}
$data['announcementContent'] = $announcementContent;
$data['enableAiRewrite'] = $enableAiRewrite;
$data['aiRewritePrompt'] = $aiRewritePrompt;
} else {
$data['groupPushSubType'] = 1;
$data['announcementContent'] = '';
$data['enableAiRewrite'] = 0;
$data['aiRewritePrompt'] = '';
}
} else {
$data['isLoop'] = 0;
$friendsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->friends ?? []) : [];
$trafficPoolsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->trafficPools ?? []) : [];
$friendTargets = array_key_exists('wechatFriends', $param)
? $this->extractIdList($param['wechatFriends'], '好友参数格式错误')
: $friendsExisting;
$trafficPools = array_key_exists('trafficPools', $param)
? $this->extractIdList($param['trafficPools'], '流量池参数格式错误')
: $trafficPoolsExisting;
if (empty($friendTargets) && empty($trafficPools)) {
throw new \Exception('好友推送需至少选择好友或流量池');
}
$data['friends'] = json_encode($friendTargets, JSON_UNESCAPED_UNICODE);
$data['trafficPools'] = json_encode($trafficPools, JSON_UNESCAPED_UNICODE);
$data['groups'] = json_encode([], JSON_UNESCAPED_UNICODE);
$data['groupPushSubType'] = 1;
$data['announcementContent'] = '';
$data['enableAiRewrite'] = 0;
$data['aiRewritePrompt'] = '';
}
return $data;
}
/**
* 获取参数值,若不存在则返回默认值
* @param array $param
* @param string $key
* @param mixed $default
* @return mixed
*/
private function getParamValue(array $param, string $key, $default)
{
return array_key_exists($key, $param) ? $param[$key] : $default;
}
/**
* 将值转换为整型布尔
* @param mixed $value
* @return int
*/
private function toBoolInt($value): int
{
return empty($value) ? 0 : 1;
}
/**
* 从参数中提取ID列表
* @param mixed $items
* @param string $errorMessage
* @return array
* @throws \Exception
*/
private function extractIdList($items, string $errorMessage = '参数格式错误'): array
{
if (!is_array($items)) {
throw new \Exception($errorMessage);
}
$ids = [];
foreach ($items as $item) {
if (is_array($item) && isset($item['id'])) {
$item = $item['id'];
}
if ($item === '' || $item === null) {
continue;
}
$ids[] = $item;
}
return array_values(array_unique($ids));
}
/**
* 解码JSON数组
* @param mixed $value
* @return array
*/
private function decodeJsonArray($value): array
{
if (empty($value)) {
return [];
}
if (is_array($value)) {
return $value;
}
$decoded = json_decode($value, true);
return is_array($decoded) ? $decoded : [];
}
/**
* 验证内容是否包含链接
* @param string $content 要检测的内容
* @return bool
*/
private function containsLink($content)
{
// 定义各种链接的正则表达式模式
$patterns = [
// HTTP/HTTPS链接
'/https?:\/\/[^\s]+/i',
// 京东商品链接
'/item\.jd\.com\/\d+/i',
// 京东短链接
'/u\.jd\.com\/[a-zA-Z0-9]+/i',
// 淘宝商品链接
'/item\.taobao\.com\/item\.htm\?id=\d+/i',
// 天猫商品链接
'/detail\.tmall\.com\/item\.htm\?id=\d+/i',
// 淘宝短链接
'/m\.tb\.cn\/[a-zA-Z0-9]+/i',
// 拼多多链接
'/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i',
// 苏宁易购链接
'/product\.suning\.com\/\d+\/\d+\.html/i',
// 通用域名模式(包含常见电商域名)
'/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i',
// 通用短链接模式
'/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i'
];
// 遍历所有模式进行匹配
foreach ($patterns as $pattern) {
if (preg_match($pattern, $content)) {
return true;
}
}
return false;
}
}
if (empty($workbenchId)) {
return json(['code' => 400, 'msg' => '工作台ID不能为空']);
@@ -3247,12 +3104,7 @@ class WorkbenchGroupPushController extends Controller
]
]);
}
/**
* 获取已创建群的详情(自动建群)
* @return \think\response\Json
*/
public function getCreatedGroupDetail()
}
{
$workbenchId = $this->request->param('workbenchId', 0);
$groupId = $this->request->param('groupId', 0);

View File

@@ -311,3 +311,4 @@ class WorkbenchHelperController extends Controller
}
}

View File

@@ -67,3 +67,4 @@ class WorkbenchImportContactController extends Controller
}
}

View File

@@ -123,3 +123,4 @@ class WorkbenchMomentsController extends Controller
}
}

View File

@@ -307,3 +307,4 @@ class WorkbenchTrafficController extends Controller
}
}

View File

@@ -47,7 +47,7 @@ class Workbench extends Validate
'wechatGroups' => 'checkGroupPushTarget|array|min:1', // 当targetType=1时必填
'wechatFriends' => 'checkFriendPushTarget|array', // 当targetType=2时可选可以为空
'ownerWechatId' => 'checkFriendPushService', // 当targetType=2且未选择好友/流量池时必填
'contentGroups' => 'requireIf:type,3|array|min:1',
'contentGroups' => 'checkContentGroups|array', // 群推送时必填,但群公告时可以为空
// 群公告特有参数
'announcementContent' => 'checkAnnouncementContent|max:5000', // 群公告内容当groupPushSubType=2时必填
'enableAiRewrite' => 'checkEnableAiRewrite|in:0,1', // 是否启用AI智能话术改写
@@ -106,7 +106,7 @@ class Workbench extends Validate
'endTime.dateFormat' => '发布结束时间格式错误',
'accountGroups.requireIf' => '请选择账号类型',
'accountGroups.in' => '账号类型错误',
'contentGroups.requireIf' => '选择内容库',
'contentGroups.checkContentGroups' => '群群发时必须选择内容库',
'contentGroups.array' => '内容库格式错误',
// 群消息推送相关提示
'pushType.requireIf' => '请选择推送方式',
@@ -383,4 +383,31 @@ class Workbench extends Validate
}
return true;
}
/**
* 验证内容库(群推送时必填,但群公告时可以为空)
*/
protected function checkContentGroups($value, $rule, $data)
{
// 如果是群消息推送类型
if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) {
$targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1
$groupPushSubType = isset($data['groupPushSubType']) ? intval($data['groupPushSubType']) : 1; // 默认1
// 群公告groupPushSubType=2内容库可以为空不需要验证
if ($targetType == 1 && $groupPushSubType == 2) {
// 群公告时允许为空,不进行验证
return true;
}
// 其他情况(群群发、好友推送),内容库必填
if (!isset($value) || $value === null || $value === '') {
return false;
}
if (!is_array($value) || count($value) < 1) {
return false;
}
}
return true;
}
}

View File

@@ -6,7 +6,7 @@
- **接口用途**:供第三方系统向【存客宝】上报客户线索(手机号 / 微信号等),用于后续的跟进、标签管理和画像分析。
- **接口协议**HTTP
- **请求方式**`POST`
- **请求地址** `http://ckbapi.quwanzhi.com/v1/api/scenarios`
- **请求地址** `https://ckbapi.quwanzhi.com/v1/api/scenarios`
> 具体 URL 以实际环境配置为准。