分销功能提交
This commit is contained in:
@@ -4,7 +4,7 @@ import { ArrowLeftOutlined } from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { getSafeAreaHeight } from "@/utils/common";
|
||||
interface NavCommonProps {
|
||||
title: string;
|
||||
title: string | React.ReactNode;
|
||||
backFn?: () => void;
|
||||
right?: React.ReactNode;
|
||||
left?: React.ReactNode;
|
||||
|
||||
@@ -709,7 +709,7 @@
|
||||
.adm-avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
@@ -723,13 +723,13 @@
|
||||
}
|
||||
|
||||
.friend-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.friend-name {
|
||||
.friend-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
@@ -754,7 +754,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.friend-info-item {
|
||||
font-size: 13px;
|
||||
@@ -764,7 +764,7 @@
|
||||
gap: 4px;
|
||||
|
||||
.info-label {
|
||||
color: #999;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -775,11 +775,11 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.friend-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -866,11 +866,11 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.popup-footer {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
.popup-footer {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.friend-info-card {
|
||||
display: flex;
|
||||
@@ -1441,22 +1441,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
.moments-action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.moments-action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.action-button, .action-button-dark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.action-button, .action-button-dark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: #1677ff;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: #1677ff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
@@ -1465,22 +1465,22 @@
|
||||
&:active {
|
||||
background: #0958d9;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button-dark {
|
||||
.action-button-dark {
|
||||
background: #1677ff;
|
||||
color: white;
|
||||
|
||||
&:active {
|
||||
background: #0958d9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moments-content {
|
||||
|
||||
@@ -30,6 +30,20 @@ export interface FormData {
|
||||
wechatGroups: string[];
|
||||
wechatGroupsOptions: GroupSelectionItem[];
|
||||
messagePlans: any[];
|
||||
// 分销相关
|
||||
distributionEnabled?: boolean;
|
||||
// 选中的分销渠道ID列表(前端使用,提交时转为 distributionChannels)
|
||||
distributionChannelIds?: Array<string | number>;
|
||||
// 选中的分销渠道选项(用于回显名称)
|
||||
distributionChannelsOptions?: Array<{
|
||||
id: string | number;
|
||||
name: string;
|
||||
code?: string;
|
||||
}>;
|
||||
// 获客奖励金额(元,前端使用,提交时转为 customerRewardAmount)
|
||||
distributionCustomerReward?: number;
|
||||
// 添加奖励金额(元,前端使用,提交时转为 addFriendRewardAmount)
|
||||
distributionAddReward?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
export const defFormData: FormData = {
|
||||
@@ -56,4 +70,9 @@ export const defFormData: FormData = {
|
||||
wechatGroupsOptions: [],
|
||||
contentGroups: [],
|
||||
contentGroupsOptions: [],
|
||||
distributionEnabled: false,
|
||||
distributionChannelIds: [],
|
||||
distributionChannelsOptions: [],
|
||||
distributionCustomerReward: undefined,
|
||||
distributionAddReward: undefined,
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
updatePlan,
|
||||
} from "./index.api";
|
||||
import { FormData, defFormData, steps } from "./index.data";
|
||||
import { fetchChannelList } from "@/pages/mobile/workspace/distribution-management/api";
|
||||
|
||||
export default function NewPlan() {
|
||||
const router = useNavigate();
|
||||
@@ -49,6 +50,42 @@ export default function NewPlan() {
|
||||
//获取计划详情
|
||||
|
||||
const detail = await getPlanDetail(planId);
|
||||
|
||||
// 处理分销相关数据回填
|
||||
const distributionChannels = detail.distributionChannels || [];
|
||||
let distributionChannelsOptions: Array<{ id: string | number; name: string; code?: string }> = [];
|
||||
|
||||
if (distributionChannels.length > 0) {
|
||||
// 判断 distributionChannels 是对象数组还是ID数组
|
||||
const isObjectArray = distributionChannels.some((item: any) => typeof item === 'object' && item !== null);
|
||||
|
||||
if (isObjectArray) {
|
||||
// 如果已经是对象数组,直接使用(包含 id, code, name)
|
||||
distributionChannelsOptions = distributionChannels.map((channel: any) => ({
|
||||
id: channel.id,
|
||||
name: channel.name || `渠道${channel.id}`,
|
||||
code: channel.code,
|
||||
}));
|
||||
} else {
|
||||
// 如果是ID数组,需要查询渠道信息
|
||||
try {
|
||||
const channelRes = await fetchChannelList({ page: 1, limit: 200, status: "enabled" });
|
||||
distributionChannelsOptions = distributionChannels.map((channelId: number) => {
|
||||
const channel = channelRes.list.find((c: any) => c.id === channelId);
|
||||
return channel
|
||||
? { id: channelId, name: channel.name, code: channel.code }
|
||||
: { id: channelId, name: `渠道${channelId}` };
|
||||
});
|
||||
} catch {
|
||||
// 如果获取渠道信息失败,使用默认名称
|
||||
distributionChannelsOptions = distributionChannels.map((channelId: number) => ({
|
||||
id: channelId,
|
||||
name: `渠道${channelId}`,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
name: detail.name ?? "",
|
||||
@@ -76,6 +113,12 @@ export default function NewPlan() {
|
||||
contentGroupsOptions: detail.contentGroupsOptions ?? [],
|
||||
status: detail.status ?? 0,
|
||||
messagePlans: detail.messagePlans ?? [],
|
||||
// 分销相关数据回填
|
||||
distributionEnabled: detail.distributionEnabled ?? false,
|
||||
distributionChannelIds: distributionChannelsOptions.map(item => item.id),
|
||||
distributionChannelsOptions: distributionChannelsOptions,
|
||||
distributionCustomerReward: detail.customerRewardAmount,
|
||||
distributionAddReward: detail.addFriendRewardAmount,
|
||||
}));
|
||||
} else {
|
||||
if (scenarioId) {
|
||||
@@ -118,21 +161,45 @@ export default function NewPlan() {
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
// 构建提交数据,转换分销相关字段为接口需要的格式
|
||||
const submitData: any = {
|
||||
...formData,
|
||||
sceneId: Number(formData.scenario),
|
||||
};
|
||||
|
||||
// 转换分销相关字段为接口需要的格式
|
||||
if (formData.distributionEnabled) {
|
||||
submitData.distributionEnabled = true;
|
||||
// 转换渠道ID数组,确保都是数字类型
|
||||
submitData.distributionChannels = (formData.distributionChannelIds || []).map(id =>
|
||||
typeof id === 'string' ? Number(id) : id
|
||||
);
|
||||
// 转换奖励金额,确保是浮点数,最多2位小数
|
||||
submitData.customerRewardAmount = formData.distributionCustomerReward
|
||||
? Number(Number(formData.distributionCustomerReward).toFixed(2))
|
||||
: 0;
|
||||
submitData.addFriendRewardAmount = formData.distributionAddReward
|
||||
? Number(Number(formData.distributionAddReward).toFixed(2))
|
||||
: 0;
|
||||
} else {
|
||||
// 如果未开启分销,设置为false
|
||||
submitData.distributionEnabled = false;
|
||||
}
|
||||
|
||||
// 移除前端使用的字段,避免提交到后端
|
||||
delete submitData.distributionChannelIds;
|
||||
delete submitData.distributionChannelsOptions;
|
||||
delete submitData.distributionCustomerReward;
|
||||
delete submitData.distributionAddReward;
|
||||
|
||||
if (isEdit && planId) {
|
||||
// 编辑:拼接后端需要的完整参数
|
||||
const editData = {
|
||||
...formData,
|
||||
...{ sceneId: Number(formData.scenario) },
|
||||
id: Number(planId),
|
||||
planId: Number(planId),
|
||||
// 兼容后端需要的字段
|
||||
// 你可以根据实际需要补充其它字段
|
||||
};
|
||||
await updatePlan(editData);
|
||||
submitData.id = Number(planId);
|
||||
submitData.planId = Number(planId);
|
||||
await updatePlan(submitData);
|
||||
} else {
|
||||
// 新建
|
||||
formData.sceneId = Number(formData.scenario);
|
||||
await createPlan(formData);
|
||||
await createPlan(submitData);
|
||||
}
|
||||
message.success(isEdit ? "计划已更新" : "获客计划已创建");
|
||||
const sceneItem = sceneList.find(v => formData.scenario === v.id);
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Input, Button, Tag, Switch, Modal, Spin } from "antd";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { Input, Button, Tag, Switch, Spin, message, Modal } from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
CloseOutlined,
|
||||
DownloadOutlined,
|
||||
SearchOutlined,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Checkbox, Popup } from "antd-mobile";
|
||||
import { uploadFile } from "@/api/common";
|
||||
import styles from "./base.module.scss";
|
||||
import { fetchChannelList } from "@/pages/mobile/workspace/distribution-management/api";
|
||||
import { posterTemplates } from "./base.data";
|
||||
import GroupSelection from "@/components/GroupSelection";
|
||||
import FileUpload from "@/components/Upload/FileUpload";
|
||||
import { GroupSelectionItem } from "@/components/GroupSelection/data";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import PopupHeader from "@/components/PopuLayout/header";
|
||||
import PopupFooter from "@/components/PopuLayout/footer";
|
||||
|
||||
interface BasicSettingsProps {
|
||||
isEdit: boolean;
|
||||
@@ -68,6 +75,26 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
questionExtraction: formData.phoneSettings?.questionExtraction ?? true,
|
||||
});
|
||||
|
||||
// 分销相关状态
|
||||
const [distributionEnabled, setDistributionEnabled] = useState<boolean>(
|
||||
formData.distributionEnabled ?? false,
|
||||
);
|
||||
const [channelModalVisible, setChannelModalVisible] = useState(false);
|
||||
const [channelLoading, setChannelLoading] = useState(false);
|
||||
const [channelList, setChannelList] = useState<any[]>([]);
|
||||
const [tempSelectedChannelIds, setTempSelectedChannelIds] = useState<
|
||||
Array<string | number>
|
||||
>(formData.distributionChannelIds || []);
|
||||
const [channelSearchQuery, setChannelSearchQuery] = useState("");
|
||||
const [channelCurrentPage, setChannelCurrentPage] = useState(1);
|
||||
const [channelTotal, setChannelTotal] = useState(0);
|
||||
const [customerReward, setCustomerReward] = useState<number | undefined>(
|
||||
formData.distributionCustomerReward
|
||||
);
|
||||
const [addReward, setAddReward] = useState<number | undefined>(
|
||||
formData.distributionAddReward
|
||||
);
|
||||
|
||||
// 新增:自定义海报相关状态
|
||||
const [customPosters, setCustomPosters] = useState<Material[]>([]);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
@@ -97,6 +124,19 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
setTips(formData.tips || "");
|
||||
}, [formData.tips]);
|
||||
|
||||
// 同步分销相关的外部表单数据到本地状态
|
||||
useEffect(() => {
|
||||
setDistributionEnabled(formData.distributionEnabled ?? false);
|
||||
setTempSelectedChannelIds(formData.distributionChannelIds || []);
|
||||
setCustomerReward(formData.distributionCustomerReward);
|
||||
setAddReward(formData.distributionAddReward);
|
||||
}, [
|
||||
formData.distributionEnabled,
|
||||
formData.distributionChannelIds,
|
||||
formData.distributionCustomerReward,
|
||||
formData.distributionAddReward,
|
||||
]);
|
||||
|
||||
// 选中场景
|
||||
const handleScenarioSelect = (sceneId: number) => {
|
||||
onChange({ ...formData, scenario: sceneId });
|
||||
@@ -225,6 +265,181 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
wechatGroupsOptions: groups,
|
||||
});
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
// 加载分销渠道列表,支持keyword和分页,强制只获取启用的渠道
|
||||
const loadDistributionChannels = useCallback(
|
||||
async (keyword: string = "", page: number = 1) => {
|
||||
setChannelLoading(true);
|
||||
try {
|
||||
const res = await fetchChannelList({
|
||||
page,
|
||||
limit: PAGE_SIZE,
|
||||
keyword: keyword.trim() || undefined,
|
||||
status: "enabled", // 强制只获取启用的渠道
|
||||
});
|
||||
setChannelList(res.list || []);
|
||||
setChannelTotal(res.total || 0);
|
||||
} catch (error: any) {
|
||||
|
||||
} finally {
|
||||
setChannelLoading(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleToggleDistribution = (value: boolean) => {
|
||||
setDistributionEnabled(value);
|
||||
// 关闭时清空已选渠道和奖励金额
|
||||
if (!value) {
|
||||
setTempSelectedChannelIds([]);
|
||||
setCustomerReward(undefined);
|
||||
setAddReward(undefined);
|
||||
onChange({
|
||||
...formData,
|
||||
distributionEnabled: false,
|
||||
distributionChannelIds: [],
|
||||
distributionChannelsOptions: [],
|
||||
distributionCustomerReward: undefined,
|
||||
distributionAddReward: undefined,
|
||||
});
|
||||
} else {
|
||||
onChange({
|
||||
...formData,
|
||||
distributionEnabled: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 打开弹窗时获取第一页
|
||||
useEffect(() => {
|
||||
if (channelModalVisible) {
|
||||
setChannelSearchQuery("");
|
||||
setChannelCurrentPage(1);
|
||||
// 复制一份已选渠道到临时变量
|
||||
setTempSelectedChannelIds(formData.distributionChannelIds || []);
|
||||
loadDistributionChannels("", 1);
|
||||
}
|
||||
}, [channelModalVisible, loadDistributionChannels, formData.distributionChannelIds]);
|
||||
|
||||
// 搜索防抖
|
||||
useEffect(() => {
|
||||
if (!channelModalVisible) return;
|
||||
const timer = setTimeout(() => {
|
||||
setChannelCurrentPage(1);
|
||||
loadDistributionChannels(channelSearchQuery, 1);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [channelSearchQuery, channelModalVisible, loadDistributionChannels]);
|
||||
|
||||
// 翻页时重新请求
|
||||
useEffect(() => {
|
||||
if (!channelModalVisible) return;
|
||||
loadDistributionChannels(channelSearchQuery, channelCurrentPage);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [channelCurrentPage]);
|
||||
|
||||
const handleOpenChannelModal = () => {
|
||||
setChannelModalVisible(true);
|
||||
};
|
||||
|
||||
const handleChannelToggle = (channel: any) => {
|
||||
const id = channel.id;
|
||||
setTempSelectedChannelIds(prev =>
|
||||
prev.includes(id) ? prev.filter(v => v !== id) : [...prev, id],
|
||||
);
|
||||
};
|
||||
|
||||
// 直接使用从API返回的渠道列表(API已过滤为只返回启用的)
|
||||
const filteredChannels = channelList;
|
||||
|
||||
const channelTotalPages = Math.max(1, Math.ceil(channelTotal / PAGE_SIZE));
|
||||
|
||||
// 全选当前页
|
||||
const handleSelectAllCurrentPage = (checked: boolean) => {
|
||||
if (checked) {
|
||||
// 全选:添加当前页面所有未选中的渠道
|
||||
const currentPageChannels = filteredChannels.filter(
|
||||
(channel: any) => !tempSelectedChannelIds.includes(channel.id),
|
||||
);
|
||||
setTempSelectedChannelIds(prev => [
|
||||
...prev,
|
||||
...currentPageChannels.map((c: any) => c.id),
|
||||
]);
|
||||
} else {
|
||||
// 取消全选:移除当前页面的所有渠道
|
||||
const currentPageChannelIds = filteredChannels.map((c: any) => c.id);
|
||||
setTempSelectedChannelIds(prev =>
|
||||
prev.filter(id => !currentPageChannelIds.includes(id)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 检查当前页是否全选
|
||||
const isCurrentPageAllSelected =
|
||||
filteredChannels.length > 0 &&
|
||||
filteredChannels.every((channel: any) =>
|
||||
tempSelectedChannelIds.includes(channel.id),
|
||||
);
|
||||
|
||||
const handleConfirmChannels = () => {
|
||||
const selectedOptions =
|
||||
channelList
|
||||
.filter(c => tempSelectedChannelIds.includes(c.id))
|
||||
.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
})) || [];
|
||||
|
||||
onChange({
|
||||
...formData,
|
||||
distributionEnabled: true,
|
||||
distributionChannelIds: tempSelectedChannelIds,
|
||||
distributionChannelsOptions: selectedOptions,
|
||||
});
|
||||
setDistributionEnabled(true);
|
||||
setChannelModalVisible(false);
|
||||
};
|
||||
|
||||
const handleCancelChannels = () => {
|
||||
setChannelModalVisible(false);
|
||||
// 取消时恢复为表单中的已有值
|
||||
setTempSelectedChannelIds(formData.distributionChannelIds || []);
|
||||
};
|
||||
|
||||
// 获取显示文本(参考设备选择)
|
||||
const getChannelDisplayText = () => {
|
||||
const selectedChannels = formData.distributionChannelsOptions || [];
|
||||
if (selectedChannels.length === 0) return "";
|
||||
return `已选择 ${selectedChannels.length} 个渠道`;
|
||||
};
|
||||
|
||||
// 删除已选渠道
|
||||
const handleRemoveChannel = (id: string | number) => {
|
||||
const newChannelIds = (formData.distributionChannelIds || []).filter(
|
||||
(cid: string | number) => cid !== id
|
||||
);
|
||||
const newChannelOptions = (formData.distributionChannelsOptions || []).filter(
|
||||
(item: { id: string | number; name: string }) => item.id !== id
|
||||
);
|
||||
onChange({
|
||||
...formData,
|
||||
distributionChannelIds: newChannelIds,
|
||||
distributionChannelsOptions: newChannelOptions,
|
||||
});
|
||||
};
|
||||
|
||||
// 清除所有已选渠道
|
||||
const handleClearAllChannels = () => {
|
||||
onChange({
|
||||
...formData,
|
||||
distributionChannelIds: [],
|
||||
distributionChannelsOptions: [],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles["basic-container"]}>
|
||||
{/* 场景选择区块 */}
|
||||
@@ -473,6 +688,190 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分销设置 */}
|
||||
<div className={styles["basic-distribution"]}>
|
||||
<div className={styles["basic-distribution-header"]}>
|
||||
<div>
|
||||
<div className={styles["basic-distribution-title"]}>分销设置</div>
|
||||
<div className={styles["basic-distribution-desc"]}>
|
||||
开启后,可将当前场景的获客用户同步到指定分销渠道
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={distributionEnabled}
|
||||
onChange={handleToggleDistribution}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{distributionEnabled && (
|
||||
<>
|
||||
{/* 输入框 - 参考设备选择样式 */}
|
||||
<div className={styles["distribution-input-wrapper"]}>
|
||||
<Input
|
||||
placeholder="选择分销渠道"
|
||||
value={getChannelDisplayText()}
|
||||
onClick={handleOpenChannelModal}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
onClear={handleClearAllChannels}
|
||||
size="large"
|
||||
readOnly
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</div>
|
||||
{/* 已选渠道列表 - 参考设备选择样式 */}
|
||||
{formData.distributionChannelsOptions &&
|
||||
formData.distributionChannelsOptions.length > 0 ? (
|
||||
<div
|
||||
className={styles["distribution-selected-list"]}
|
||||
style={{
|
||||
maxHeight: 300,
|
||||
overflowY: "auto",
|
||||
marginTop: 8,
|
||||
border: "1px solid #e5e6eb",
|
||||
borderRadius: 8,
|
||||
background: "#fff",
|
||||
}}
|
||||
>
|
||||
{formData.distributionChannelsOptions.map(
|
||||
(item: { id: string | number; name: string; code?: string }) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={styles["distribution-selected-item"]}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "8px 12px",
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{/* 渠道图标 */}
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: "6px",
|
||||
background:
|
||||
"linear-gradient(135deg, #1677ff 0%, #0958d9 100%)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "hidden",
|
||||
boxShadow: "0 2px 8px rgba(22, 119, 255, 0.25)",
|
||||
marginRight: "12px",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: "#fff",
|
||||
fontWeight: 700,
|
||||
textShadow: "0 1px 3px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
{(item.name || "渠")[0]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
color: "#1a1a1a",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
{item.code && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "#8c8c8c",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
编码: {item.code}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
style={{
|
||||
marginLeft: 4,
|
||||
color: "#ff4d4f",
|
||||
border: "none",
|
||||
background: "none",
|
||||
minWidth: 24,
|
||||
height: 24,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onClick={() => handleRemoveChannel(item.id)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{/* 奖励金额设置 */}
|
||||
<div className={styles["distribution-rewards"]}>
|
||||
<div className={styles["basic-label"]}>获客奖励金额(元)</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入获客奖励金额"
|
||||
value={customerReward}
|
||||
onChange={e => {
|
||||
const value = e.target.value ? Number(e.target.value) : undefined;
|
||||
setCustomerReward(value);
|
||||
onChange({
|
||||
...formData,
|
||||
distributionCustomerReward: value,
|
||||
});
|
||||
}}
|
||||
min={0}
|
||||
step={0.01}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
<div className={styles["basic-label"]}>添加奖励金额(元)</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入添加奖励金额"
|
||||
value={addReward}
|
||||
onChange={e => {
|
||||
const value = e.target.value ? Number(e.target.value) : undefined;
|
||||
setAddReward(value);
|
||||
onChange({
|
||||
...formData,
|
||||
distributionAddReward: value,
|
||||
});
|
||||
}}
|
||||
min={0}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 订单导入区块 - 使用FileUpload组件 */}
|
||||
<div className={styles["basic-order-upload"]} style={openOrder}>
|
||||
<div className={styles["basic-order-upload-label"]}>订单表格上传</div>
|
||||
@@ -559,6 +958,115 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
onChange={value => onChange({ ...formData, status: value ? 1 : 0 })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 分销渠道选择弹框 - 参考设备选择样式 */}
|
||||
<Popup
|
||||
visible={channelModalVisible}
|
||||
onMaskClick={handleCancelChannels}
|
||||
position="bottom"
|
||||
bodyStyle={{ height: "100vh" }}
|
||||
closeOnMaskClick={false}
|
||||
>
|
||||
<Layout
|
||||
header={
|
||||
<PopupHeader
|
||||
title="选择分销渠道"
|
||||
searchQuery={channelSearchQuery}
|
||||
setSearchQuery={setChannelSearchQuery}
|
||||
searchPlaceholder="搜索渠道名称、编码..."
|
||||
loading={channelLoading}
|
||||
onRefresh={() => loadDistributionChannels(channelSearchQuery, channelCurrentPage)}
|
||||
showTabs={false}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<PopupFooter
|
||||
currentPage={channelCurrentPage}
|
||||
totalPages={channelTotalPages}
|
||||
loading={channelLoading}
|
||||
selectedCount={tempSelectedChannelIds.length}
|
||||
onPageChange={setChannelCurrentPage}
|
||||
onCancel={handleCancelChannels}
|
||||
onConfirm={handleConfirmChannels}
|
||||
isAllSelected={isCurrentPageAllSelected}
|
||||
onSelectAll={handleSelectAllCurrentPage}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles["channelList"]}>
|
||||
{channelLoading && channelList.length === 0 ? (
|
||||
<div className={styles["loadingBox"]}>
|
||||
<div className={styles["loadingText"]}>加载中...</div>
|
||||
</div>
|
||||
) : filteredChannels.length === 0 ? (
|
||||
<div className={styles["loadingBox"]}>
|
||||
<div className={styles["loadingText"]}>
|
||||
暂无分销渠道,请先在「分销管理」中创建渠道
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles["channelListInner"]}>
|
||||
{filteredChannels.map((channel: any) => (
|
||||
<div key={channel.id} className={styles["channelItem"]}>
|
||||
{/* 顶部行:选择框和编码 */}
|
||||
<div className={styles["headerRow"]}>
|
||||
<div className={styles["checkboxContainer"]}>
|
||||
<Checkbox
|
||||
checked={tempSelectedChannelIds.includes(channel.id)}
|
||||
onChange={() => handleChannelToggle(channel)}
|
||||
className={styles["channelCheckbox"]}
|
||||
/>
|
||||
</div>
|
||||
<span className={styles["codeText"]}>
|
||||
编码: {channel.code}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 主要内容区域:渠道信息 */}
|
||||
<div className={styles["mainContent"]}>
|
||||
{/* 渠道信息 */}
|
||||
<div className={styles["channelContent"]}>
|
||||
<div className={styles["channelInfoRow"]}>
|
||||
<span className={styles["channelName"]}>
|
||||
{channel.name}
|
||||
</span>
|
||||
<div
|
||||
className={
|
||||
channel.status === "enabled"
|
||||
? styles["statusEnabled"]
|
||||
: styles["statusDisabled"]
|
||||
}
|
||||
>
|
||||
{channel.status === "enabled" ? "启用" : "禁用"}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles["channelInfoDetail"]}>
|
||||
{channel.phone && (
|
||||
<div className={styles["infoItem"]}>
|
||||
<span className={styles["infoLabel"]}>手机号:</span>
|
||||
<span className={styles["infoValue"]}>
|
||||
{channel.phone}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{channel.wechatId && (
|
||||
<div className={styles["infoItem"]}>
|
||||
<span className={styles["infoLabel"]}>微信号:</span>
|
||||
<span className={styles["infoValue"]}>
|
||||
{channel.wechatId}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
</Popup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -161,3 +161,214 @@
|
||||
justify-content: space-between;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
// 分销渠道选择弹框样式 - 参考设备选择
|
||||
.channelList {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.channelListInner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.channelItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid #f5f5f5;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.headerRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checkboxContainer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.codeText {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
font-family: monospace;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
|
||||
.channelCheckbox {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.channelContent {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.channelInfoRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.channelName {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.statusEnabled {
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
color: #52c41a;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.statusDisabled {
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
color: #ff4d4f;
|
||||
background: #fff2f0;
|
||||
border: 1px solid #ffccc7;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.channelInfoDetail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.infoItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.infoLabel {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
|
||||
&.customerCount {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
color: #888;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
// 分销设置
|
||||
.basic-distribution {
|
||||
margin: 16px 0;
|
||||
padding: 16px;
|
||||
background: #f7f8fa;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
|
||||
.basic-distribution-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.basic-distribution-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.basic-distribution-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.distribution-input-wrapper {
|
||||
position: relative;
|
||||
margin-top: 12px;
|
||||
|
||||
.ant-input {
|
||||
padding-left: 38px !important;
|
||||
height: 56px;
|
||||
border-radius: 16px !important;
|
||||
border: 1px solid #e5e6eb !important;
|
||||
font-size: 16px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
}
|
||||
|
||||
.distribution-selected-list {
|
||||
.distribution-selected-item {
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.distribution-rewards {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e5e6eb;
|
||||
}
|
||||
}
|
||||
|
||||
.basic-distribution-modal-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.basic-distribution-modal-tag {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// 分销管理 API
|
||||
|
||||
import request from "@/api/request";
|
||||
import type {
|
||||
Channel,
|
||||
Statistics,
|
||||
FundStatistics,
|
||||
ChannelEarnings,
|
||||
WithdrawalRequest,
|
||||
WithdrawalStatus,
|
||||
} from "./data";
|
||||
|
||||
// 获取统计数据
|
||||
export const fetchStatistics = async (): Promise<Statistics> => {
|
||||
return request("/v1/distribution/channels/statistics", {}, "GET");
|
||||
};
|
||||
|
||||
// 获取渠道列表
|
||||
export const fetchChannelList = async (params: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
keyword?: string;
|
||||
status?: "enabled" | "disabled"; // 渠道状态筛选
|
||||
}): Promise<{ list: Channel[]; total: number }> => {
|
||||
return request("/v1/distribution/channels", params, "GET");
|
||||
};
|
||||
|
||||
// 创建渠道
|
||||
export const createChannel = async (data: {
|
||||
name: string;
|
||||
phone?: string;
|
||||
wechatId?: string;
|
||||
remarks?: string;
|
||||
}): Promise<Channel> => {
|
||||
return request("/v1/distribution/channel", data, "POST");
|
||||
};
|
||||
|
||||
// 更新渠道
|
||||
export const updateChannel = async (
|
||||
id: string,
|
||||
data: {
|
||||
name: string;
|
||||
phone?: string;
|
||||
wechatId?: string;
|
||||
remarks?: string;
|
||||
},
|
||||
): Promise<Channel> => {
|
||||
return request(`/v1/distribution/channel/${id}`, data, "PUT");
|
||||
};
|
||||
|
||||
// 删除渠道
|
||||
export const deleteChannel = async (id: string): Promise<void> => {
|
||||
return request(`/v1/distribution/channel/${id}`, null, "DELETE");
|
||||
};
|
||||
|
||||
// 禁用/启用渠道
|
||||
export const toggleChannelStatus = async (
|
||||
id: string,
|
||||
status: "enabled" | "disabled",
|
||||
): Promise<void> => {
|
||||
return request(`/v1/distribution/channel/${id}/status`, { status }, "PUT");
|
||||
};
|
||||
|
||||
// 获取资金统计数据
|
||||
export const fetchFundStatistics = async (): Promise<FundStatistics> => {
|
||||
return request("/v1/distribution/channels/revenue-statistics", {}, "GET");
|
||||
};
|
||||
|
||||
// 获取渠道收益列表
|
||||
export const fetchChannelEarningsList = async (params: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
keyword?: string;
|
||||
}): Promise<{ list: ChannelEarnings[]; total: number }> => {
|
||||
const queryParams: any = {};
|
||||
if (params.page) queryParams.page = params.page;
|
||||
if (params.limit) queryParams.limit = params.limit;
|
||||
if (params.keyword) queryParams.keyword = params.keyword;
|
||||
|
||||
return request("/v1/distribution/channels/revenue-detail", queryParams, "GET");
|
||||
};
|
||||
|
||||
// 获取提现申请列表
|
||||
export const fetchWithdrawalList = async (params: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
status?: WithdrawalStatus;
|
||||
date?: string;
|
||||
keyword?: string;
|
||||
}): Promise<{ list: WithdrawalRequest[]; total: number }> => {
|
||||
const queryParams: any = {};
|
||||
if (params.page) queryParams.page = params.page;
|
||||
if (params.limit) queryParams.limit = params.limit;
|
||||
if (params.status && params.status !== "all") {
|
||||
queryParams.status = params.status;
|
||||
}
|
||||
if (params.date) queryParams.date = params.date;
|
||||
if (params.keyword) queryParams.keyword = params.keyword;
|
||||
|
||||
return request("/v1/distribution/withdrawals", queryParams, "GET");
|
||||
};
|
||||
|
||||
// 审核提现申请
|
||||
export const reviewWithdrawal = async (
|
||||
id: string,
|
||||
action: "approve" | "reject",
|
||||
remark?: string,
|
||||
): Promise<void> => {
|
||||
const data: any = { action };
|
||||
// 拒绝时 remark 必填,通过时可选
|
||||
if (action === "reject") {
|
||||
if (!remark || !remark.trim()) {
|
||||
throw new Error("拒绝时必须填写审核备注");
|
||||
}
|
||||
data.remark = remark.trim();
|
||||
} else if (remark) {
|
||||
// 通过时如果有备注也传递
|
||||
data.remark = remark.trim();
|
||||
}
|
||||
return request(`/v1/distribution/withdrawals/${id}/review`, data, "POST");
|
||||
};
|
||||
|
||||
// 标记为已打款
|
||||
export const markAsPaid = async (
|
||||
id: string,
|
||||
payType: "wechat" | "alipay" | "bankcard",
|
||||
remark?: string,
|
||||
): Promise<void> => {
|
||||
const data: any = { payType };
|
||||
if (remark) {
|
||||
data.remark = remark.trim();
|
||||
}
|
||||
return request(`/v1/distribution/withdrawals/${id}/mark-paid`, data, "POST");
|
||||
};
|
||||
|
||||
// 生成二维码
|
||||
export const generateQRCode = async (
|
||||
type: "h5" | "miniprogram",
|
||||
): Promise<{
|
||||
type: "h5" | "miniprogram";
|
||||
qrCode: string;
|
||||
url: string;
|
||||
}> => {
|
||||
return request("/v1/distribution/channel/generate-qrcode", { type }, "POST");
|
||||
};
|
||||
@@ -0,0 +1,290 @@
|
||||
.modalWrapper {
|
||||
:global(.ant-modal-content) {
|
||||
padding: 0;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(.ant-modal-body) {
|
||||
padding: 0;
|
||||
max-height: 85vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 85vh;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
// 头部
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.headerLeft {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.closeBtn {
|
||||
font-size: 20px;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
|
||||
&:hover {
|
||||
color: #222;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建方式选择
|
||||
.methodTabs {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.methodTab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #f8f9fa;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
border-color: #1890ff;
|
||||
color: #222;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.tabIcon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
// 表单样式
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.formItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #222;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #ff4d4f;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.input {
|
||||
:global(.ant-input) {
|
||||
border-radius: 8px;
|
||||
height: 44px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.phoneHint {
|
||||
margin-top: 4px;
|
||||
min-height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
:global(.adm-text-area) {
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// 扫码创建样式
|
||||
.scanContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.qrCodeContainer {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.qrCodeBox {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.qrCode {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.qrCodePlaceholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qrCodeIcon {
|
||||
font-size: 80px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.scanInstruction {
|
||||
font-size: 16px;
|
||||
color: #222;
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.scanDescription {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
margin: 0 0 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qrCodeTypeSelector {
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.typeTabs {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
background: #f5f5f5;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.typeTab {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #1890ff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 底部按钮
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cancelBtn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.submitBtn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
// 响应式
|
||||
@media (max-width: 375px) {
|
||||
.header {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.methodTabs {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.qrCodeBox {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Button, TextArea, SpinLoading } from "antd-mobile";
|
||||
import { Modal, Input, message } from "antd";
|
||||
import { CloseOutlined, UserOutlined, QrcodeOutlined } from "@ant-design/icons";
|
||||
import { generateQRCode } from "../api";
|
||||
import styles from "./AddChannelModal.module.scss";
|
||||
|
||||
interface AddChannelModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
editData?: {
|
||||
id: string;
|
||||
name: string;
|
||||
phone?: string;
|
||||
wechatId?: string;
|
||||
remarks?: string;
|
||||
};
|
||||
onSubmit?: (data: {
|
||||
id?: string;
|
||||
name: string;
|
||||
phone?: string;
|
||||
wechatId?: string;
|
||||
remarks?: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
type CreateMethod = "manual" | "scan";
|
||||
|
||||
const AddChannelModal: React.FC<AddChannelModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
editData,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const isEdit = !!editData;
|
||||
const [createMethod, setCreateMethod] = useState<CreateMethod>("manual");
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
phone: "",
|
||||
wechatId: "",
|
||||
remarks: "",
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [qrCodeType, setQrCodeType] = useState<"h5" | "miniprogram">("h5");
|
||||
const [qrCodeData, setQrCodeData] = useState<{
|
||||
qrCode: string;
|
||||
url: string;
|
||||
type: "h5" | "miniprogram";
|
||||
} | null>(null);
|
||||
const [qrCodeLoading, setQrCodeLoading] = useState(false);
|
||||
const generatingRef = useRef(false); // 用于防止重复请求
|
||||
|
||||
// 当编辑数据变化时,更新表单数据
|
||||
useEffect(() => {
|
||||
if (editData) {
|
||||
setFormData({
|
||||
name: editData.name || "",
|
||||
phone: editData.phone || "",
|
||||
wechatId: editData.wechatId || "",
|
||||
remarks: editData.remarks || "",
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
name: "",
|
||||
phone: "",
|
||||
wechatId: "",
|
||||
remarks: "",
|
||||
});
|
||||
}
|
||||
}, [editData, visible]);
|
||||
|
||||
// 当弹窗打开或切换到扫码创建时,自动生成二维码
|
||||
useEffect(() => {
|
||||
// 只有在弹窗可见、非编辑模式、选择扫码方式、没有二维码数据、且不在加载中时才生成
|
||||
if (visible && !isEdit && createMethod === "scan" && !qrCodeData && !qrCodeLoading && !generatingRef.current) {
|
||||
// 使用 setTimeout 确保状态更新完成
|
||||
const timer = setTimeout(() => {
|
||||
handleGenerateQRCode();
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, createMethod]);
|
||||
|
||||
// 当二维码类型变化时,重新生成二维码
|
||||
useEffect(() => {
|
||||
if (visible && !isEdit && createMethod === "scan" && qrCodeData && !qrCodeLoading && !generatingRef.current) {
|
||||
// 重置状态后重新生成
|
||||
setQrCodeData(null);
|
||||
setScanning(false);
|
||||
// 使用 setTimeout 确保状态更新完成
|
||||
const timer = setTimeout(() => {
|
||||
handleGenerateQRCode();
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [qrCodeType]);
|
||||
|
||||
// 验证手机号格式
|
||||
const validatePhone = (phone: string): boolean => {
|
||||
if (!phone) return true; // 手机号是可选的,空值视为有效
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
return phoneRegex.test(phone);
|
||||
};
|
||||
|
||||
// 处理手机号输入,只允许输入数字,最多11位
|
||||
const handlePhoneChange = (value: string) => {
|
||||
// 只保留数字
|
||||
const numbersOnly = value.replace(/\D/g, "");
|
||||
// 限制最多11位
|
||||
const limitedValue = numbersOnly.slice(0, 11);
|
||||
handleInputChange("phone", limitedValue);
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (createMethod === "manual") {
|
||||
if (!formData.name.trim()) {
|
||||
message.error("请输入渠道名称");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
if (formData.phone && formData.phone.trim()) {
|
||||
if (!validatePhone(formData.phone.trim())) {
|
||||
message.error("请输入正确的手机号(11位数字,1开头)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await onSubmit?.({
|
||||
id: editData?.id,
|
||||
name: formData.name.trim(),
|
||||
phone: formData.phone?.trim() || undefined,
|
||||
wechatId: formData.wechatId?.trim() || undefined,
|
||||
remarks: formData.remarks?.trim() || undefined,
|
||||
});
|
||||
// 成功后关闭弹窗(父组件会处理成功提示)
|
||||
handleClose();
|
||||
} catch (e) {
|
||||
// 错误已在父组件处理,这里不需要再次提示
|
||||
// 保持弹窗打开,让用户修改后重试
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
// 扫码创建逻辑
|
||||
if (!scanning) {
|
||||
setScanning(true);
|
||||
// TODO: 实现扫码创建逻辑
|
||||
message.info("扫码创建功能开发中");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setFormData({
|
||||
name: "",
|
||||
phone: "",
|
||||
wechatId: "",
|
||||
remarks: "",
|
||||
});
|
||||
setScanning(false);
|
||||
setQrCodeData(null);
|
||||
setQrCodeType("h5");
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 生成二维码
|
||||
const handleGenerateQRCode = async () => {
|
||||
// 如果正在生成,直接返回,避免重复请求
|
||||
if (generatingRef.current || qrCodeLoading) {
|
||||
return;
|
||||
}
|
||||
generatingRef.current = true;
|
||||
setQrCodeLoading(true);
|
||||
try {
|
||||
const res = await generateQRCode(qrCodeType);
|
||||
// 确保返回的数据有效
|
||||
if (res && res.qrCode) {
|
||||
setQrCodeData(res);
|
||||
setScanning(true);
|
||||
} else {
|
||||
throw new Error("二维码数据格式错误");
|
||||
}
|
||||
} catch (e: any) {
|
||||
// 接口拦截器已经显示了错误提示,这里不需要再次显示
|
||||
// 请求失败时重置状态,允许重试
|
||||
setQrCodeData(null);
|
||||
setScanning(false);
|
||||
} finally {
|
||||
setQrCodeLoading(false);
|
||||
generatingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重新生成二维码
|
||||
const handleRegenerateQR = async () => {
|
||||
setScanning(false);
|
||||
setQrCodeData(null);
|
||||
await handleGenerateQRCode();
|
||||
};
|
||||
|
||||
// 当切换到扫码创建时,自动生成二维码
|
||||
useEffect(() => {
|
||||
if (visible && createMethod === "scan" && !isEdit && !qrCodeData && !qrCodeLoading) {
|
||||
handleGenerateQRCode();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [createMethod, visible]);
|
||||
|
||||
// 当二维码类型变化时,重新生成二维码
|
||||
useEffect(() => {
|
||||
if (visible && createMethod === "scan" && !isEdit && qrCodeData) {
|
||||
handleGenerateQRCode();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [qrCodeType, visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width="90%"
|
||||
style={{ maxWidth: "500px" }}
|
||||
centered
|
||||
className={styles.modalWrapper}
|
||||
maskClosable={true}
|
||||
closable={false}
|
||||
>
|
||||
<div className={styles.modal}>
|
||||
{/* 头部 */}
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerLeft}>
|
||||
<h3 className={styles.title}>{isEdit ? "编辑渠道" : "新增渠道"}</h3>
|
||||
<p className={styles.subtitle}>
|
||||
{isEdit
|
||||
? "修改渠道信息"
|
||||
: "选择创建方式: 手动填写或扫码获取微信信息"}
|
||||
</p>
|
||||
</div>
|
||||
<CloseOutlined className={styles.closeBtn} onClick={handleClose} />
|
||||
</div>
|
||||
|
||||
{/* 创建方式选择 */}
|
||||
{!isEdit && (
|
||||
<div className={styles.methodTabs}>
|
||||
<button
|
||||
className={`${styles.methodTab} ${
|
||||
createMethod === "manual" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => setCreateMethod("manual")}
|
||||
>
|
||||
<UserOutlined className={styles.tabIcon} />
|
||||
<span>手动创建</span>
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.methodTab} ${
|
||||
createMethod === "scan" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => setCreateMethod("scan")}
|
||||
>
|
||||
<QrcodeOutlined className={styles.tabIcon} />
|
||||
<span>扫码创建</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className={styles.content}>
|
||||
{createMethod === "manual" || isEdit ? (
|
||||
<div className={styles.form}>
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.label}>
|
||||
渠道名称 <span className={styles.required}>*</span>
|
||||
</label>
|
||||
<Input
|
||||
placeholder="请输入渠道名称"
|
||||
value={formData.name}
|
||||
onChange={e => handleInputChange("name", e.target.value)}
|
||||
className={styles.input}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.label}>联系电话</label>
|
||||
<Input
|
||||
placeholder="请输入11位手机号"
|
||||
value={formData.phone}
|
||||
onChange={e => handlePhoneChange(e.target.value)}
|
||||
className={styles.input}
|
||||
maxLength={11}
|
||||
type="tel"
|
||||
/>
|
||||
{formData.phone && formData.phone.length > 0 && (
|
||||
<div className={styles.phoneHint}>
|
||||
{formData.phone.length < 11 ? (
|
||||
<span style={{ color: "#999", fontSize: "12px" }}>
|
||||
还需输入 {11 - formData.phone.length} 位数字
|
||||
</span>
|
||||
) : !validatePhone(formData.phone) ? (
|
||||
<span style={{ color: "#ff4d4f", fontSize: "12px" }}>
|
||||
手机号格式不正确,请以1开头
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ color: "#52c41a", fontSize: "12px" }}>
|
||||
✓ 手机号格式正确
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.label}>微信号</label>
|
||||
<Input
|
||||
placeholder="请输入微信号"
|
||||
value={formData.wechatId}
|
||||
onChange={e => handleInputChange("wechatId", e.target.value)}
|
||||
className={styles.input}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formItem}>
|
||||
<label className={styles.label}>备注</label>
|
||||
<TextArea
|
||||
placeholder="请输入备注信息"
|
||||
value={formData.remarks}
|
||||
onChange={value => handleInputChange("remarks", value)}
|
||||
className={styles.textarea}
|
||||
rows={4}
|
||||
showCount
|
||||
maxLength={200}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.scanContent}>
|
||||
{/* 二维码类型选择 */}
|
||||
<div className={styles.qrCodeTypeSelector}>
|
||||
<div className={styles.typeTabs}>
|
||||
<button
|
||||
className={`${styles.typeTab} ${
|
||||
qrCodeType === "h5" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => setQrCodeType("h5")}
|
||||
>
|
||||
H5
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.typeTab} ${
|
||||
qrCodeType === "miniprogram" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => setQrCodeType("miniprogram")}
|
||||
>
|
||||
小程序
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.qrCodeContainer}>
|
||||
<div className={styles.qrCodeBox}>
|
||||
{qrCodeLoading ? (
|
||||
<div className={styles.qrCodeLoading}>
|
||||
<SpinLoading color="primary" style={{ "--size": "24px" }} />
|
||||
<span style={{ marginTop: "12px", fontSize: "14px", color: "#666" }}>
|
||||
生成中...
|
||||
</span>
|
||||
</div>
|
||||
) : qrCodeData && qrCodeData.qrCode ? (
|
||||
<img
|
||||
src={qrCodeData.qrCode}
|
||||
alt="二维码"
|
||||
className={styles.qrCode}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.qrCodePlaceholder}>
|
||||
<QrcodeOutlined className={styles.qrCodeIcon} />
|
||||
<span style={{ marginTop: "12px", fontSize: "14px", color: "#999" }}>
|
||||
点击生成二维码
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className={styles.scanInstruction}>
|
||||
使用微信扫描二维码
|
||||
</p>
|
||||
<p className={styles.scanDescription}>
|
||||
扫描后将自动获取微信信息并创建渠道
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
fill="outline"
|
||||
onClick={handleClose}
|
||||
className={styles.cancelBtn}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
loading={loading}
|
||||
className={styles.submitBtn}
|
||||
>
|
||||
{isEdit ? "保存" : "创建"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddChannelModal;
|
||||
@@ -0,0 +1,67 @@
|
||||
// 分销管理数据模型
|
||||
|
||||
// 渠道信息
|
||||
export interface Channel {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
phone?: string;
|
||||
wechatId?: string;
|
||||
createType: "manual" | "auto"; // 手动创建 | 自动创建
|
||||
status?: "enabled" | "disabled"; // 启用 | 禁用
|
||||
totalCustomers: number; // 总获客数
|
||||
todayCustomers: number; // 今日获客数
|
||||
totalFriends: number; // 总加好友数
|
||||
todayFriends: number; // 今日加好友数
|
||||
createTime: string;
|
||||
remarks?: string; // 备注信息
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
export interface Statistics {
|
||||
totalChannels: number; // 总渠道数
|
||||
todayChannels: number; // 今日渠道数
|
||||
totalCustomers: number; // 总获客数
|
||||
todayCustomers: number; // 今日获客数
|
||||
totalFriends: number; // 总加好友数
|
||||
todayFriends: number; // 今日加好友数
|
||||
}
|
||||
|
||||
// 资金统计数据
|
||||
export interface FundStatistics {
|
||||
totalExpenditure: number; // 总支出
|
||||
withdrawn: number; // 已提现
|
||||
pendingReview: number; // 待审核
|
||||
}
|
||||
|
||||
// 渠道收益信息
|
||||
export interface ChannelEarnings {
|
||||
channelId: string; // 渠道ID
|
||||
channelName: string; // 渠道名称
|
||||
channelCode: string; // 渠道编码
|
||||
totalRevenue: number; // 总收益(元)
|
||||
withdrawable: number; // 可提现(元)
|
||||
withdrawn: number; // 已提现(元)
|
||||
pendingReview: number; // 待审核(元)
|
||||
}
|
||||
|
||||
// 提现申请状态
|
||||
export type WithdrawalStatus = "all" | "pending" | "approved" | "rejected" | "paid";
|
||||
|
||||
// 提现申请信息
|
||||
export interface WithdrawalRequest {
|
||||
id: string;
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
channelCode: string;
|
||||
amount: number;
|
||||
status: "pending" | "approved" | "rejected" | "paid";
|
||||
applyDate: string;
|
||||
reviewDate?: string;
|
||||
reviewer?: string;
|
||||
remark?: string; // 备注(拒绝或打款时的备注)
|
||||
payType?: "wechat" | "alipay" | "bankcard"; // 打款方式
|
||||
}
|
||||
|
||||
// 标签页类型
|
||||
export type TabType = "channel" | "fund" | "withdrawal";
|
||||
@@ -0,0 +1,192 @@
|
||||
// 渠道详情 API(模拟数据)
|
||||
|
||||
import type {
|
||||
ChannelDetail,
|
||||
ChannelStatistics,
|
||||
RevenueRecord,
|
||||
RevenueType,
|
||||
WithdrawalDetailRecord,
|
||||
WithdrawalDetailStatus,
|
||||
} from "./data";
|
||||
|
||||
// 模拟延迟
|
||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
// 获取渠道详情
|
||||
export const fetchChannelDetail = async (
|
||||
id: string,
|
||||
): Promise<ChannelDetail> => {
|
||||
await delay(500);
|
||||
return {
|
||||
id: "CH1765355105571YX9UWX",
|
||||
name: "张大大",
|
||||
code: "CH1765355105571YX9UWX",
|
||||
phone: undefined,
|
||||
wechatId: undefined,
|
||||
createType: "manual",
|
||||
remark: undefined,
|
||||
createTime: "2025-12-10 16:25",
|
||||
};
|
||||
};
|
||||
|
||||
// 获取渠道统计数据
|
||||
export const fetchChannelStatistics = async (
|
||||
id: string,
|
||||
): Promise<ChannelStatistics> => {
|
||||
await delay(500);
|
||||
return {
|
||||
totalFriends: 0,
|
||||
todayFriends: 0,
|
||||
totalCustomers: 0,
|
||||
todayCustomers: 0,
|
||||
totalRevenue: 8.0,
|
||||
pendingWithdrawal: 8.0,
|
||||
pendingReview: 0.0,
|
||||
withdrawn: 0.0,
|
||||
};
|
||||
};
|
||||
|
||||
// 获取收益明细列表
|
||||
export const fetchRevenueList = async (params: {
|
||||
channelId: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
type?: RevenueType;
|
||||
date?: string;
|
||||
}): Promise<{ list: RevenueRecord[]; total: number; page: number }> => {
|
||||
await delay(500);
|
||||
|
||||
// 模拟数据
|
||||
const mockRecords: RevenueRecord[] = [
|
||||
{
|
||||
id: "REV001",
|
||||
title: "加好友任务-春季活动",
|
||||
type: "addFriend",
|
||||
typeLabel: "加好友",
|
||||
amount: 5.0,
|
||||
date: "2025-12-09 16:54",
|
||||
},
|
||||
];
|
||||
|
||||
// 筛选过滤
|
||||
let filteredList = mockRecords;
|
||||
if (params.type && params.type !== "all") {
|
||||
filteredList = filteredList.filter(item => item.type === params.type);
|
||||
}
|
||||
if (params.date) {
|
||||
filteredList = filteredList.filter(item => item.date.startsWith(params.date!));
|
||||
}
|
||||
|
||||
const page = params.page || 1;
|
||||
const limit = params.limit || 10;
|
||||
const start = (page - 1) * limit;
|
||||
const end = start + limit;
|
||||
const paginatedList = filteredList.slice(start, end);
|
||||
|
||||
return {
|
||||
list: paginatedList,
|
||||
total: filteredList.length,
|
||||
page,
|
||||
};
|
||||
};
|
||||
|
||||
// 获取提现明细列表
|
||||
export const fetchWithdrawalDetailList = async (params: {
|
||||
channelId: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
status?: WithdrawalDetailStatus;
|
||||
date?: string;
|
||||
}): Promise<{
|
||||
list: WithdrawalDetailRecord[];
|
||||
total: number;
|
||||
page: number;
|
||||
}> => {
|
||||
await delay(500);
|
||||
|
||||
// 模拟数据
|
||||
const mockRecords: WithdrawalDetailRecord[] = [
|
||||
{
|
||||
id: "WD001",
|
||||
amount: 100.0,
|
||||
status: "pending",
|
||||
applyDate: "2025-12-10 14:30",
|
||||
},
|
||||
{
|
||||
id: "WD002",
|
||||
amount: 50.0,
|
||||
status: "approved",
|
||||
applyDate: "2025-12-09 10:20",
|
||||
reviewDate: "2025-12-09 15:30",
|
||||
},
|
||||
{
|
||||
id: "WD003",
|
||||
amount: 200.0,
|
||||
status: "paid",
|
||||
applyDate: "2025-12-08 09:15",
|
||||
reviewDate: "2025-12-08 14:20",
|
||||
paidDate: "2025-12-08 16:45",
|
||||
},
|
||||
{
|
||||
id: "WD004",
|
||||
amount: 80.0,
|
||||
status: "rejected",
|
||||
applyDate: "2025-12-07 11:00",
|
||||
reviewDate: "2025-12-07 16:00",
|
||||
remark: "提现金额超出限制",
|
||||
},
|
||||
{
|
||||
id: "WD005",
|
||||
amount: 150.0,
|
||||
status: "approved",
|
||||
applyDate: "2025-12-06 13:25",
|
||||
reviewDate: "2025-12-06 18:10",
|
||||
},
|
||||
{
|
||||
id: "WD006",
|
||||
amount: 120.0,
|
||||
status: "paid",
|
||||
applyDate: "2025-12-05 08:30",
|
||||
reviewDate: "2025-12-05 12:15",
|
||||
paidDate: "2025-12-05 14:20",
|
||||
},
|
||||
{
|
||||
id: "WD007",
|
||||
amount: 60.0,
|
||||
status: "pending",
|
||||
applyDate: "2025-12-04 15:40",
|
||||
},
|
||||
{
|
||||
id: "WD008",
|
||||
amount: 90.0,
|
||||
status: "paid",
|
||||
applyDate: "2025-12-03 10:10",
|
||||
reviewDate: "2025-12-03 14:30",
|
||||
paidDate: "2025-12-03 16:00",
|
||||
remark: "已到账",
|
||||
},
|
||||
];
|
||||
|
||||
// 筛选过滤
|
||||
let filteredList = mockRecords;
|
||||
if (params.status && params.status !== "all") {
|
||||
filteredList = filteredList.filter(item => item.status === params.status);
|
||||
}
|
||||
if (params.date) {
|
||||
filteredList = filteredList.filter(item =>
|
||||
item.applyDate.startsWith(params.date!),
|
||||
);
|
||||
}
|
||||
|
||||
const page = params.page || 1;
|
||||
const limit = params.limit || 10;
|
||||
const start = (page - 1) * limit;
|
||||
const end = start + limit;
|
||||
const paginatedList = filteredList.slice(start, end);
|
||||
|
||||
return {
|
||||
list: paginatedList,
|
||||
total: filteredList.length,
|
||||
page,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
// 渠道详情数据模型
|
||||
|
||||
// 渠道详情信息
|
||||
export interface ChannelDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
phone?: string;
|
||||
wechatId?: string;
|
||||
createType: "manual" | "auto";
|
||||
remark?: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
// 渠道统计数据
|
||||
export interface ChannelStatistics {
|
||||
totalFriends: number; // 总加好友数
|
||||
todayFriends: number; // 今日加好友数
|
||||
totalCustomers: number; // 总获客数
|
||||
todayCustomers: number; // 今日获客数
|
||||
totalRevenue: number; // 总收益
|
||||
pendingWithdrawal: number; // 待提现
|
||||
pendingReview: number; // 待审核
|
||||
withdrawn: number; // 已提现
|
||||
}
|
||||
|
||||
// 收益明细类型
|
||||
export type RevenueType = "all" | "addFriend" | "customer" | "other";
|
||||
|
||||
// 收益明细记录
|
||||
export interface RevenueRecord {
|
||||
id: string;
|
||||
title: string;
|
||||
type: "addFriend" | "customer" | "other";
|
||||
typeLabel: string;
|
||||
amount: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
// 提现明细状态
|
||||
export type WithdrawalDetailStatus = "all" | "pending" | "approved" | "rejected" | "paid";
|
||||
|
||||
// 提现明细记录
|
||||
export interface WithdrawalDetailRecord {
|
||||
id: string;
|
||||
amount: number;
|
||||
status: "pending" | "approved" | "rejected" | "paid";
|
||||
applyDate: string;
|
||||
reviewDate?: string;
|
||||
paidDate?: string;
|
||||
remark?: string;
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
.container {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// 头部标题样式
|
||||
.headerTitle {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mainTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
line-height: 1.2;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
// 标签页容器
|
||||
.tabsContainer {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
:global(.adm-tabs-header) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:global(.adm-tabs-tab) {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:global(.adm-tabs-tab-active) {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载和错误状态
|
||||
.loadingContainer,
|
||||
.errorContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.errorContainer p {
|
||||
color: #999;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
// 基本信息卡片
|
||||
.basicInfoCard {
|
||||
margin: 16px;
|
||||
background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.basicInfoHeader {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.basicInfoTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.basicInfoIcon {
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.basicInfoTitleText {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.basicInfoSubtitle {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.createType {
|
||||
padding: 4px 12px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.basicInfoContent {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.channelName {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.channelCodeBox {
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 16px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.infoGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.infoCard {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.infoCardIcon {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infoCardText {
|
||||
font-size: 13px;
|
||||
color: #444;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.phoneCard {
|
||||
background: #f6ffed;
|
||||
.infoCardIcon {
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
|
||||
.wechatCard {
|
||||
background: #e6f7ff;
|
||||
.infoCardIcon {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.remarkCard {
|
||||
background: #f9f0ff;
|
||||
.infoCardIcon {
|
||||
color: #722ed1;
|
||||
}
|
||||
}
|
||||
|
||||
.timeCard {
|
||||
background: #f5f5f5;
|
||||
.infoCardIcon {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
// 数据统计
|
||||
.statisticsSection {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sectionDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #1890ff;
|
||||
}
|
||||
|
||||
.statisticsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.statCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.statIcon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.statChange {
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.blueCard {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
.statIcon,
|
||||
.statValue,
|
||||
.statChange,
|
||||
.statLabel {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.greenCard {
|
||||
background: #52c41a;
|
||||
color: #fff;
|
||||
.statIcon,
|
||||
.statValue,
|
||||
.statChange,
|
||||
.statLabel {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.purpleCard {
|
||||
background: #722ed1;
|
||||
color: #fff;
|
||||
.statIcon,
|
||||
.statValue,
|
||||
.statLabel {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.orangeCard {
|
||||
background: #fa8c16;
|
||||
color: #fff;
|
||||
.statIcon,
|
||||
.statValue,
|
||||
.statLabel {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.yellowCard {
|
||||
background: #faad14;
|
||||
color: #fff;
|
||||
.statIcon,
|
||||
.statValue,
|
||||
.statLabel {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.greyCard {
|
||||
background: #595959;
|
||||
color: #fff;
|
||||
.statIcon,
|
||||
.statValue,
|
||||
.statLabel {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholderContent {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 筛选面板
|
||||
.filterPanel {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
margin: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.filterTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.filterIcon {
|
||||
font-size: 16px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.filterContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filterItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filterLabel {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.filterSelect,
|
||||
.filterInput {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filterSelect {
|
||||
:global(.ant-select-selector) {
|
||||
border-radius: 8px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterInput {
|
||||
:global(.ant-input) {
|
||||
border-radius: 8px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
// 收益明细列表
|
||||
.revenueList {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.revenueStats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.revenueTotal {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.revenuePagination {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.revenueCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.revenueHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.revenueTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.revenueType {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
padding: 4px 8px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.revenueTypeIcon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.revenueFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.revenueDate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.revenueDateIcon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.revenueAmount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.revenueAmountLabel {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.revenueAmountValue {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.loadingContainer,
|
||||
.emptyContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 提现明细列表
|
||||
.withdrawalDetailList {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.withdrawalDetailCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.withdrawalDetailHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.withdrawalDetailAmount {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.statusPending {
|
||||
background: #fff7e6;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.statusApproved {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.statusRejected {
|
||||
background: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.statusPaid {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.withdrawalDetailInfo {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.emptyState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px dashed #e0e0e0;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 64px;
|
||||
color: #d9d9d9;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
// 响应式
|
||||
@media (max-width: 375px) {
|
||||
.statisticsGrid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,623 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { Tabs, SpinLoading, DatePicker } from "antd-mobile";
|
||||
import { Button, Input, Select } from "antd";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import {
|
||||
TeamOutlined,
|
||||
PhoneOutlined,
|
||||
WechatOutlined,
|
||||
FileTextOutlined,
|
||||
CalendarOutlined,
|
||||
UserAddOutlined,
|
||||
WalletOutlined,
|
||||
DollarOutlined,
|
||||
FilterOutlined,
|
||||
DownOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
fetchChannelDetail,
|
||||
fetchChannelStatistics,
|
||||
fetchRevenueList,
|
||||
fetchWithdrawalDetailList,
|
||||
} from "./api";
|
||||
import type {
|
||||
ChannelDetail,
|
||||
ChannelStatistics,
|
||||
RevenueRecord,
|
||||
RevenueType,
|
||||
WithdrawalDetailRecord,
|
||||
WithdrawalDetailStatus,
|
||||
} from "./data";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
// 格式化金额显示(后端返回的是分,需要转换为元)
|
||||
const formatCurrency = (amount: number): string => {
|
||||
// 将分转换为元
|
||||
const yuan = amount / 100;
|
||||
if (yuan >= 10000) {
|
||||
return "¥" + (yuan / 10000).toFixed(2) + "万";
|
||||
}
|
||||
return "¥" + yuan.toFixed(2);
|
||||
};
|
||||
|
||||
const ChannelDetailPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [channelDetail, setChannelDetail] = useState<ChannelDetail | null>(
|
||||
null,
|
||||
);
|
||||
const [statistics, setStatistics] = useState<ChannelStatistics | null>(null);
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [revenueList, setRevenueList] = useState<RevenueRecord[]>([]);
|
||||
const [revenueLoading, setRevenueLoading] = useState(false);
|
||||
const [revenueType, setRevenueType] = useState<RevenueType>("all");
|
||||
const [revenueDate, setRevenueDate] = useState<Date | null>(null);
|
||||
const [showRevenueDatePicker, setShowRevenueDatePicker] = useState(false);
|
||||
const [revenuePage, setRevenuePage] = useState(1);
|
||||
const [revenueTotal, setRevenueTotal] = useState(0);
|
||||
const [withdrawalDetailList, setWithdrawalDetailList] = useState<
|
||||
WithdrawalDetailRecord[]
|
||||
>([]);
|
||||
const [withdrawalDetailLoading, setWithdrawalDetailLoading] =
|
||||
useState(false);
|
||||
const [withdrawalDetailStatus, setWithdrawalDetailStatus] =
|
||||
useState<WithdrawalDetailStatus>("all");
|
||||
const [withdrawalDetailDate, setWithdrawalDetailDate] = useState<Date | null>(
|
||||
null,
|
||||
);
|
||||
const [showWithdrawalDetailDatePicker, setShowWithdrawalDetailDatePicker] =
|
||||
useState(false);
|
||||
const [withdrawalDetailPage, setWithdrawalDetailPage] = useState(1);
|
||||
const [withdrawalDetailTotal, setWithdrawalDetailTotal] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
loadDetail();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id && activeTab === "revenue") {
|
||||
loadRevenueList();
|
||||
} else if (id && activeTab === "withdrawal") {
|
||||
loadWithdrawalDetailList();
|
||||
}
|
||||
}, [
|
||||
id,
|
||||
activeTab,
|
||||
revenueType,
|
||||
revenueDate,
|
||||
revenuePage,
|
||||
withdrawalDetailStatus,
|
||||
withdrawalDetailDate,
|
||||
withdrawalDetailPage,
|
||||
]);
|
||||
|
||||
const loadDetail = async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const [detail, stats] = await Promise.all([
|
||||
fetchChannelDetail(id),
|
||||
fetchChannelStatistics(id),
|
||||
]);
|
||||
setChannelDetail(detail);
|
||||
setStatistics(stats);
|
||||
} catch (e) {
|
||||
// 处理错误
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadRevenueList = async () => {
|
||||
if (!id) return;
|
||||
setRevenueLoading(true);
|
||||
try {
|
||||
const res = await fetchRevenueList({
|
||||
channelId: id,
|
||||
page: revenuePage,
|
||||
limit: 10,
|
||||
type: revenueType,
|
||||
date: revenueDate
|
||||
? `${revenueDate.getFullYear()}-${String(
|
||||
revenueDate.getMonth() + 1,
|
||||
).padStart(2, "0")}-${String(revenueDate.getDate()).padStart(2, "0")}`
|
||||
: undefined,
|
||||
});
|
||||
setRevenueList(res.list);
|
||||
setRevenueTotal(res.total);
|
||||
} catch (e) {
|
||||
// 处理错误
|
||||
} finally {
|
||||
setRevenueLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadWithdrawalDetailList = async () => {
|
||||
if (!id) return;
|
||||
setWithdrawalDetailLoading(true);
|
||||
try {
|
||||
const res = await fetchWithdrawalDetailList({
|
||||
channelId: id,
|
||||
page: withdrawalDetailPage,
|
||||
limit: 10,
|
||||
status: withdrawalDetailStatus,
|
||||
date: withdrawalDetailDate
|
||||
? `${withdrawalDetailDate.getFullYear()}-${String(
|
||||
withdrawalDetailDate.getMonth() + 1,
|
||||
).padStart(2, "0")}-${String(
|
||||
withdrawalDetailDate.getDate(),
|
||||
).padStart(2, "0")}`
|
||||
: undefined,
|
||||
});
|
||||
setWithdrawalDetailList(res.list);
|
||||
setWithdrawalDetailTotal(res.total);
|
||||
} catch (e) {
|
||||
// 处理错误
|
||||
} finally {
|
||||
setWithdrawalDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
left={<></>}
|
||||
title="分销客户详情"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles.loadingContainer}>
|
||||
<SpinLoading color="primary" />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (!channelDetail || !statistics) {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
left={<></>}
|
||||
title="分销客户详情"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles.errorContainer}>
|
||||
<p>渠道不存在</p>
|
||||
<Button onClick={() => navigate(-1)}>返回</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ key: "overview", title: "概览" },
|
||||
{ key: "revenue", title: "收益明细" },
|
||||
{ key: "withdrawal", title: "提现明细" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
left={<></>}
|
||||
title={
|
||||
<div className={styles.headerTitle}>
|
||||
<div className={styles.mainTitle}>分销客户详情</div>
|
||||
<div className={styles.subTitle}>渠道信息与统计</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
{/* 标签页 */}
|
||||
<div className={styles.tabsContainer}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={key => setActiveTab(key)}
|
||||
className={styles.tabs}
|
||||
>
|
||||
{tabs.map(tab => (
|
||||
<Tabs.Tab key={tab.key} title={tab.title} />
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* 概览标签页内容 */}
|
||||
{activeTab === "overview" && (
|
||||
<>
|
||||
{/* 基本信息 */}
|
||||
<div className={styles.basicInfoCard}>
|
||||
<div className={styles.basicInfoHeader}>
|
||||
<div className={styles.basicInfoTitle}>
|
||||
<TeamOutlined className={styles.basicInfoIcon} />
|
||||
<div>
|
||||
<div className={styles.basicInfoTitleText}>基本信息</div>
|
||||
<div className={styles.basicInfoSubtitle}>
|
||||
渠道详细信息
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className={styles.createType}>
|
||||
{channelDetail.createType === "manual"
|
||||
? "手动创建"
|
||||
: "自动创建"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.basicInfoContent}>
|
||||
<div className={styles.channelName}>
|
||||
{channelDetail.name}
|
||||
</div>
|
||||
|
||||
<div className={styles.channelCodeBox}>
|
||||
{channelDetail.code}
|
||||
</div>
|
||||
|
||||
<div className={styles.infoGrid}>
|
||||
<div className={`${styles.infoCard} ${styles.phoneCard}`}>
|
||||
<PhoneOutlined className={styles.infoCardIcon} />
|
||||
<div className={styles.infoCardText}>
|
||||
{channelDetail.phone || "未填写"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.infoCard} ${styles.wechatCard}`}>
|
||||
<WechatOutlined className={styles.infoCardIcon} />
|
||||
<div className={styles.infoCardText}>
|
||||
{channelDetail.wechatId || "未填写"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.infoCard} ${styles.remarkCard}`}>
|
||||
<FileTextOutlined className={styles.infoCardIcon} />
|
||||
<div className={styles.infoCardText}>
|
||||
{channelDetail.remark || "暂无备注"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.infoCard} ${styles.timeCard}`}>
|
||||
<CalendarOutlined className={styles.infoCardIcon} />
|
||||
<div className={styles.infoCardText}>
|
||||
{channelDetail.createTime}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据统计 */}
|
||||
<div className={styles.statisticsSection}>
|
||||
<div className={styles.sectionTitle}>
|
||||
<div className={styles.sectionDot}></div>
|
||||
<span>数据统计</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.statisticsGrid}>
|
||||
<div className={`${styles.statCard} ${styles.blueCard}`}>
|
||||
<UserAddOutlined className={styles.statIcon} />
|
||||
<div className={styles.statValue}>{statistics.totalFriends}</div>
|
||||
<div className={styles.statChange}>
|
||||
今日: {statistics.todayFriends}
|
||||
</div>
|
||||
<div className={styles.statLabel}>总加好友数</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.statCard} ${styles.greenCard}`}>
|
||||
<TeamOutlined className={styles.statIcon} />
|
||||
<div className={styles.statValue}>{statistics.totalCustomers}</div>
|
||||
<div className={styles.statChange}>
|
||||
今日: {statistics.todayCustomers}
|
||||
</div>
|
||||
<div className={styles.statLabel}>总获客数</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.statCard} ${styles.purpleCard}`}>
|
||||
<WalletOutlined className={styles.statIcon} />
|
||||
<div className={styles.statValue}>
|
||||
{formatCurrency(statistics.totalRevenue)}
|
||||
</div>
|
||||
<div className={styles.statLabel}>总收益</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.statCard} ${styles.orangeCard}`}>
|
||||
<DollarOutlined className={styles.statIcon} />
|
||||
<div className={styles.statValue}>
|
||||
{formatCurrency(statistics.pendingWithdrawal)}
|
||||
</div>
|
||||
<div className={styles.statLabel}>待提现</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.statCard} ${styles.yellowCard}`}>
|
||||
<FileTextOutlined className={styles.statIcon} />
|
||||
<div className={styles.statValue}>
|
||||
{formatCurrency(statistics.pendingReview)}
|
||||
</div>
|
||||
<div className={styles.statLabel}>待审核</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.statCard} ${styles.greyCard}`}>
|
||||
<WalletOutlined className={styles.statIcon} />
|
||||
<div className={styles.statValue}>
|
||||
{formatCurrency(statistics.withdrawn)}
|
||||
</div>
|
||||
<div className={styles.statLabel}>已提现</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 收益明细标签页内容 */}
|
||||
{activeTab === "revenue" && (
|
||||
<>
|
||||
{/* 筛选条件 */}
|
||||
<div className={styles.filterPanel}>
|
||||
<div className={styles.filterTitle}>
|
||||
<FilterOutlined className={styles.filterIcon} />
|
||||
<span>筛选条件</span>
|
||||
</div>
|
||||
<div className={styles.filterContent}>
|
||||
<div className={styles.filterItem}>
|
||||
<label className={styles.filterLabel}>类型</label>
|
||||
<Select
|
||||
value={revenueType}
|
||||
onChange={value => {
|
||||
setRevenueType(value as RevenueType);
|
||||
setRevenuePage(1);
|
||||
}}
|
||||
className={styles.filterSelect}
|
||||
suffixIcon={<DownOutlined />}
|
||||
>
|
||||
<Select.Option value="all">全部</Select.Option>
|
||||
<Select.Option value="addFriend">加好友</Select.Option>
|
||||
<Select.Option value="customer">获客</Select.Option>
|
||||
<Select.Option value="other">其他</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterItem}>
|
||||
<label className={styles.filterLabel}>时间</label>
|
||||
<Input
|
||||
readOnly
|
||||
placeholder="选择日期"
|
||||
value={
|
||||
revenueDate
|
||||
? `${revenueDate.getFullYear()}-${String(
|
||||
revenueDate.getMonth() + 1,
|
||||
).padStart(2, "0")}-${String(
|
||||
revenueDate.getDate(),
|
||||
).padStart(2, "0")}`
|
||||
: ""
|
||||
}
|
||||
onClick={() => setShowRevenueDatePicker(true)}
|
||||
prefix={<CalendarOutlined />}
|
||||
className={styles.filterInput}
|
||||
/>
|
||||
<DatePicker
|
||||
visible={showRevenueDatePicker}
|
||||
title="选择日期"
|
||||
value={revenueDate}
|
||||
onClose={() => setShowRevenueDatePicker(false)}
|
||||
onConfirm={val => {
|
||||
setRevenueDate(val);
|
||||
setShowRevenueDatePicker(false);
|
||||
setRevenuePage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 记录统计和列表 */}
|
||||
<div className={styles.revenueList}>
|
||||
<div className={styles.revenueStats}>
|
||||
<span className={styles.revenueTotal}>
|
||||
共 {revenueTotal} 条记录
|
||||
</span>
|
||||
<span className={styles.revenuePagination}>
|
||||
{revenuePage}/{Math.ceil(revenueTotal / 10)} 条
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{revenueLoading ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<SpinLoading color="primary" />
|
||||
</div>
|
||||
) : revenueList.length === 0 ? (
|
||||
<div className={styles.emptyContainer}>暂无收益记录</div>
|
||||
) : (
|
||||
revenueList.map(record => (
|
||||
<div key={record.id} className={styles.revenueCard}>
|
||||
<div className={styles.revenueHeader}>
|
||||
<div className={styles.revenueTitle}>{record.title}</div>
|
||||
<div className={styles.revenueType}>
|
||||
{record.type === "addFriend" && (
|
||||
<UserAddOutlined className={styles.revenueTypeIcon} />
|
||||
)}
|
||||
<span>{record.typeLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.revenueFooter}>
|
||||
<div className={styles.revenueDate}>
|
||||
<CalendarOutlined className={styles.revenueDateIcon} />
|
||||
<span>{record.date}</span>
|
||||
</div>
|
||||
<div className={styles.revenueAmount}>
|
||||
<span className={styles.revenueAmountLabel}>收益</span>
|
||||
<span className={styles.revenueAmountValue}>
|
||||
{formatCurrency(record.amount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 提现明细标签页内容 */}
|
||||
{activeTab === "withdrawal" && (
|
||||
<>
|
||||
{/* 筛选条件 */}
|
||||
<div className={styles.filterPanel}>
|
||||
<div className={styles.filterTitle}>
|
||||
<FilterOutlined className={styles.filterIcon} />
|
||||
<span>筛选条件</span>
|
||||
</div>
|
||||
<div className={styles.filterContent}>
|
||||
<div className={styles.filterItem}>
|
||||
<label className={styles.filterLabel}>状态</label>
|
||||
<Select
|
||||
value={withdrawalDetailStatus}
|
||||
onChange={value => {
|
||||
setWithdrawalDetailStatus(value as WithdrawalDetailStatus);
|
||||
setWithdrawalDetailPage(1);
|
||||
}}
|
||||
className={styles.filterSelect}
|
||||
suffixIcon={<DownOutlined />}
|
||||
>
|
||||
<Select.Option value="all">全部</Select.Option>
|
||||
<Select.Option value="pending">待审核</Select.Option>
|
||||
<Select.Option value="approved">已通过</Select.Option>
|
||||
<Select.Option value="rejected">已拒绝</Select.Option>
|
||||
<Select.Option value="paid">已打款</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterItem}>
|
||||
<label className={styles.filterLabel}>时间</label>
|
||||
<Input
|
||||
readOnly
|
||||
placeholder="选择日期"
|
||||
value={
|
||||
withdrawalDetailDate
|
||||
? `${withdrawalDetailDate.getFullYear()}-${String(
|
||||
withdrawalDetailDate.getMonth() + 1,
|
||||
).padStart(2, "0")}-${String(
|
||||
withdrawalDetailDate.getDate(),
|
||||
).padStart(2, "0")}`
|
||||
: ""
|
||||
}
|
||||
onClick={() => setShowWithdrawalDetailDatePicker(true)}
|
||||
prefix={<CalendarOutlined />}
|
||||
className={styles.filterInput}
|
||||
/>
|
||||
<DatePicker
|
||||
visible={showWithdrawalDetailDatePicker}
|
||||
title="选择日期"
|
||||
value={withdrawalDetailDate}
|
||||
onClose={() => setShowWithdrawalDetailDatePicker(false)}
|
||||
onConfirm={val => {
|
||||
setWithdrawalDetailDate(val);
|
||||
setShowWithdrawalDetailDatePicker(false);
|
||||
setWithdrawalDetailPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 记录统计和列表 */}
|
||||
<div className={styles.withdrawalDetailList}>
|
||||
<div className={styles.revenueStats}>
|
||||
<span className={styles.revenueTotal}>
|
||||
共 {withdrawalDetailTotal} 条记录
|
||||
</span>
|
||||
<span className={styles.revenuePagination}>
|
||||
{withdrawalDetailPage}/{Math.ceil(withdrawalDetailTotal / 10)} 条
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{withdrawalDetailLoading ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<SpinLoading color="primary" />
|
||||
</div>
|
||||
) : withdrawalDetailList.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyIcon}>
|
||||
<WalletOutlined />
|
||||
</div>
|
||||
<div className={styles.emptyText}>暂无提现明细</div>
|
||||
</div>
|
||||
) : (
|
||||
withdrawalDetailList.map(record => (
|
||||
<div key={record.id} className={styles.withdrawalDetailCard}>
|
||||
<div className={styles.withdrawalDetailHeader}>
|
||||
<div className={styles.withdrawalDetailAmount}>
|
||||
{formatCurrency(record.amount)}
|
||||
</div>
|
||||
<span
|
||||
className={`${styles.statusBadge} ${
|
||||
record.status === "pending"
|
||||
? styles.statusPending
|
||||
: record.status === "approved"
|
||||
? styles.statusApproved
|
||||
: record.status === "rejected"
|
||||
? styles.statusRejected
|
||||
: styles.statusPaid
|
||||
}`}
|
||||
>
|
||||
{record.status === "pending"
|
||||
? "待审核"
|
||||
: record.status === "approved"
|
||||
? "已通过"
|
||||
: record.status === "rejected"
|
||||
? "已拒绝"
|
||||
: "已打款"}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.withdrawalDetailInfo}>
|
||||
<div className={styles.infoItem}>
|
||||
<span className={styles.infoLabel}>申请时间:</span>
|
||||
<span className={styles.infoValue}>
|
||||
{record.applyDate}
|
||||
</span>
|
||||
</div>
|
||||
{record.reviewDate && (
|
||||
<div className={styles.infoItem}>
|
||||
<span className={styles.infoLabel}>审核时间:</span>
|
||||
<span className={styles.infoValue}>
|
||||
{record.reviewDate}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{record.paidDate && (
|
||||
<div className={styles.infoItem}>
|
||||
<span className={styles.infoLabel}>打款时间:</span>
|
||||
<span className={styles.infoValue}>
|
||||
{record.paidDate}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{record.remark && (
|
||||
<div className={styles.infoItem}>
|
||||
<span className={styles.infoLabel}>备注:</span>
|
||||
<span className={styles.infoValue}>
|
||||
{record.remark}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelDetailPage;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import {
|
||||
ClockCircleOutlined,
|
||||
ContactsOutlined,
|
||||
BookOutlined,
|
||||
ApartmentOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
@@ -96,6 +97,20 @@ const Workspace: React.FC = () => {
|
||||
bgColor: "#fff7e6",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
id: "distribution-management",
|
||||
name: "分销管理",
|
||||
description: "管理分销客户和渠道",
|
||||
icon: (
|
||||
<ApartmentOutlined
|
||||
className={styles.icon}
|
||||
style={{ color: "#722ed1" }}
|
||||
/>
|
||||
),
|
||||
path: "/workspace/distribution-management",
|
||||
bgColor: "#f9f0ff",
|
||||
isNew: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,11 +18,13 @@ import NewDistribution from "@/pages/mobile/workspace/traffic-distribution/form/
|
||||
import ContactImportList from "@/pages/mobile/workspace/contact-import/list";
|
||||
import ContactImportForm from "@/pages/mobile/workspace/contact-import/form";
|
||||
import ContactImportDetail from "@/pages/mobile/workspace/contact-import/detail";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
import AiAnalyzer from "@/pages/mobile/workspace/ai-analyzer";
|
||||
import AIKnowledgeList from "@/pages/mobile/workspace/ai-knowledge/list";
|
||||
import AIKnowledgeDetail from "@/pages/mobile/workspace/ai-knowledge/detail";
|
||||
import AIKnowledgeForm from "@/pages/mobile/workspace/ai-knowledge/form";
|
||||
import DistributionManagement from "@/pages/mobile/workspace/distribution-management";
|
||||
import ChannelDetailPage from "@/pages/mobile/workspace/distribution-management/detail";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const workspaceRoutes = [
|
||||
{
|
||||
@@ -202,6 +204,17 @@ const workspaceRoutes = [
|
||||
element: <AIKnowledgeForm />,
|
||||
auth: true,
|
||||
},
|
||||
// 分销管理
|
||||
{
|
||||
path: "/workspace/distribution-management",
|
||||
element: <DistributionManagement />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/distribution-management/:id",
|
||||
element: <ChannelDetailPage />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default workspaceRoutes;
|
||||
|
||||
@@ -198,6 +198,33 @@ Route::group('v1/', function () {
|
||||
Route::post('disable', 'app\cunkebao\controller\StoreAccountController@disable'); // 禁用/启用账号
|
||||
});
|
||||
|
||||
// 分销渠道管理
|
||||
Route::group('distribution', function () {
|
||||
// 渠道列表和统计
|
||||
Route::group('channels', function () {
|
||||
Route::get('', 'app\cunkebao\controller\distribution\ChannelController@index'); // 获取渠道列表
|
||||
Route::get('statistics', 'app\cunkebao\controller\distribution\ChannelController@statistics'); // 获取渠道统计数据
|
||||
Route::get('revenue-statistics', 'app\cunkebao\controller\distribution\ChannelController@revenueStatistics'); // 获取渠道收益统计(全局)
|
||||
Route::get('revenue-detail', 'app\cunkebao\controller\distribution\ChannelController@revenueDetail'); // 获取渠道收益明细(单个渠道)
|
||||
});
|
||||
// 单个渠道操作
|
||||
Route::group('channel', function () {
|
||||
Route::post('', 'app\cunkebao\controller\distribution\ChannelController@create'); // 添加渠道
|
||||
Route::put(':id', 'app\cunkebao\controller\distribution\ChannelController@update'); // 编辑渠道
|
||||
Route::delete(':id', 'app\cunkebao\controller\distribution\ChannelController@delete'); // 删除渠道
|
||||
Route::post(':id/toggle-status', 'app\cunkebao\controller\distribution\ChannelController@toggleStatus'); // 禁用/启用渠道
|
||||
Route::post('generate-qrcode', 'app\cunkebao\controller\distribution\ChannelController@generateQrCode'); // 生成渠道二维码
|
||||
});
|
||||
// 提现申请管理
|
||||
Route::group('withdrawals', function () {
|
||||
Route::get('', 'app\cunkebao\controller\distribution\WithdrawalController@index'); // 获取提现申请列表
|
||||
Route::post('', 'app\cunkebao\controller\distribution\WithdrawalController@create'); // 创建提现申请
|
||||
Route::get(':id', 'app\cunkebao\controller\distribution\WithdrawalController@detail'); // 获取提现申请详情
|
||||
Route::post(':id/review', 'app\cunkebao\controller\distribution\WithdrawalController@review'); // 审核提现申请(通过/拒绝)
|
||||
Route::post(':id/mark-paid', 'app\cunkebao\controller\distribution\WithdrawalController@markPaid'); // 标记为已打款
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
})->middleware(['jwt']);
|
||||
@@ -218,6 +245,20 @@ Route::group('v1/frontend', function () {
|
||||
//Route::post('decryptphones', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@decryptphones');
|
||||
});
|
||||
Route::post('business/form/importsave', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@decryptphones');
|
||||
|
||||
// 分销渠道注册(H5扫码)
|
||||
Route::group('distribution/channel', function () {
|
||||
Route::get('register', 'app\cunkebao\controller\distribution\ChannelController@registerByQrCode'); // H5页面(GET显示表单)
|
||||
Route::post('register', 'app\cunkebao\controller\distribution\ChannelController@registerByQrCode'); // 提交渠道信息(POST)
|
||||
});
|
||||
|
||||
// 分销渠道用户端(无需JWT认证,通过渠道编码访问)
|
||||
Route::group('distribution/user', function () {
|
||||
Route::post('login', 'app\cunkebao\controller\distribution\ChannelUserController@login'); // 渠道登录
|
||||
Route::get('home', 'app\cunkebao\controller\distribution\ChannelUserController@index'); // 获取渠道首页数据
|
||||
Route::get('revenue-records', 'app\cunkebao\controller\distribution\ChannelUserController@revenueRecords'); // 获取收益明细列表
|
||||
Route::get('withdrawal-records', 'app\cunkebao\controller\distribution\ChannelUserController@withdrawalRecords'); // 获取提现明细列表
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,590 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller\distribution;
|
||||
|
||||
use app\cunkebao\model\DistributionChannel;
|
||||
use app\cunkebao\model\DistributionWithdrawal;
|
||||
use app\common\util\JwtUtil;
|
||||
use think\Controller;
|
||||
use think\Db;
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* 分销渠道用户端控制器
|
||||
* 用户通过渠道编码访问,无需JWT认证
|
||||
*/
|
||||
class ChannelUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* 初始化方法,设置跨域响应头
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
|
||||
// 处理OPTIONS预检请求
|
||||
if ($this->request->method(true) == 'OPTIONS') {
|
||||
$origin = $this->request->header('origin', '*');
|
||||
header("Access-Control-Allow-Origin: " . $origin);
|
||||
header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization, Cookie");
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, PATCH');
|
||||
header("Access-Control-Allow-Credentials: true");
|
||||
header("Access-Control-Max-Age: 86400");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置跨域响应头
|
||||
* @param \think\response\Json $response
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
protected function setCorsHeaders($response)
|
||||
{
|
||||
$origin = $this->request->header('origin', '*');
|
||||
$response->header([
|
||||
'Access-Control-Allow-Origin' => $origin,
|
||||
'Access-Control-Allow-Headers' => 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cookie',
|
||||
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS, PATCH',
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
'Access-Control-Max-Age' => '86400',
|
||||
]);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道登录
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
try {
|
||||
// 获取参数
|
||||
$phone = $this->request->param('phone', '');
|
||||
$password = $this->request->param('password', '');
|
||||
|
||||
// 参数验证
|
||||
if (empty($phone)) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '手机号不能为空',
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
|
||||
if (empty($password)) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '密码不能为空',
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
|
||||
// 查询渠道信息(通过手机号)
|
||||
$channel = Db::name('distribution_channel')
|
||||
->where([
|
||||
['phone', '=', $phone],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$channel) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 404,
|
||||
'success' => false,
|
||||
'msg' => '渠道不存在',
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
|
||||
// 检查渠道状态
|
||||
if ($channel['status'] !== DistributionChannel::STATUS_ENABLED) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 403,
|
||||
'success' => false,
|
||||
'msg' => '渠道已被禁用',
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
|
||||
// 验证密码(MD5加密)
|
||||
$passwordMd5 = md5($password);
|
||||
if ($channel['password'] !== $passwordMd5) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 401,
|
||||
'success' => false,
|
||||
'msg' => '密码错误',
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
|
||||
// 准备token载荷(不包含密码)
|
||||
$payload = [
|
||||
'id' => $channel['id'],
|
||||
'channelId' => $channel['id'],
|
||||
'channelCode' => $channel['code'],
|
||||
'channelName' => $channel['name'],
|
||||
'companyId' => $channel['companyId'],
|
||||
'type' => 'channel', // 标识这是渠道登录
|
||||
];
|
||||
|
||||
// 生成JWT令牌(30天有效期)
|
||||
$expire = 86400 * 30;
|
||||
$token = JwtUtil::createToken($payload, $expire);
|
||||
$tokenExpired = time() + $expire;
|
||||
|
||||
// 更新最后登录时间(可选)
|
||||
Db::name('distribution_channel')
|
||||
->where('id', $channel['id'])
|
||||
->update([
|
||||
'updateTime' => time()
|
||||
]);
|
||||
|
||||
// 返回数据(不包含密码)
|
||||
$data = [
|
||||
'token' => $token,
|
||||
'tokenExpired' => $tokenExpired,
|
||||
'channelInfo' => [
|
||||
'id' => (string)$channel['id'],
|
||||
'channelCode' => $channel['code'],
|
||||
'channelName' => $channel['name'],
|
||||
'phone' => $channel['phone'] ?: '',
|
||||
'wechatId' => $channel['wechatId'] ?: '',
|
||||
'status' => $channel['status'],
|
||||
'totalCustomers' => (int)$channel['totalCustomers'],
|
||||
'todayCustomers' => (int)$channel['todayCustomers'],
|
||||
'totalFriends' => (int)$channel['totalFriends'],
|
||||
'todayFriends' => (int)$channel['todayFriends'],
|
||||
'withdrawableAmount' => round(($channel['withdrawableAmount'] ?? 0) / 100, 2), // 分转元
|
||||
]
|
||||
];
|
||||
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 200,
|
||||
'success' => true,
|
||||
'msg' => '登录成功',
|
||||
'data' => $data
|
||||
]));
|
||||
|
||||
} catch (Exception $e) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => $e->getCode() ?: 500,
|
||||
'success' => false,
|
||||
'msg' => '登录失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取渠道首页数据
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
// 获取参数
|
||||
$channelCode = $this->request->param('channelCode', '');
|
||||
|
||||
// 参数验证
|
||||
if (empty($channelCode)) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '渠道编码不能为空',
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
|
||||
// 查询渠道信息
|
||||
$channel = Db::name('distribution_channel')
|
||||
->where([
|
||||
['code', '=', $channelCode],
|
||||
['status', '=', DistributionChannel::STATUS_ENABLED],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$channel) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 404,
|
||||
'success' => false,
|
||||
'msg' => '渠道不存在或已被禁用',
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
|
||||
$channelId = $channel['id'];
|
||||
$companyId = $channel['companyId'];
|
||||
|
||||
// 1. 渠道基本信息
|
||||
$channelInfo = [
|
||||
'channelName' => $channel['name'] ?? '',
|
||||
'channelCode' => $channel['code'] ?? '',
|
||||
];
|
||||
|
||||
// 2. 财务统计
|
||||
// 当前可提现金额
|
||||
$withdrawableAmount = intval($channel['withdrawableAmount'] ?? 0);
|
||||
|
||||
// 已提现金额(已打款的提现申请)
|
||||
$withdrawnAmount = Db::name('distribution_withdrawal')
|
||||
->where([
|
||||
['companyId', '=', $companyId],
|
||||
['channelId', '=', $channelId],
|
||||
['status', '=', DistributionWithdrawal::STATUS_PAID]
|
||||
])
|
||||
->sum('amount');
|
||||
$withdrawnAmount = intval($withdrawnAmount ?? 0);
|
||||
|
||||
// 待审核金额(待审核的提现申请)
|
||||
$pendingReviewAmount = Db::name('distribution_withdrawal')
|
||||
->where([
|
||||
['companyId', '=', $companyId],
|
||||
['channelId', '=', $channelId],
|
||||
['status', '=', DistributionWithdrawal::STATUS_PENDING]
|
||||
])
|
||||
->sum('amount');
|
||||
$pendingReviewAmount = intval($pendingReviewAmount ?? 0);
|
||||
|
||||
// 总收益(所有收益记录的总和)
|
||||
$totalRevenue = Db::name('distribution_revenue_record')
|
||||
->where([
|
||||
['companyId', '=', $companyId],
|
||||
['channelId', '=', $channelId]
|
||||
])
|
||||
->sum('amount');
|
||||
$totalRevenue = intval($totalRevenue ?? 0);
|
||||
|
||||
$financialStats = [
|
||||
'withdrawableAmount' => round($withdrawableAmount / 100, 2), // 当前可提现金额(元)
|
||||
'totalRevenue' => round($totalRevenue / 100, 2), // 总收益(元)
|
||||
'pendingReview' => round($pendingReviewAmount / 100, 2), // 待审核(元)
|
||||
'withdrawn' => round($withdrawnAmount / 100, 2), // 已提现(元)
|
||||
];
|
||||
|
||||
// 3. 客户和好友统计
|
||||
$customerStats = [
|
||||
'totalFriends' => (int)($channel['totalFriends'] ?? 0), // 总加好友数
|
||||
'todayFriends' => (int)($channel['todayFriends'] ?? 0), // 今日加好友数
|
||||
'totalCustomers' => (int)($channel['totalCustomers'] ?? 0), // 总获客数
|
||||
'todayCustomers' => (int)($channel['todayCustomers'] ?? 0), // 今日获客数
|
||||
];
|
||||
|
||||
// 返回数据
|
||||
$data = [
|
||||
'channelInfo' => $channelInfo,
|
||||
'financialStats' => $financialStats,
|
||||
'customerStats' => $customerStats,
|
||||
];
|
||||
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 200,
|
||||
'success' => true,
|
||||
'msg' => '获取成功',
|
||||
'data' => $data
|
||||
]));
|
||||
|
||||
} catch (Exception $e) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => $e->getCode() ?: 500,
|
||||
'success' => false,
|
||||
'msg' => '获取数据失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取收益明细列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function revenueRecords()
|
||||
{
|
||||
try {
|
||||
// 获取参数
|
||||
$channelCode = $this->request->param('channelCode', '');
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$type = $this->request->param('type', 'all'); // all, customer_acquisition, add_friend, order, poster, phone, other
|
||||
$date = $this->request->param('date', ''); // 日期筛选,格式:Y-m-d
|
||||
|
||||
// 参数验证
|
||||
if (empty($channelCode)) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '渠道编码不能为空',
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
|
||||
$page = max(1, intval($page));
|
||||
$limit = max(1, min(100, intval($limit)));
|
||||
|
||||
// 查询渠道信息
|
||||
$channel = Db::name('distribution_channel')
|
||||
->where([
|
||||
['code', '=', $channelCode],
|
||||
['status', '=', DistributionChannel::STATUS_ENABLED],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$channel) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 404,
|
||||
'success' => false,
|
||||
'msg' => '渠道不存在或已被禁用',
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
|
||||
$channelId = $channel['id'];
|
||||
$companyId = $channel['companyId'];
|
||||
|
||||
// 构建查询条件
|
||||
$where = [
|
||||
['companyId', '=', $companyId],
|
||||
['channelId', '=', $channelId]
|
||||
];
|
||||
|
||||
// 类型筛选
|
||||
if ($type !== 'all') {
|
||||
$where[] = ['type', '=', $type];
|
||||
}
|
||||
|
||||
// 日期筛选
|
||||
if (!empty($date)) {
|
||||
$dateStart = strtotime($date . ' 00:00:00');
|
||||
$dateEnd = strtotime($date . ' 23:59:59');
|
||||
if ($dateStart && $dateEnd) {
|
||||
$where[] = ['createTime', 'between', [$dateStart, $dateEnd]];
|
||||
}
|
||||
}
|
||||
|
||||
// 查询总数
|
||||
$total = Db::name('distribution_revenue_record')
|
||||
->where($where)
|
||||
->count();
|
||||
|
||||
// 查询列表(按创建时间倒序)
|
||||
$list = Db::name('distribution_revenue_record')
|
||||
->where($where)
|
||||
->order('createTime DESC')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
// 从活动表(customer_acquisition_task)获取类型标签映射(使用 sourceId 关联活动ID)
|
||||
$formattedList = [];
|
||||
if (!empty($list)) {
|
||||
// 收集本页涉及到的活动ID
|
||||
$taskIds = [];
|
||||
foreach ($list as $row) {
|
||||
if (!empty($row['sourceId'])) {
|
||||
$taskIds[] = (int)$row['sourceId'];
|
||||
}
|
||||
}
|
||||
$taskIds = array_values(array_unique($taskIds));
|
||||
|
||||
// 获取活动名称映射:taskId => name
|
||||
$taskNameMap = [];
|
||||
if (!empty($taskIds)) {
|
||||
$taskNameMap = Db::name('customer_acquisition_task')
|
||||
->whereIn('id', $taskIds)
|
||||
->column('name', 'id');
|
||||
}
|
||||
|
||||
// 格式化数据
|
||||
foreach ($list as $item) {
|
||||
$taskId = !empty($item['sourceId']) ? (int)$item['sourceId'] : 0;
|
||||
$taskName = $taskId && isset($taskNameMap[$taskId]) ? $taskNameMap[$taskId] : null;
|
||||
|
||||
$formattedItem = [
|
||||
'id' => (string)$item['id'],
|
||||
'sourceType' => $item['sourceType'] ?? '其他',
|
||||
'type' => $item['type'] ?? 'other',
|
||||
// 类型标签优先取活动名称,没有则回退为 sourceType 或 “其他”
|
||||
'typeLabel' => $taskName ?: (!empty($item['sourceType']) ? $item['sourceType'] : '其他'),
|
||||
'amount' => round($item['amount'] / 100, 2), // 分转元
|
||||
'remark' => isset($item['remark']) && $item['remark'] !== '' ? $item['remark'] : null,
|
||||
'createTime' => !empty($item['createTime']) ? date('Y-m-d H:i', $item['createTime']) : '',
|
||||
];
|
||||
$formattedList[] = $formattedItem;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 200,
|
||||
'success' => true,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $formattedList,
|
||||
'total' => (int)$total,
|
||||
'page' => $page,
|
||||
'limit' => $limit
|
||||
]
|
||||
]));
|
||||
|
||||
} catch (Exception $e) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => $e->getCode() ?: 500,
|
||||
'success' => false,
|
||||
'msg' => '获取收益明细失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提现明细列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function withdrawalRecords()
|
||||
{
|
||||
try {
|
||||
// 获取参数
|
||||
$channelCode = $this->request->param('channelCode', '');
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$status = $this->request->param('status', 'all'); // all, pending, approved, rejected, paid
|
||||
$payType = $this->request->param('payType', 'all'); // all, wechat, alipay, bankcard
|
||||
$date = $this->request->param('date', ''); // 日期筛选,格式:Y-m-d
|
||||
|
||||
// 参数验证
|
||||
if (empty($channelCode)) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '渠道编码不能为空',
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
|
||||
$page = max(1, intval($page));
|
||||
$limit = max(1, min(100, intval($limit)));
|
||||
|
||||
// 校验到账方式参数
|
||||
$validPayTypes = ['all', 'wechat', 'alipay', 'bankcard'];
|
||||
if (!in_array($payType, $validPayTypes)) {
|
||||
$payType = 'all';
|
||||
}
|
||||
|
||||
// 查询渠道信息
|
||||
$channel = Db::name('distribution_channel')
|
||||
->where([
|
||||
['code', '=', $channelCode],
|
||||
['status', '=', DistributionChannel::STATUS_ENABLED],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$channel) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 404,
|
||||
'success' => false,
|
||||
'msg' => '渠道不存在或已被禁用',
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
|
||||
$channelId = $channel['id'];
|
||||
$companyId = $channel['companyId'];
|
||||
|
||||
// 构建查询条件
|
||||
$where = [
|
||||
['companyId', '=', $companyId],
|
||||
['channelId', '=', $channelId]
|
||||
];
|
||||
|
||||
// 状态筛选
|
||||
if ($status !== 'all') {
|
||||
$where[] = ['status', '=', $status];
|
||||
}
|
||||
|
||||
// 到账方式筛选
|
||||
if ($payType !== 'all') {
|
||||
$where[] = ['payType', '=', $payType];
|
||||
}
|
||||
|
||||
// 日期筛选
|
||||
if (!empty($date)) {
|
||||
$dateStart = strtotime($date . ' 00:00:00');
|
||||
$dateEnd = strtotime($date . ' 23:59:59');
|
||||
if ($dateStart && $dateEnd) {
|
||||
$where[] = ['applyTime', 'between', [$dateStart, $dateEnd]];
|
||||
}
|
||||
}
|
||||
|
||||
// 查询总数
|
||||
$total = Db::name('distribution_withdrawal')
|
||||
->where($where)
|
||||
->count();
|
||||
|
||||
// 查询列表(按申请时间倒序)
|
||||
$list = Db::name('distribution_withdrawal')
|
||||
->where($where)
|
||||
->order('applyTime DESC')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
// 格式化数据
|
||||
$formattedList = [];
|
||||
foreach ($list as $item) {
|
||||
// 状态标签映射
|
||||
$statusLabels = [
|
||||
'pending' => '待审核',
|
||||
'approved' => '已通过',
|
||||
'rejected' => '已拒绝',
|
||||
'paid' => '已打款'
|
||||
];
|
||||
|
||||
// 支付类型标签映射
|
||||
$payTypeLabels = [
|
||||
'wechat' => '微信',
|
||||
'alipay' => '支付宝',
|
||||
'bankcard' => '银行卡'
|
||||
];
|
||||
|
||||
$payType = !empty($item['payType']) ? $item['payType'] : null;
|
||||
|
||||
$formattedItem = [
|
||||
'id' => (string)$item['id'],
|
||||
'amount' => round($item['amount'] / 100, 2), // 分转元
|
||||
'status' => $item['status'] ?? 'pending',
|
||||
'statusLabel' => $statusLabels[$item['status'] ?? 'pending'] ?? '待审核',
|
||||
'payType' => $payType,
|
||||
'payTypeLabel' => $payType && isset($payTypeLabels[$payType]) ? $payTypeLabels[$payType] : null,
|
||||
'applyTime' => !empty($item['applyTime']) ? date('Y-m-d H:i', $item['applyTime']) : '',
|
||||
'reviewTime' => !empty($item['reviewTime']) ? date('Y-m-d H:i', $item['reviewTime']) : null,
|
||||
'reviewer' => !empty($item['reviewer']) ? $item['reviewer'] : null,
|
||||
'remark' => !empty($item['remark']) ? $item['remark'] : null,
|
||||
];
|
||||
$formattedList[] = $formattedItem;
|
||||
}
|
||||
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => 200,
|
||||
'success' => true,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $formattedList,
|
||||
'total' => (int)$total,
|
||||
'page' => $page,
|
||||
'limit' => $limit
|
||||
]
|
||||
]));
|
||||
|
||||
} catch (Exception $e) {
|
||||
return $this->setCorsHeaders(json([
|
||||
'code' => $e->getCode() ?: 500,
|
||||
'success' => false,
|
||||
'msg' => '获取提现明细失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,670 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller\distribution;
|
||||
|
||||
use app\cunkebao\controller\BaseController;
|
||||
use app\cunkebao\model\DistributionWithdrawal;
|
||||
use library\ResponseHelper;
|
||||
use think\Db;
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* 分销渠道提现申请控制器
|
||||
*/
|
||||
class WithdrawalController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取提现申请列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
// 获取参数
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 20);
|
||||
$status = $this->request->param('status', 'all');
|
||||
$date = $this->request->param('date', '');
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
// 参数验证
|
||||
$page = max(1, intval($page));
|
||||
$limit = max(1, min(100, intval($limit))); // 限制最大100
|
||||
|
||||
// 验证状态参数
|
||||
$validStatuses = ['all', DistributionWithdrawal::STATUS_PENDING, DistributionWithdrawal::STATUS_APPROVED, DistributionWithdrawal::STATUS_REJECTED, DistributionWithdrawal::STATUS_PAID];
|
||||
if (!in_array($status, $validStatuses)) {
|
||||
$status = 'all';
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
$where = [];
|
||||
$where[] = ['w.companyId', '=', $companyId];
|
||||
|
||||
// 状态筛选
|
||||
if ($status !== 'all') {
|
||||
$where[] = ['w.status', '=', $status];
|
||||
}
|
||||
|
||||
// 日期筛选(格式:YYYY/MM/DD)
|
||||
if (!empty($date)) {
|
||||
// 转换日期格式 YYYY/MM/DD 为时间戳范围
|
||||
$dateParts = explode('/', $date);
|
||||
if (count($dateParts) === 3) {
|
||||
$dateStr = $dateParts[0] . '-' . $dateParts[1] . '-' . $dateParts[2];
|
||||
$dateStart = strtotime($dateStr . ' 00:00:00');
|
||||
$dateEnd = strtotime($dateStr . ' 23:59:59');
|
||||
if ($dateStart && $dateEnd) {
|
||||
$where[] = ['w.applyTime', 'between', [$dateStart, $dateEnd]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关键词搜索(模糊匹配渠道名称、渠道编码)
|
||||
if (!empty($keyword)) {
|
||||
$keyword = trim($keyword);
|
||||
// 需要关联渠道表进行搜索
|
||||
}
|
||||
|
||||
// 构建查询(关联渠道表获取渠道名称和编码,只关联未删除的渠道)
|
||||
$query = Db::name('distribution_withdrawal')
|
||||
->alias('w')
|
||||
->join('distribution_channel c', 'w.channelId = c.id AND c.deleteTime = 0', 'left')
|
||||
->where($where);
|
||||
|
||||
// 关键词搜索(如果有关键词,添加渠道表关联条件)
|
||||
if (!empty($keyword)) {
|
||||
$query->where(function ($query) use ($keyword) {
|
||||
$query->where('c.name', 'like', '%' . $keyword . '%')
|
||||
->whereOr('c.code', 'like', '%' . $keyword . '%');
|
||||
});
|
||||
}
|
||||
|
||||
// 查询总数
|
||||
$total = $query->count();
|
||||
|
||||
// 查询列表(按申请时间倒序)
|
||||
$list = $query->field([
|
||||
'w.id',
|
||||
'w.channelId',
|
||||
'w.amount',
|
||||
'w.status',
|
||||
'w.payType',
|
||||
'w.applyTime',
|
||||
'w.reviewTime',
|
||||
'w.reviewer',
|
||||
'w.remark',
|
||||
'c.name as channelName',
|
||||
'c.code as channelCode'
|
||||
])
|
||||
->order('w.applyTime DESC')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
// 格式化数据
|
||||
$formattedList = [];
|
||||
foreach ($list as $item) {
|
||||
// 格式化申请日期为 YYYY/MM/DD
|
||||
$applyDate = '';
|
||||
if (!empty($item['applyTime'])) {
|
||||
$applyDate = date('Y/m/d', $item['applyTime']);
|
||||
}
|
||||
|
||||
// 格式化审核日期
|
||||
$reviewDate = null;
|
||||
if (!empty($item['reviewTime'])) {
|
||||
$reviewDate = date('Y-m-d H:i:s', $item['reviewTime']);
|
||||
}
|
||||
|
||||
$formattedItem = [
|
||||
'id' => (string)$item['id'],
|
||||
'channelId' => (string)$item['channelId'],
|
||||
'channelName' => $item['channelName'] ?? '',
|
||||
'channelCode' => $item['channelCode'] ?? '',
|
||||
'amount' => round($item['amount'] / 100, 2), // 分转元,保留2位小数
|
||||
'status' => $item['status'] ?? DistributionWithdrawal::STATUS_PENDING,
|
||||
'payType' => !empty($item['payType']) ? $item['payType'] : null, // 支付类型
|
||||
'applyDate' => $applyDate,
|
||||
'reviewDate' => $reviewDate,
|
||||
'reviewer' => !empty($item['reviewer']) ? $item['reviewer'] : null,
|
||||
'remark' => !empty($item['remark']) ? $item['remark'] : null,
|
||||
];
|
||||
$formattedList[] = $formattedItem;
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return json([
|
||||
'code' => 200,
|
||||
'success' => true,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $formattedList,
|
||||
'total' => (int)$total
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return json([
|
||||
'code' => $e->getCode() ?: 500,
|
||||
'success' => false,
|
||||
'msg' => '获取提现申请列表失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建提现申请
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
try {
|
||||
// 获取参数(接口接收的金额单位为元)
|
||||
// 原先使用 channelId,现在改为使用渠道编码 channelCode
|
||||
$channelCode = $this->request->param('channelCode', '');
|
||||
$amount = $this->request->param('amount', 0); // 金额单位:元
|
||||
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
// 参数验证
|
||||
if (empty($channelCode)) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '渠道编码不能为空',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 验证金额(转换为浮点数进行验证)
|
||||
$amount = floatval($amount);
|
||||
if (empty($amount) || $amount <= 0) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '提现金额必须大于0',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 验证金额格式(最多2位小数)
|
||||
if (!preg_match('/^\d+(\.\d{1,2})?$/', (string)$amount)) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '提现金额格式不正确,最多保留2位小数',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查渠道是否存在且属于当前公司(通过渠道编码查询)
|
||||
$channel = Db::name('distribution_channel')
|
||||
->where([
|
||||
['code', '=', $channelCode],
|
||||
['companyId', '=', $companyId],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$channel) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'success' => false,
|
||||
'msg' => '渠道不存在或没有权限',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 统一使用渠道ID变量,后续逻辑仍然基于 channelId
|
||||
$channelId = $channel['id'];
|
||||
|
||||
// 检查渠道状态
|
||||
if ($channel['status'] !== 'enabled') {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '渠道已禁用,无法申请提现',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查可提现金额
|
||||
// 数据库存储的是分,接口接收的是元,需要统一单位进行比较
|
||||
$withdrawableAmountInFen = intval($channel['withdrawableAmount'] ?? 0); // 数据库中的分
|
||||
$withdrawableAmountInYuan = round($withdrawableAmountInFen / 100, 2); // 转换为元用于提示
|
||||
$amountInFen = intval(round($amount * 100)); // 将接口接收的元转换为分
|
||||
|
||||
if ($amountInFen > $withdrawableAmountInFen) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '提现金额不能超过可提现金额(' . number_format($withdrawableAmountInYuan, 2) . '元)',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查是否有待审核的申请
|
||||
$pendingWithdrawal = Db::name('distribution_withdrawal')
|
||||
->where([
|
||||
['channelId', '=', $channelId],
|
||||
['companyId', '=', $companyId],
|
||||
['status', '=', DistributionWithdrawal::STATUS_PENDING]
|
||||
])
|
||||
->find();
|
||||
|
||||
if ($pendingWithdrawal) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '该渠道已有待审核的提现申请,请等待审核完成后再申请',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 创建提现申请(金额以分存储)
|
||||
$withdrawalData = [
|
||||
'companyId' => $companyId,
|
||||
'channelId' => $channelId,
|
||||
'amount' => $amountInFen, // 存储为分
|
||||
'status' => DistributionWithdrawal::STATUS_PENDING,
|
||||
'applyTime' => time(),
|
||||
'createTime' => time(),
|
||||
'updateTime' => time(),
|
||||
];
|
||||
|
||||
$withdrawalId = Db::name('distribution_withdrawal')->insertGetId($withdrawalData);
|
||||
|
||||
if (!$withdrawalId) {
|
||||
throw new Exception('创建提现申请失败');
|
||||
}
|
||||
|
||||
// 扣除渠道可提现金额(以分为单位)
|
||||
Db::name('distribution_channel')
|
||||
->where('id', $channelId)
|
||||
->setDec('withdrawableAmount', $amountInFen);
|
||||
|
||||
// 提交事务
|
||||
Db::commit();
|
||||
|
||||
// 获取创建的申请数据
|
||||
$withdrawal = Db::name('distribution_withdrawal')
|
||||
->alias('w')
|
||||
->join('distribution_channel c', 'w.channelId = c.id', 'left')
|
||||
->where('w.id', $withdrawalId)
|
||||
->field([
|
||||
'w.id',
|
||||
'w.channelId',
|
||||
'w.amount',
|
||||
'w.status',
|
||||
'w.payType',
|
||||
'w.applyTime',
|
||||
'c.name as channelName',
|
||||
'c.code as channelCode'
|
||||
])
|
||||
->find();
|
||||
|
||||
// 格式化返回数据(分转元)
|
||||
$result = [
|
||||
'id' => (string)$withdrawal['id'],
|
||||
'channelId' => (string)$withdrawal['channelId'],
|
||||
'channelName' => $withdrawal['channelName'] ?? '',
|
||||
'channelCode' => $withdrawal['channelCode'] ?? '',
|
||||
'amount' => round($withdrawal['amount'] / 100, 2), // 分转元,保留2位小数
|
||||
'status' => $withdrawal['status'],
|
||||
'payType' => !empty($withdrawal['payType']) ? $withdrawal['payType'] : null, // 支付类型:wechat、alipay、bankcard(创建时为null)
|
||||
'applyDate' => !empty($withdrawal['applyTime']) ? date('Y/m/d', $withdrawal['applyTime']) : '',
|
||||
'reviewDate' => null,
|
||||
'reviewer' => null,
|
||||
'remark' => null,
|
||||
];
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'success' => true,
|
||||
'msg' => '提现申请提交成功',
|
||||
'data' => $result
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
return json([
|
||||
'code' => $e->getCode() ?: 500,
|
||||
'success' => false,
|
||||
'msg' => '提交提现申请失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核提现申请(通过/拒绝)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function review()
|
||||
{
|
||||
try {
|
||||
// 获取参数
|
||||
$id = $this->request->param('id', 0);
|
||||
$action = $this->request->param('action', ''); // approve 或 reject
|
||||
$remark = $this->request->param('remark', '');
|
||||
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
$reviewer = $this->getUserInfo('username') ?: $this->getUserInfo('account') ?: '系统管理员';
|
||||
|
||||
// 参数验证
|
||||
if (empty($id)) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '申请ID不能为空',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
if (!in_array($action, ['approve', 'reject'])) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '审核操作参数错误,必须为 approve 或 reject',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 如果是拒绝,备注必填
|
||||
if ($action === 'reject' && empty($remark)) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '拒绝申请时,拒绝理由不能为空',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查申请是否存在且属于当前公司
|
||||
$withdrawal = Db::name('distribution_withdrawal')
|
||||
->where([
|
||||
['id', '=', $id],
|
||||
['companyId', '=', $companyId]
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$withdrawal) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'success' => false,
|
||||
'msg' => '提现申请不存在或没有权限',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查申请状态
|
||||
if ($withdrawal['status'] !== DistributionWithdrawal::STATUS_PENDING) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '该申请已审核,无法重复审核',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
Db::startTrans();
|
||||
try {
|
||||
$updateData = [
|
||||
'reviewTime' => time(),
|
||||
'reviewer' => $reviewer,
|
||||
'remark' => $remark ?: '',
|
||||
'updateTime' => time(),
|
||||
];
|
||||
|
||||
if ($action === 'approve') {
|
||||
// 审核通过
|
||||
$updateData['status'] = DistributionWithdrawal::STATUS_APPROVED;
|
||||
} else {
|
||||
// 审核拒绝,退回可提现金额(金额以分存储)
|
||||
$updateData['status'] = DistributionWithdrawal::STATUS_REJECTED;
|
||||
|
||||
// 退回渠道可提现金额(以分为单位)
|
||||
Db::name('distribution_channel')
|
||||
->where('id', $withdrawal['channelId'])
|
||||
->setInc('withdrawableAmount', intval($withdrawal['amount']));
|
||||
}
|
||||
|
||||
// 更新申请状态
|
||||
Db::name('distribution_withdrawal')
|
||||
->where('id', $id)
|
||||
->update($updateData);
|
||||
|
||||
// 提交事务
|
||||
Db::commit();
|
||||
|
||||
$msg = $action === 'approve' ? '审核通过成功' : '审核拒绝成功';
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'success' => true,
|
||||
'msg' => $msg,
|
||||
'data' => [
|
||||
'id' => (string)$id,
|
||||
'status' => $updateData['status']
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
return json([
|
||||
'code' => $e->getCode() ?: 500,
|
||||
'success' => false,
|
||||
'msg' => '审核失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打款(标记为已打款)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function markPaid()
|
||||
{
|
||||
try {
|
||||
// 获取参数
|
||||
$id = $this->request->param('id', 0);
|
||||
$payType = $this->request->param('payType', ''); // 支付类型:wechat、alipay、bankcard
|
||||
$remark = $this->request->param('remark', '');
|
||||
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
// 参数验证
|
||||
if (empty($id)) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '申请ID不能为空',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 验证支付类型
|
||||
$validPayTypes = [
|
||||
DistributionWithdrawal::PAY_TYPE_WECHAT,
|
||||
DistributionWithdrawal::PAY_TYPE_ALIPAY,
|
||||
DistributionWithdrawal::PAY_TYPE_BANKCARD
|
||||
];
|
||||
if (empty($payType) || !in_array($payType, $validPayTypes)) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '支付类型不能为空,必须为:wechat(微信)、alipay(支付宝)、bankcard(银行卡)',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查申请是否存在且属于当前公司
|
||||
$withdrawal = Db::name('distribution_withdrawal')
|
||||
->where([
|
||||
['id', '=', $id],
|
||||
['companyId', '=', $companyId]
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$withdrawal) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'success' => false,
|
||||
'msg' => '提现申请不存在或没有权限',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查申请状态(只有已通过的申请才能打款)
|
||||
if ($withdrawal['status'] !== DistributionWithdrawal::STATUS_APPROVED) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '只有已通过的申请才能标记为已打款',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 更新状态为已打款
|
||||
$result = Db::name('distribution_withdrawal')
|
||||
->where('id', $id)
|
||||
->update([
|
||||
'status' => DistributionWithdrawal::STATUS_PAID,
|
||||
'payType' => $payType,
|
||||
'remark' => !empty($remark) ? $remark : $withdrawal['remark'],
|
||||
'updateTime' => time()
|
||||
]);
|
||||
|
||||
if ($result === false) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'success' => false,
|
||||
'msg' => '标记打款失败',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'success' => true,
|
||||
'msg' => '标记打款成功',
|
||||
'data' => [
|
||||
'id' => (string)$id,
|
||||
'status' => DistributionWithdrawal::STATUS_PAID,
|
||||
'payType' => $payType
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return json([
|
||||
'code' => $e->getCode() ?: 500,
|
||||
'success' => false,
|
||||
'msg' => '标记打款失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提现申请详情
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function detail()
|
||||
{
|
||||
try {
|
||||
// 获取参数
|
||||
$id = $this->request->param('id', 0);
|
||||
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
// 参数验证
|
||||
if (empty($id)) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'success' => false,
|
||||
'msg' => '申请ID不能为空',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 查询申请详情(关联渠道表)
|
||||
$withdrawal = Db::name('distribution_withdrawal')
|
||||
->alias('w')
|
||||
->join('distribution_channel c', 'w.channelId = c.id AND c.deleteTime = 0', 'left')
|
||||
->where([
|
||||
['w.id', '=', $id],
|
||||
['w.companyId', '=', $companyId]
|
||||
])
|
||||
->field([
|
||||
'w.id',
|
||||
'w.channelId',
|
||||
'w.amount',
|
||||
'w.status',
|
||||
'w.payType',
|
||||
'w.applyTime',
|
||||
'w.reviewTime',
|
||||
'w.reviewer',
|
||||
'w.remark',
|
||||
'c.name as channelName',
|
||||
'c.code as channelCode'
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$withdrawal) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'success' => false,
|
||||
'msg' => '提现申请不存在或没有权限',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 格式化返回数据(分转元)
|
||||
$result = [
|
||||
'id' => (string)$withdrawal['id'],
|
||||
'channelId' => (string)$withdrawal['channelId'],
|
||||
'channelName' => $withdrawal['channelName'] ?? '',
|
||||
'channelCode' => $withdrawal['channelCode'] ?? '',
|
||||
'amount' => round($withdrawal['amount'] / 100, 2), // 分转元,保留2位小数
|
||||
'status' => $withdrawal['status'],
|
||||
'payType' => !empty($withdrawal['payType']) ? $withdrawal['payType'] : null, // 支付类型:wechat、alipay、bankcard
|
||||
'applyDate' => !empty($withdrawal['applyTime']) ? date('Y/m/d', $withdrawal['applyTime']) : '',
|
||||
'reviewDate' => !empty($withdrawal['reviewTime']) ? date('Y-m-d H:i:s', $withdrawal['reviewTime']) : null,
|
||||
'reviewer' => !empty($withdrawal['reviewer']) ? $withdrawal['reviewer'] : null,
|
||||
'remark' => !empty($withdrawal['remark']) ? $withdrawal['remark'] : null,
|
||||
];
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'success' => true,
|
||||
'msg' => '获取成功',
|
||||
'data' => $result
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return json([
|
||||
'code' => $e->getCode() ?: 500,
|
||||
'success' => false,
|
||||
'msg' => '获取详情失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,40 @@ class GetAddFriendPlanDetailV1Controller extends Controller
|
||||
$msgConf = json_decode($plan['msgConf'], true) ?: [];
|
||||
$tagConf = json_decode($plan['tagConf'], true) ?: [];
|
||||
|
||||
// 处理分销配置
|
||||
$distributionConfig = $sceneConf['distribution'] ?? [
|
||||
'enabled' => false,
|
||||
'channels' => [],
|
||||
'customerRewardAmount' => 0,
|
||||
'addFriendRewardAmount' => 0,
|
||||
];
|
||||
|
||||
// 格式化分销配置(分转元,并获取渠道详情)
|
||||
$distributionEnabled = !empty($distributionConfig['enabled']);
|
||||
$distributionChannels = [];
|
||||
if ($distributionEnabled && !empty($distributionConfig['channels'])) {
|
||||
$channels = Db::name('distribution_channel')
|
||||
->where([
|
||||
['id', 'in', $distributionConfig['channels']],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->field('id,code,name')
|
||||
->select();
|
||||
$distributionChannels = array_map(function($channel) {
|
||||
return [
|
||||
'id' => (int)$channel['id'],
|
||||
'code' => $channel['code'],
|
||||
'name' => $channel['name']
|
||||
];
|
||||
}, $channels);
|
||||
}
|
||||
|
||||
// 将分销配置添加到返回数据中
|
||||
$sceneConf['distributionEnabled'] = $distributionEnabled;
|
||||
$sceneConf['distributionChannels'] = $distributionChannels;
|
||||
$sceneConf['customerRewardAmount'] = round(($distributionConfig['customerRewardAmount'] ?? 0) / 100, 2); // 分转元
|
||||
$sceneConf['addFriendRewardAmount'] = round(($distributionConfig['addFriendRewardAmount'] ?? 0) / 100, 2); // 分转元
|
||||
|
||||
|
||||
|
||||
if(!empty($sceneConf['wechatGroups'])){
|
||||
|
||||
@@ -69,6 +69,10 @@ class PostCreateAddFriendPlanV1Controller extends BaseController
|
||||
return ResponseHelper::error('请选择设备', 400);
|
||||
}
|
||||
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
// 处理分销配置
|
||||
$distributionConfig = $this->processDistributionConfig($params, $companyId);
|
||||
|
||||
// 归类参数
|
||||
$msgConf = isset($params['messagePlans']) ? $params['messagePlans'] : [];
|
||||
@@ -106,9 +110,16 @@ class PostCreateAddFriendPlanV1Controller extends BaseController
|
||||
$sceneConf['addFriendInterval'],
|
||||
$sceneConf['startTime'],
|
||||
$sceneConf['orderTableFile'],
|
||||
$sceneConf['endTime']
|
||||
$sceneConf['endTime'],
|
||||
$sceneConf['distributionEnabled'],
|
||||
$sceneConf['distributionChannels'],
|
||||
$sceneConf['customerRewardAmount'],
|
||||
$sceneConf['addFriendRewardAmount']
|
||||
);
|
||||
|
||||
// 将分销配置添加到sceneConf中
|
||||
$sceneConf['distribution'] = $distributionConfig;
|
||||
|
||||
// 构建数据
|
||||
$data = [
|
||||
'name' => $params['name'],
|
||||
@@ -327,4 +338,75 @@ class PostCreateAddFriendPlanV1Controller extends BaseController
|
||||
json_decode($string);
|
||||
return (json_last_error() == JSON_ERROR_NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理分销配置
|
||||
*
|
||||
* @param array $params 请求参数
|
||||
* @param int $companyId 公司ID
|
||||
* @return array 分销配置
|
||||
*/
|
||||
private function processDistributionConfig($params, $companyId)
|
||||
{
|
||||
$distributionEnabled = !empty($params['distributionEnabled']) ? true : false;
|
||||
|
||||
$config = [
|
||||
'enabled' => $distributionEnabled,
|
||||
'channels' => [],
|
||||
'customerRewardAmount' => 0, // 获客奖励金额(分)
|
||||
'addFriendRewardAmount' => 0, // 添加奖励金额(分)
|
||||
];
|
||||
|
||||
// 如果未开启分销,直接返回默认配置
|
||||
if (!$distributionEnabled) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
// 验证渠道ID
|
||||
$channelIds = $params['distributionChannels'] ?? [];
|
||||
if (empty($channelIds) || !is_array($channelIds)) {
|
||||
throw new \Exception('请选择至少一个分销渠道');
|
||||
}
|
||||
|
||||
// 查询有效的渠道(只保留存在且已启用的渠道)
|
||||
$channels = Db::name('distribution_channel')
|
||||
->where([
|
||||
['id', 'in', $channelIds],
|
||||
['companyId', '=', $companyId],
|
||||
['status', '=', 'enabled'],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->field('id,code,name')
|
||||
->select();
|
||||
|
||||
// 如果没有有效渠道,才报错
|
||||
if (empty($channels)) {
|
||||
throw new \Exception('所选的分销渠道均不存在或已被禁用,请重新选择');
|
||||
}
|
||||
|
||||
// 只保留有效的渠道ID
|
||||
$config['channels'] = array_column($channels, 'id');
|
||||
|
||||
// 验证获客奖励金额(元转分)
|
||||
$customerRewardAmount = isset($params['customerRewardAmount']) ? floatval($params['customerRewardAmount']) : 0;
|
||||
if ($customerRewardAmount < 0) {
|
||||
throw new \Exception('获客奖励金额不能为负数');
|
||||
}
|
||||
if ($customerRewardAmount > 0 && !preg_match('/^\d+(\.\d{1,2})?$/', (string)$customerRewardAmount)) {
|
||||
throw new \Exception('获客奖励金额格式不正确,最多保留2位小数');
|
||||
}
|
||||
$config['customerRewardAmount'] = intval(round($customerRewardAmount * 100)); // 元转分
|
||||
|
||||
// 验证添加奖励金额(元转分)
|
||||
$addFriendRewardAmount = isset($params['addFriendRewardAmount']) ? floatval($params['addFriendRewardAmount']) : 0;
|
||||
if ($addFriendRewardAmount < 0) {
|
||||
throw new \Exception('添加奖励金额不能为负数');
|
||||
}
|
||||
if ($addFriendRewardAmount > 0 && !preg_match('/^\d+(\.\d{1,2})?$/', (string)$addFriendRewardAmount)) {
|
||||
throw new \Exception('添加奖励金额格式不正确,最多保留2位小数');
|
||||
}
|
||||
$config['addFriendRewardAmount'] = intval(round($addFriendRewardAmount * 100)); // 元转分
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace app\cunkebao\controller\plan;
|
||||
use library\ResponseHelper;
|
||||
use think\Controller;
|
||||
use think\Db;
|
||||
use app\cunkebao\service\DistributionRewardService;
|
||||
|
||||
/**
|
||||
* 对外API接口控制器
|
||||
@@ -91,6 +92,9 @@ class PostExternalApiV1Controller extends Controller
|
||||
|
||||
$identifier = !empty($params['wechatId']) ? $params['wechatId'] : $params['phone'];
|
||||
|
||||
// 渠道ID(cid),对应 distribution_channel.id
|
||||
$channelId = !empty($params['cid']) ? intval($params['cid']) : 0;
|
||||
|
||||
|
||||
$trafficPool = Db::name('traffic_pool')->where('identifier', $identifier)->find();
|
||||
if (!$trafficPool) {
|
||||
@@ -103,17 +107,44 @@ class PostExternalApiV1Controller extends Controller
|
||||
$trafficPoolId = $trafficPool['id'];
|
||||
}
|
||||
|
||||
$taskCustomer = Db::name('task_customer')->where('task_id', $plan['id'])->where('phone', $identifier)->find();
|
||||
$taskCustomer = Db::name('task_customer')
|
||||
->where('task_id', $plan['id'])
|
||||
->where('phone', $identifier)
|
||||
->find();
|
||||
|
||||
// 处理用户画像
|
||||
if(!empty($params['portrait']) && is_array($params['portrait'])){
|
||||
$this->updatePortrait($params['portrait'],$trafficPoolId,$plan['companyId']);
|
||||
}
|
||||
if (!$taskCustomer) {
|
||||
$tags = !empty($params['tags']) ? explode(',',$params['tags']) : [];
|
||||
$siteTags = !empty($params['siteTags']) ? explode(',',$params['siteTags']) : [];
|
||||
Db::name('task_customer')->insert([
|
||||
$tags = !empty($params['tags']) ? explode(',', $params['tags']) : [];
|
||||
$siteTags = !empty($params['siteTags']) ? explode(',', $params['siteTags']) : [];
|
||||
|
||||
// 处理渠道ID:只有在分销配置中允许、且渠道本身正常时,才记录到task_customer
|
||||
$finalChannelId = 0;
|
||||
if ($channelId > 0) {
|
||||
$sceneConf = json_decode($plan['sceneConf'], true) ?: [];
|
||||
$distributionConfig = $sceneConf['distribution'] ?? null;
|
||||
$allowedChannelIds = $distributionConfig['channels'] ?? [];
|
||||
if (!empty($distributionConfig) && !empty($distributionConfig['enabled']) && in_array($channelId, $allowedChannelIds)) {
|
||||
// 验证渠道是否存在且正常
|
||||
$channel = Db::name('distribution_channel')
|
||||
->where([
|
||||
['id', '=', $channelId],
|
||||
['companyId', '=', $plan['companyId']],
|
||||
['status', '=', 'enabled'],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->find();
|
||||
if ($channel) {
|
||||
$finalChannelId = intval($channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$customerId = Db::name('task_customer')->insertGetId([
|
||||
'task_id' => $plan['id'],
|
||||
'channelId' => $finalChannelId,
|
||||
'phone' => $identifier,
|
||||
'name' => !empty($params['name']) ? $params['name'] : '',
|
||||
'source' => !empty($params['source']) ? $params['source'] : '',
|
||||
@@ -123,6 +154,19 @@ class PostExternalApiV1Controller extends Controller
|
||||
'createTime' => time(),
|
||||
]);
|
||||
|
||||
// 记录获客奖励(异步处理,不影响主流程)
|
||||
if ($customerId) {
|
||||
try {
|
||||
// 只有在存在有效渠道ID时才触发分佣
|
||||
if ($finalChannelId > 0) {
|
||||
DistributionRewardService::recordCustomerReward($plan['id'], $customerId, $identifier, $finalChannelId);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 记录错误但不影响主流程
|
||||
\think\facade\Log::error('记录获客奖励失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'message' => '新增成功',
|
||||
|
||||
@@ -48,6 +48,11 @@ class PostUpdateAddFriendPlanV1Controller extends BaseController
|
||||
return ResponseHelper::error('计划不存在', 404);
|
||||
}
|
||||
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
// 处理分销配置
|
||||
$distributionConfig = $this->processDistributionConfig($params, $companyId);
|
||||
|
||||
// 归类参数
|
||||
$msgConf = isset($params['messagePlans']) ? $params['messagePlans'] : [];
|
||||
$tagConf = [
|
||||
@@ -85,9 +90,16 @@ class PostUpdateAddFriendPlanV1Controller extends BaseController
|
||||
$sceneConf['addFriendInterval'],
|
||||
$sceneConf['startTime'],
|
||||
$sceneConf['orderTableFile'],
|
||||
$sceneConf['endTime']
|
||||
$sceneConf['endTime'],
|
||||
$sceneConf['distributionEnabled'],
|
||||
$sceneConf['distributionChannels'],
|
||||
$sceneConf['customerRewardAmount'],
|
||||
$sceneConf['addFriendRewardAmount']
|
||||
);
|
||||
|
||||
// 将分销配置添加到sceneConf中
|
||||
$sceneConf['distribution'] = $distributionConfig;
|
||||
|
||||
// 构建更新数据
|
||||
$data = [
|
||||
'name' => $params['name'],
|
||||
@@ -283,4 +295,75 @@ class PostUpdateAddFriendPlanV1Controller extends BaseController
|
||||
return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理分销配置
|
||||
*
|
||||
* @param array $params 请求参数
|
||||
* @param int $companyId 公司ID
|
||||
* @return array 分销配置
|
||||
*/
|
||||
private function processDistributionConfig($params, $companyId)
|
||||
{
|
||||
$distributionEnabled = !empty($params['distributionEnabled']) ? true : false;
|
||||
|
||||
$config = [
|
||||
'enabled' => $distributionEnabled,
|
||||
'channels' => [],
|
||||
'customerRewardAmount' => 0, // 获客奖励金额(分)
|
||||
'addFriendRewardAmount' => 0, // 添加奖励金额(分)
|
||||
];
|
||||
|
||||
// 如果未开启分销,直接返回默认配置
|
||||
if (!$distributionEnabled) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
// 验证渠道ID
|
||||
$channelIds = $params['distributionChannels'] ?? [];
|
||||
if (empty($channelIds) || !is_array($channelIds)) {
|
||||
throw new \Exception('请选择至少一个分销渠道');
|
||||
}
|
||||
|
||||
// 查询有效的渠道(只保留存在且已启用的渠道)
|
||||
$channels = Db::name('distribution_channel')
|
||||
->where([
|
||||
['id', 'in', $channelIds],
|
||||
['companyId', '=', $companyId],
|
||||
['status', '=', 'enabled'],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->field('id,code,name')
|
||||
->select();
|
||||
|
||||
// 如果没有有效渠道,才报错
|
||||
if (empty($channels)) {
|
||||
throw new \Exception('所选的分销渠道均不存在或已被禁用,请重新选择');
|
||||
}
|
||||
|
||||
// 只保留有效的渠道ID
|
||||
$config['channels'] = array_column($channels, 'id');
|
||||
|
||||
// 验证获客奖励金额(元转分)
|
||||
$customerRewardAmount = isset($params['customerRewardAmount']) ? floatval($params['customerRewardAmount']) : 0;
|
||||
if ($customerRewardAmount < 0) {
|
||||
throw new \Exception('获客奖励金额不能为负数');
|
||||
}
|
||||
if ($customerRewardAmount > 0 && !preg_match('/^\d+(\.\d{1,2})?$/', (string)$customerRewardAmount)) {
|
||||
throw new \Exception('获客奖励金额格式不正确,最多保留2位小数');
|
||||
}
|
||||
$config['customerRewardAmount'] = intval(round($customerRewardAmount * 100)); // 元转分
|
||||
|
||||
// 验证添加奖励金额(元转分)
|
||||
$addFriendRewardAmount = isset($params['addFriendRewardAmount']) ? floatval($params['addFriendRewardAmount']) : 0;
|
||||
if ($addFriendRewardAmount < 0) {
|
||||
throw new \Exception('添加奖励金额不能为负数');
|
||||
}
|
||||
if ($addFriendRewardAmount > 0 && !preg_match('/^\d+(\.\d{1,2})?$/', (string)$addFriendRewardAmount)) {
|
||||
throw new \Exception('添加奖励金额格式不正确,最多保留2位小数');
|
||||
}
|
||||
$config['addFriendRewardAmount'] = intval(round($addFriendRewardAmount * 100)); // 元转分
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
@@ -113,10 +113,39 @@ class PosterWeChatMiniProgram extends Controller
|
||||
}
|
||||
// 2. 写入 ck_task_customer: 以 task_id ~~identifier~~ phone 为条件,如果存在则忽略,使用类似laravel的firstOrcreate(但我不知道thinkphp5.1里的写法)
|
||||
// $taskCustomer = Db::name('task_customer')->where('task_id', $taskId)->where('identifier', $result['phone_info']['phoneNumber'])->find();
|
||||
$taskCustomer = Db::name('task_customer')->where('task_id', $taskId)->where('phone', $result['phone_info']['phoneNumber'])->find();
|
||||
$taskCustomer = Db::name('task_customer')
|
||||
->where('task_id', $taskId)
|
||||
->where('phone', $result['phone_info']['phoneNumber'])
|
||||
->find();
|
||||
if (!$taskCustomer) {
|
||||
Db::name('task_customer')->insert([
|
||||
// 渠道ID(cid),对应 distribution_channel.id
|
||||
$channelId = intval($this->request->param('cid', 0));
|
||||
|
||||
$finalChannelId = 0;
|
||||
if ($channelId > 0) {
|
||||
// 获取任务信息,解析分销配置
|
||||
$sceneConf = json_decode($task['sceneConf'] ?? '[]', true) ?: [];
|
||||
$distributionConfig = $sceneConf['distribution'] ?? null;
|
||||
$allowedChannelIds = $distributionConfig['channels'] ?? [];
|
||||
if (!empty($distributionConfig) && !empty($distributionConfig['enabled']) && in_array($channelId, $allowedChannelIds)) {
|
||||
// 验证渠道是否存在且正常
|
||||
$channel = Db::name('distribution_channel')
|
||||
->where([
|
||||
['id', '=', $channelId],
|
||||
['companyId', '=', $task['companyId']],
|
||||
['status', '=', 'enabled'],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->find();
|
||||
if ($channel) {
|
||||
$finalChannelId = $channelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$customerId = Db::name('task_customer')->insertGetId([
|
||||
'task_id' => $taskId,
|
||||
'channelId' => $finalChannelId,
|
||||
// 'identifier' => $result['phone_info']['phoneNumber'],
|
||||
'phone' => $result['phone_info']['phoneNumber'],
|
||||
'source' => $task['name'],
|
||||
@@ -124,6 +153,23 @@ class PosterWeChatMiniProgram extends Controller
|
||||
'tags' => json_encode([]),
|
||||
'siteTags' => json_encode([]),
|
||||
]);
|
||||
|
||||
// 记录获客奖励(异步处理,不影响主流程)
|
||||
if ($customerId) {
|
||||
try {
|
||||
if ($finalChannelId > 0) {
|
||||
\app\cunkebao\service\DistributionRewardService::recordCustomerReward(
|
||||
$taskId,
|
||||
$customerId,
|
||||
$result['phone_info']['phoneNumber'],
|
||||
$finalChannelId
|
||||
);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 记录错误但不影响主流程
|
||||
\think\facade\Log::error('记录获客奖励失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
// return $result['phone_info']['phoneNumber'];
|
||||
return json([
|
||||
@@ -149,6 +195,8 @@ class PosterWeChatMiniProgram extends Controller
|
||||
|
||||
$taskId = request()->param('id');
|
||||
$rawInput = trim((string)request()->param('phone', ''));
|
||||
// 渠道ID(cid),对应 distribution_channel.id
|
||||
$channelId = intval(request()->param('cid', 0));
|
||||
if ($rawInput === '') {
|
||||
return json([
|
||||
'code' => 400,
|
||||
@@ -164,6 +212,28 @@ class PosterWeChatMiniProgram extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
// 预先根据任务的分销配置校验渠道是否有效(仅当传入了cid时)
|
||||
$finalChannelId = 0;
|
||||
if ($channelId > 0) {
|
||||
$sceneConf = json_decode($task['sceneConf'] ?? '[]', true) ?: [];
|
||||
$distributionConfig = $sceneConf['distribution'] ?? null;
|
||||
$allowedChannelIds = $distributionConfig['channels'] ?? [];
|
||||
if (!empty($distributionConfig) && !empty($distributionConfig['enabled']) && in_array($channelId, $allowedChannelIds)) {
|
||||
// 验证渠道是否存在且正常
|
||||
$channel = Db::name('distribution_channel')
|
||||
->where([
|
||||
['id', '=', $channelId],
|
||||
['companyId', '=', $task['companyId']],
|
||||
['status', '=', 'enabled'],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->find();
|
||||
if ($channel) {
|
||||
$finalChannelId = $channelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$lines = preg_split('/\r\n|\r|\n/', $rawInput);
|
||||
foreach ($lines as $line) {
|
||||
@@ -210,17 +280,35 @@ class PosterWeChatMiniProgram extends Controller
|
||||
->find();
|
||||
if (empty($taskCustomer)) {
|
||||
$insertCustomer = [
|
||||
'task_id' => $taskId,
|
||||
'phone' => $identifier,
|
||||
'source' => $task['name'],
|
||||
'createTime' => time(),
|
||||
'tags' => json_encode([]),
|
||||
'siteTags' => json_encode([]),
|
||||
'task_id' => $taskId,
|
||||
'channelId' => $finalChannelId, // 记录本次导入归属的分销渠道(如有)
|
||||
'phone' => $identifier,
|
||||
'source' => $task['name'],
|
||||
'createTime'=> time(),
|
||||
'tags' => json_encode([]),
|
||||
'siteTags' => json_encode([]),
|
||||
];
|
||||
if ($remark !== '') {
|
||||
$insertCustomer['remark'] = $remark;
|
||||
}
|
||||
Db::name('task_customer')->insert($insertCustomer);
|
||||
// 使用 insertGetId 以便在需要时记录获客奖励
|
||||
$customerId = Db::name('task_customer')->insertGetId($insertCustomer);
|
||||
|
||||
// 表单录入成功即视为一次获客:
|
||||
// 仅在存在有效渠道ID时,记录获客奖励(谁的cid谁获客)
|
||||
if (!empty($customerId) && $finalChannelId > 0) {
|
||||
try {
|
||||
\app\cunkebao\service\DistributionRewardService::recordCustomerReward(
|
||||
$taskId,
|
||||
$customerId,
|
||||
$identifier,
|
||||
$finalChannelId
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// 记录错误但不影响主流程
|
||||
\think\facade\Log::error('记录获客奖励失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
} elseif ($remark !== '' && $taskCustomer['remark'] !== $remark) {
|
||||
Db::name('task_customer')
|
||||
->where('id', $taskCustomer['id'])
|
||||
|
||||
@@ -100,7 +100,8 @@ class GetWechatMomentsV1Controller extends BaseController
|
||||
}
|
||||
|
||||
$query = Db::table('s2_wechat_moments')
|
||||
->where('wechatAccountId', $accountId);
|
||||
->where('wechatAccountId', $accountId)
|
||||
->where('userName', $wechatId);
|
||||
|
||||
// 关键词搜索
|
||||
if ($keyword = trim((string)$this->request->param('keyword', ''))) {
|
||||
|
||||
87
Server/application/cunkebao/model/DistributionChannel.php
Normal file
87
Server/application/cunkebao/model/DistributionChannel.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\model;
|
||||
|
||||
use app\cunkebao\model\BaseModel;
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 分销渠道模型
|
||||
*/
|
||||
class DistributionChannel extends BaseModel
|
||||
{
|
||||
// 设置表名
|
||||
protected $name = 'distribution_channel';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $deleteTime = 'deleteTime';
|
||||
protected $defaultSoftDelete = 0;
|
||||
|
||||
// 类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'companyId' => 'integer',
|
||||
'totalCustomers' => 'integer',
|
||||
'todayCustomers' => 'integer',
|
||||
'totalFriends' => 'integer',
|
||||
'todayFriends' => 'integer',
|
||||
'withdrawableAmount' => 'integer',
|
||||
'createTime' => 'timestamp',
|
||||
'updateTime' => 'timestamp',
|
||||
'deleteTime' => 'timestamp',
|
||||
];
|
||||
|
||||
/**
|
||||
* 生成渠道编码
|
||||
* 格式:QD + 时间戳 + 9位随机字符串
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function generateChannelCode()
|
||||
{
|
||||
$prefix = 'QD';
|
||||
$timestamp = time();
|
||||
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
$randomStr = '';
|
||||
|
||||
// 生成9位随机字符串
|
||||
for ($i = 0; $i < 9; $i++) {
|
||||
$randomStr .= $chars[mt_rand(0, strlen($chars) - 1)];
|
||||
}
|
||||
|
||||
$code = $prefix . $timestamp . $randomStr;
|
||||
|
||||
// 检查是否已存在
|
||||
$exists = self::where('code', $code)->find();
|
||||
if ($exists) {
|
||||
// 如果已存在,递归重新生成
|
||||
return self::generateChannelCode();
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建类型:manual(手动创建)
|
||||
*/
|
||||
const CREATE_TYPE_MANUAL = 'manual';
|
||||
|
||||
/**
|
||||
* 创建类型:auto(扫码创建)
|
||||
*/
|
||||
const CREATE_TYPE_AUTO = 'auto';
|
||||
|
||||
/**
|
||||
* 状态:enabled(启用)
|
||||
*/
|
||||
const STATUS_ENABLED = 'enabled';
|
||||
|
||||
/**
|
||||
* 状态:disabled(禁用)
|
||||
*/
|
||||
const STATUS_DISABLED = 'disabled';
|
||||
}
|
||||
|
||||
71
Server/application/cunkebao/model/DistributionWithdrawal.php
Normal file
71
Server/application/cunkebao/model/DistributionWithdrawal.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\model;
|
||||
|
||||
use app\cunkebao\model\BaseModel;
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 分销渠道提现申请模型
|
||||
*/
|
||||
class DistributionWithdrawal extends BaseModel
|
||||
{
|
||||
// 设置表名
|
||||
protected $name = 'distribution_withdrawal';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'companyId' => 'integer',
|
||||
'channelId' => 'integer',
|
||||
'amount' => 'integer',
|
||||
'reviewTime' => 'timestamp',
|
||||
'applyTime' => 'timestamp',
|
||||
'createTime' => 'timestamp',
|
||||
'updateTime' => 'timestamp',
|
||||
];
|
||||
|
||||
/**
|
||||
* 状态:pending(待审核)
|
||||
*/
|
||||
const STATUS_PENDING = 'pending';
|
||||
|
||||
/**
|
||||
* 状态:approved(已通过)
|
||||
*/
|
||||
const STATUS_APPROVED = 'approved';
|
||||
|
||||
/**
|
||||
* 状态:rejected(已拒绝)
|
||||
*/
|
||||
const STATUS_REJECTED = 'rejected';
|
||||
|
||||
/**
|
||||
* 状态:paid(已打款)
|
||||
*/
|
||||
const STATUS_PAID = 'paid';
|
||||
|
||||
/**
|
||||
* 支付类型:wechat(微信)
|
||||
*/
|
||||
const PAY_TYPE_WECHAT = 'wechat';
|
||||
|
||||
/**
|
||||
* 支付类型:alipay(支付宝)
|
||||
*/
|
||||
const PAY_TYPE_ALIPAY = 'alipay';
|
||||
|
||||
/**
|
||||
* 支付类型:bankcard(银行卡)
|
||||
*/
|
||||
const PAY_TYPE_BANKCARD = 'bankcard';
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\service;
|
||||
|
||||
use think\Db;
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* 分销奖励服务类
|
||||
* 处理获客和添加好友时的分销收益记录
|
||||
*/
|
||||
class DistributionRewardService
|
||||
{
|
||||
/**
|
||||
* 记录获客奖励
|
||||
*
|
||||
* @param int $taskId 获客计划ID
|
||||
* @param int $customerId 客户ID(task_customer表的id)
|
||||
* @param string $phone 客户手机号
|
||||
* @param int|null $channelId 渠道ID(分销渠道ID,对应distribution_channel.id)。为空时按配置的所有渠道分配
|
||||
* @return bool
|
||||
*/
|
||||
public static function recordCustomerReward($taskId, $customerId, $phone, $channelId = null)
|
||||
{
|
||||
try {
|
||||
// 获取获客计划信息
|
||||
$task = Db::name('customer_acquisition_task')
|
||||
->where('id', $taskId)
|
||||
->find();
|
||||
|
||||
if (!$task) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解析分销配置
|
||||
$sceneConf = json_decode($task['sceneConf'], true) ?: [];
|
||||
$distributionConfig = $sceneConf['distribution'] ?? null;
|
||||
|
||||
// 检查是否开启分销
|
||||
if (empty($distributionConfig) || empty($distributionConfig['enabled'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有获客奖励
|
||||
$rewardAmount = intval($distributionConfig['customerRewardAmount'] ?? 0);
|
||||
if ($rewardAmount <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取渠道列表(从分销配置中获取允许分佣的渠道)
|
||||
$channelIds = $distributionConfig['channels'] ?? [];
|
||||
if (empty($channelIds) || !is_array($channelIds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$companyId = $task['companyId'];
|
||||
$sceneId = $task['sceneId'];
|
||||
|
||||
// 获取场景名称(用于展示来源类型)
|
||||
$scene = Db::name('plan_scene')
|
||||
->where('id', $sceneId)
|
||||
->field('name')
|
||||
->find();
|
||||
$sceneName = $scene['name'] ?? '未知场景';
|
||||
|
||||
// 如果指定了 channelId(cid),仅允许该渠道获得分佣
|
||||
if (!empty($channelId)) {
|
||||
// 必须在配置的渠道列表中,且是有效ID
|
||||
if (!in_array($channelId, $channelIds)) {
|
||||
// 该渠道不在本计划允许分佣的渠道列表中,直接返回
|
||||
return false;
|
||||
}
|
||||
$channelIds = [$channelId];
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 为每个渠道记录收益并更新可提现金额
|
||||
foreach ($channelIds as $channelId) {
|
||||
// 验证渠道是否存在
|
||||
$channel = Db::name('distribution_channel')
|
||||
->where([
|
||||
['id', '=', $channelId],
|
||||
['companyId', '=', $companyId],
|
||||
['status', '=', 'enabled'],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$channel) {
|
||||
continue; // 跳过不存在的渠道
|
||||
}
|
||||
|
||||
// 记录收益明细
|
||||
Db::name('distribution_revenue_record')->insert([
|
||||
'companyId' => $companyId,
|
||||
'channelId' => $channelId,
|
||||
'channelCode' => $channel['code'],
|
||||
'type' => 'customer_acquisition', // 获客类型
|
||||
'sourceType' => $sceneName,
|
||||
'sourceId' => $taskId, // 活动ID(获客任务ID)
|
||||
'amount' => $rewardAmount, // 金额(分)
|
||||
'remark' => '获客奖励:' . $phone,
|
||||
'createTime' => time(),
|
||||
'updateTime' => time(),
|
||||
]);
|
||||
|
||||
// 更新渠道可提现金额
|
||||
Db::name('distribution_channel')
|
||||
->where('id', $channelId)
|
||||
->setInc('withdrawableAmount', $rewardAmount);
|
||||
|
||||
// 更新渠道获客统计
|
||||
Db::name('distribution_channel')
|
||||
->where('id', $channelId)
|
||||
->setInc('totalCustomers', 1);
|
||||
|
||||
// 更新今日获客统计(如果是今天)
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
|
||||
$createTime = time();
|
||||
if ($createTime >= $todayStart && $createTime <= $todayEnd) {
|
||||
Db::name('distribution_channel')
|
||||
->where('id', $channelId)
|
||||
->setInc('todayCustomers', 1);
|
||||
}
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// 记录错误日志,但不影响主流程
|
||||
\think\Log::error('记录获客奖励失败:' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录添加好友奖励
|
||||
*
|
||||
* @param int $taskId 获客计划ID
|
||||
* @param int $customerId 客户ID(task_customer表的id)
|
||||
* @param string $phone 客户手机号
|
||||
* @param int|null $channelId 渠道ID(分销渠道ID,对应distribution_channel.id)。为空时按配置的所有渠道分配
|
||||
* @return bool
|
||||
*/
|
||||
public static function recordAddFriendReward($taskId, $customerId, $phone, $channelId = null)
|
||||
{
|
||||
try {
|
||||
// 获取获客计划信息
|
||||
$task = Db::name('customer_acquisition_task')
|
||||
->where('id', $taskId)
|
||||
->find();
|
||||
|
||||
if (!$task) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解析分销配置
|
||||
$sceneConf = json_decode($task['sceneConf'], true) ?: [];
|
||||
$distributionConfig = $sceneConf['distribution'] ?? null;
|
||||
|
||||
// 检查是否开启分销
|
||||
if (empty($distributionConfig) || empty($distributionConfig['enabled'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有添加奖励
|
||||
$rewardAmount = intval($distributionConfig['addFriendRewardAmount'] ?? 0);
|
||||
if ($rewardAmount <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取渠道列表(从分销配置中获取允许分佣的渠道)
|
||||
$channelIds = $distributionConfig['channels'] ?? [];
|
||||
if (empty($channelIds) || !is_array($channelIds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$companyId = $task['companyId'];
|
||||
$sceneId = $task['sceneId'];
|
||||
|
||||
// 获取场景名称(用于展示来源类型)
|
||||
$scene = Db::name('plan_scene')
|
||||
->where('id', $sceneId)
|
||||
->field('name')
|
||||
->find();
|
||||
$sceneName = $scene['name'] ?? '未知场景';
|
||||
|
||||
// 如果指定了 channelId(cid),仅允许该渠道获得分佣
|
||||
if (!empty($channelId)) {
|
||||
// 必须在配置的渠道列表中,且是有效ID
|
||||
if (!in_array($channelId, $channelIds)) {
|
||||
// 该渠道不在本计划允许分佣的渠道列表中,直接返回
|
||||
return false;
|
||||
}
|
||||
$channelIds = [$channelId];
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 为每个渠道记录收益并更新可提现金额
|
||||
foreach ($channelIds as $channelId) {
|
||||
// 验证渠道是否存在
|
||||
$channel = Db::name('distribution_channel')
|
||||
->where([
|
||||
['id', '=', $channelId],
|
||||
['companyId', '=', $companyId],
|
||||
['status', '=', 'enabled'],
|
||||
['deleteTime', '=', 0]
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$channel) {
|
||||
continue; // 跳过不存在的渠道
|
||||
}
|
||||
|
||||
// 记录收益明细
|
||||
Db::name('distribution_revenue_record')->insert([
|
||||
'companyId' => $companyId,
|
||||
'channelId' => $channelId,
|
||||
'channelCode' => $channel['code'],
|
||||
'type' => 'add_friend', // 添加好友类型
|
||||
'sourceType' => $sceneName,
|
||||
'sourceId' => $taskId, // 活动ID(获客任务ID)
|
||||
'amount' => $rewardAmount, // 金额(分)
|
||||
'remark' => '添加好友奖励:' . $phone,
|
||||
'createTime' => time(),
|
||||
'updateTime' => time(),
|
||||
]);
|
||||
|
||||
// 更新渠道可提现金额
|
||||
Db::name('distribution_channel')
|
||||
->where('id', $channelId)
|
||||
->setInc('withdrawableAmount', $rewardAmount);
|
||||
|
||||
// 更新渠道好友统计
|
||||
Db::name('distribution_channel')
|
||||
->where('id', $channelId)
|
||||
->setInc('totalFriends', 1);
|
||||
|
||||
// 更新今日好友统计(如果是今天)
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
|
||||
$createTime = time();
|
||||
if ($createTime >= $todayStart && $createTime <= $todayEnd) {
|
||||
Db::name('distribution_channel')
|
||||
->where('id', $channelId)
|
||||
->setInc('todayFriends', 1);
|
||||
}
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// 记录错误日志,但不影响主流程
|
||||
\think\Log::error('记录添加好友奖励失败:' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
Server/application/cunkebao/validate/DistributionChannel.php
Normal file
31
Server/application/cunkebao/validate/DistributionChannel.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* 分销渠道验证器
|
||||
*/
|
||||
class DistributionChannel extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'name' => 'require|length:1,50',
|
||||
'phone' => 'regex:^1[3-9]\d{9}$',
|
||||
'wechatId' => 'max:50',
|
||||
'remarks' => 'max:200',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'name.require' => '渠道名称不能为空',
|
||||
'name.length' => '渠道名称长度必须在1-50个字符之间',
|
||||
'phone.regex' => '手机号格式不正确,请输入11位数字且以1开头',
|
||||
'wechatId.max' => '微信号长度不能超过50个字符',
|
||||
'remarks.max' => '备注信息长度不能超过200个字符',
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'create' => ['name', 'phone', 'wechatId', 'remarks'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use app\common\service\AuthService;
|
||||
use app\common\service\WechatAccountHealthScoreService;
|
||||
use app\api\controller\WebSocketController;
|
||||
use Workerman\Lib\Timer;
|
||||
use app\cunkebao\service\DistributionRewardService;
|
||||
|
||||
class Adapter implements WeChatServiceInterface
|
||||
{
|
||||
@@ -375,9 +376,26 @@ class Adapter implements WeChatServiceInterface
|
||||
|
||||
if ($passedWeChatId && !empty($task_info['msgConf'])) {
|
||||
|
||||
// 更新状态为4(已通过并已发消息)
|
||||
Db::name('task_customer')
|
||||
->where('id', $task['id'])
|
||||
->update(['status' => 4,'passTime' => time(), 'updateTime' => time()]);
|
||||
|
||||
// 记录添加好友奖励(如果之前没有记录过,status从其他状态变为4时)
|
||||
// 注意:如果status已经是2,说明已经记录过奖励,这里不再重复记录
|
||||
if ($task['status'] != 2 && !empty($task['channelId'])) {
|
||||
try {
|
||||
DistributionRewardService::recordAddFriendReward(
|
||||
$task['task_id'],
|
||||
$task['id'],
|
||||
$task['phone'],
|
||||
intval($task['channelId'])
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// 记录错误但不影响主流程
|
||||
Log::error('记录添加好友奖励失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$wechatFriendRecord = $this->getWeChatAccoutIdAndFriendIdByWeChatIdAndFriendPhone($passedWeChatId, $task['phone']);
|
||||
$msgConf = is_string($task_info['msgConf']) ? json_decode($task_info['msgConf'], 1) : $task_info['msgConf'];
|
||||
@@ -395,9 +413,26 @@ class Adapter implements WeChatServiceInterface
|
||||
|
||||
// 已经执行成功的话,直接break,同时更新对应task_customer的状态为2(添加成功)
|
||||
if (isset($latestFriendTask['status']) && $latestFriendTask['status'] == 1) {
|
||||
// 更新状态
|
||||
Db::name('task_customer')
|
||||
->where('id', $task['id'])
|
||||
->update(['status' => 2, 'updateTime' => time()]);
|
||||
|
||||
// 记录添加好友奖励(异步处理,不影响主流程)
|
||||
if (!empty($task['channelId'])) {
|
||||
try {
|
||||
DistributionRewardService::recordAddFriendReward(
|
||||
$task['task_id'],
|
||||
$task['id'],
|
||||
$task['phone'],
|
||||
intval($task['channelId'])
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// 记录错误但不影响主流程
|
||||
Log::error('记录添加好友奖励失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
219
Server/sql.sql
219
Server/sql.sql
@@ -11,7 +11,7 @@
|
||||
Target Server Version : 50736
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 24/11/2025 16:50:43
|
||||
Date: 16/12/2025 16:39:24
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
@@ -90,7 +90,7 @@ CREATE TABLE `ck_ai_knowledge_base_type` (
|
||||
`delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间',
|
||||
`status` tinyint(2) NULL DEFAULT 1 COMMENT '状态 1启用 0禁用',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ai知识库类型' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ai知识库类型' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_ai_settings
|
||||
@@ -108,7 +108,7 @@ CREATE TABLE `ck_ai_settings` (
|
||||
`botId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '智能体id',
|
||||
`datasetId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库id',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI配置' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI配置' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_app_version
|
||||
@@ -145,7 +145,7 @@ CREATE TABLE `ck_attachments` (
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_hash_key`(`hash_key`) USING BTREE,
|
||||
INDEX `idx_server`(`server`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 505 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '附件表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 580 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '附件表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_call_recording
|
||||
@@ -164,6 +164,24 @@ CREATE TABLE `ck_call_recording` (
|
||||
UNIQUE INDEX `uk_id_phone_isCallOut_companyId`(`id`, `phone`, `isCallOut`, `companyId`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_chat_groups
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ck_chat_groups`;
|
||||
CREATE TABLE `ck_chat_groups` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称',
|
||||
`groupMemo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述',
|
||||
`groupType` tinyint(2) NULL DEFAULT NULL COMMENT '类型 1好友 2群',
|
||||
`userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID',
|
||||
`sort` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '排序',
|
||||
`createTime` int(11) NULL DEFAULT NULL,
|
||||
`isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除',
|
||||
`deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '聊天分组' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_company
|
||||
-- ----------------------------
|
||||
@@ -222,7 +240,7 @@ CREATE TABLE `ck_content_item` (
|
||||
INDEX `idx_wechatid`(`wechatId`) USING BTREE,
|
||||
INDEX `idx_friendid`(`friendId`) USING BTREE,
|
||||
INDEX `idx_create_time`(`createTime`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 5993 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容项目表-存储朋友圈采集数据' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 6090 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容项目表-存储朋友圈采集数据' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_content_library
|
||||
@@ -230,9 +248,10 @@ CREATE TABLE `ck_content_item` (
|
||||
DROP TABLE IF EXISTS `ck_content_library`;
|
||||
CREATE TABLE `ck_content_library` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`formType` tinyint(2) NULL DEFAULT 0 COMMENT '0存客宝 1触客宝',
|
||||
`sourceType` tinyint(2) NOT NULL DEFAULT 1 COMMENT '类型 1好友 2群 3自定义',
|
||||
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容库名称',
|
||||
`devices` json NULL COMMENT '设备列表,JSON格式:[{\"id\":1,\"name\":\"设备1\"},{\"id\":2,\"name\":\"设备2\"}]',
|
||||
`devices` json NULL COMMENT '设备列表',
|
||||
`catchType` json NULL COMMENT '采集类型',
|
||||
`sourceFriends` json NULL COMMENT '选择的微信好友',
|
||||
`sourceGroups` json NULL COMMENT '选择的微信群',
|
||||
@@ -252,7 +271,7 @@ CREATE TABLE `ck_content_library` (
|
||||
`isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除',
|
||||
`deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 100 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容库表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 135 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容库表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_coze_conversation
|
||||
@@ -331,7 +350,7 @@ CREATE TABLE `ck_customer_acquisition_task` (
|
||||
`deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间',
|
||||
`apiKey` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 168 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客计划表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 178 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客计划表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_device
|
||||
@@ -372,7 +391,7 @@ CREATE TABLE `ck_device_handle_log` (
|
||||
`companyId` int(11) NULL DEFAULT NULL COMMENT '租户id',
|
||||
`createTime` int(11) NULL DEFAULT NULL COMMENT '操作时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 339 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 351 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_device_taskconf
|
||||
@@ -395,7 +414,7 @@ CREATE TABLE `ck_device_taskconf` (
|
||||
`updateTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 30 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备任务配置表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备任务配置表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_device_user
|
||||
@@ -408,7 +427,7 @@ CREATE TABLE `ck_device_user` (
|
||||
`deviceId` int(11) UNSIGNED NOT NULL COMMENT '设备id',
|
||||
`deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备跟操盘手的关联关系' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备跟操盘手的关联关系' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_device_wechat_login
|
||||
@@ -425,7 +444,85 @@ CREATE TABLE `ck_device_wechat_login` (
|
||||
`isTips` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否提示迁移',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `wechatId`(`wechatId`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 312 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备登录微信记录表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 322 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备登录微信记录表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_distribution_channel
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ck_distribution_channel`;
|
||||
CREATE TABLE `ck_distribution_channel` (
|
||||
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '渠道ID',
|
||||
`companyId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '公司ID',
|
||||
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '渠道名称',
|
||||
`code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '渠道编码(系统生成)',
|
||||
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '联系电话',
|
||||
`password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '密码(MD5加密)',
|
||||
`wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '微信号',
|
||||
`remarks` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注信息',
|
||||
`createType` enum('manual','auto') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'manual' COMMENT '创建类型:manual手动创建,auto扫码创建',
|
||||
`status` enum('enabled','disabled') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'enabled' COMMENT '状态:enabled启用,disabled禁用',
|
||||
`totalCustomers` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '总获客数',
|
||||
`todayCustomers` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '今日获客数',
|
||||
`totalFriends` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '总加好友数',
|
||||
`todayFriends` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '今日加好友数',
|
||||
`withdrawableAmount` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '可提现金额(分)',
|
||||
`createTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updateTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleteTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '删除时间(软删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_companyId`(`companyId`) USING BTREE,
|
||||
INDEX `idx_code`(`code`) USING BTREE,
|
||||
INDEX `idx_status`(`status`) USING BTREE,
|
||||
INDEX `idx_deleteTime`(`deleteTime`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分销渠道表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_distribution_revenue_record
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ck_distribution_revenue_record`;
|
||||
CREATE TABLE `ck_distribution_revenue_record` (
|
||||
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '收益记录ID',
|
||||
`companyId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '公司ID',
|
||||
`channelId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '渠道ID',
|
||||
`channelCode` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '渠道编码(冗余字段,方便查询)',
|
||||
`type` enum('customer_acquisition','add_friend','order','poster','phone','other') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'other' COMMENT '收益类型:customer_acquisition获客,add_friend加好友,order订单,poster海报,phone电话,other其他',
|
||||
`sourceType` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '来源类型(如:海报获客、加好友任务等)',
|
||||
`sourceId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '来源ID(关联任务ID或其他业务ID)',
|
||||
`amount` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '收益金额(分,整型,单位分)',
|
||||
`remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注信息',
|
||||
`createTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updateTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_companyId`(`companyId`) USING BTREE,
|
||||
INDEX `idx_channelId`(`channelId`) USING BTREE,
|
||||
INDEX `idx_channelCode`(`channelCode`) USING BTREE,
|
||||
INDEX `idx_type`(`type`) USING BTREE,
|
||||
INDEX `idx_createTime`(`createTime`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分销渠道收益明细表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_distribution_withdrawal
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ck_distribution_withdrawal`;
|
||||
CREATE TABLE `ck_distribution_withdrawal` (
|
||||
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '提现申请ID',
|
||||
`companyId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '公司ID',
|
||||
`channelId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '渠道ID',
|
||||
`amount` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '提现金额(分,整型,单位分)',
|
||||
`payType` enum('wechat','alipay','bankcard') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'wechat' COMMENT '支付类型:wechat微信,alipay支付宝,bankcard银行卡',
|
||||
`status` enum('pending','approved','rejected','paid') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'pending' COMMENT '状态:pending待审核,approved已通过,rejected已拒绝,paid已打款',
|
||||
`reviewer` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '审核人',
|
||||
`reviewTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '审核时间',
|
||||
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注/拒绝理由',
|
||||
`applyTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '申请时间',
|
||||
`createTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updateTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_companyId`(`companyId`) USING BTREE,
|
||||
INDEX `idx_channelId`(`channelId`) USING BTREE,
|
||||
INDEX `idx_status`(`status`) USING BTREE,
|
||||
INDEX `idx_applyTime`(`applyTime`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分销渠道提现申请表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_flow_package
|
||||
@@ -628,7 +725,7 @@ CREATE TABLE `ck_kf_auto_greetings_record` (
|
||||
INDEX `idx_user`(`userId`) USING BTREE,
|
||||
INDEX `idx_createTime`(`createTime`) USING BTREE,
|
||||
INDEX `idx_isSend`(`isSend`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '问候规则使用记录表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '问候规则使用记录表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_kf_follow_up
|
||||
@@ -653,7 +750,7 @@ CREATE TABLE `ck_kf_follow_up` (
|
||||
INDEX `idx_level`(`type`) USING BTREE,
|
||||
INDEX `idx_isRemind`(`isRemind`) USING BTREE,
|
||||
INDEX `idx_isProcess`(`isProcess`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '跟进提醒' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '跟进提醒' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_kf_friend_settings
|
||||
@@ -675,7 +772,7 @@ CREATE TABLE `ck_kf_friend_settings` (
|
||||
INDEX `idx_userId`(`userId`) USING BTREE,
|
||||
INDEX `idx_wechatAccountId`(`wechatAccountId`) USING BTREE,
|
||||
INDEX `idx_friendId`(`friendId`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 42 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '好友AI配置' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '好友AI配置' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_kf_keywords
|
||||
@@ -769,7 +866,7 @@ CREATE TABLE `ck_kf_notice` (
|
||||
`createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间',
|
||||
`readTime` int(12) NULL DEFAULT NULL COMMENT '读取时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 247 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '通知消息' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 252 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '通知消息' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_kf_questions
|
||||
@@ -810,7 +907,7 @@ CREATE TABLE `ck_kf_reply` (
|
||||
`isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除',
|
||||
`delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 130751 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 130753 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_kf_reply_group
|
||||
@@ -874,7 +971,7 @@ CREATE TABLE `ck_kf_to_do` (
|
||||
INDEX `idx_level`(`level`) USING BTREE,
|
||||
INDEX `idx_isRemind`(`isRemind`) USING BTREE,
|
||||
INDEX `idx_isProcess`(`isProcess`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '待办事项' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '待办事项' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_menus
|
||||
@@ -919,7 +1016,7 @@ CREATE TABLE `ck_order` (
|
||||
`payInfo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '错误信息',
|
||||
`deleteTime` int(11) UNSIGNED NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 79 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 106 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_plan_scene
|
||||
@@ -937,7 +1034,7 @@ CREATE TABLE `ck_plan_scene` (
|
||||
`deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间',
|
||||
`scenarioTags` json NULL COMMENT '标签',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客场景' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客场景' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_plan_tags
|
||||
@@ -959,6 +1056,7 @@ DROP TABLE IF EXISTS `ck_task_customer`;
|
||||
CREATE TABLE `ck_task_customer` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`task_id` int(11) NOT NULL,
|
||||
`channelId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '渠道ID(分销渠道ID,对应distribution_channel.id)',
|
||||
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户姓名',
|
||||
`source` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '来源',
|
||||
`phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
|
||||
@@ -976,8 +1074,9 @@ CREATE TABLE `ck_task_customer` (
|
||||
INDEX `task_id`(`task_id`) USING BTREE,
|
||||
INDEX `addTime`(`addTime`) USING BTREE,
|
||||
INDEX `passTime`(`passTime`) USING BTREE,
|
||||
INDEX `updateTime`(`updateTime`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 28204 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
INDEX `updateTime`(`updateTime`) USING BTREE,
|
||||
INDEX `idx_channelId`(`channelId`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 28222 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_tokens_company
|
||||
@@ -985,12 +1084,29 @@ CREATE TABLE `ck_task_customer` (
|
||||
DROP TABLE IF EXISTS `ck_tokens_company`;
|
||||
CREATE TABLE `ck_tokens_company` (
|
||||
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`userId` int(10) NULL DEFAULT 0,
|
||||
`companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id',
|
||||
`tokens` bigint(100) NULL DEFAULT NULL,
|
||||
`createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间',
|
||||
`updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间',
|
||||
`isAdmin` tinyint(2) NULL DEFAULT 0 COMMENT '是否公司主号',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公司算力账户' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公司算力账户' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_tokens_form
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ck_tokens_form`;
|
||||
CREATE TABLE `ck_tokens_form` (
|
||||
`id` int(11) UNSIGNED NOT NULL,
|
||||
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称',
|
||||
`tokens` int(12) NULL DEFAULT 0 COMMENT '消耗token',
|
||||
`isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除',
|
||||
`status` tinyint(2) NULL DEFAULT 0 COMMENT '状态',
|
||||
`createTime` int(11) NULL DEFAULT 0,
|
||||
`delTime` int(11) NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_tokens_package
|
||||
@@ -1026,14 +1142,14 @@ CREATE TABLE `ck_tokens_record` (
|
||||
`userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID',
|
||||
`wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id',
|
||||
`friendIdOrGroupId` int(11) NULL DEFAULT NULL COMMENT '好友id或者群id',
|
||||
`form` tinyint(2) NULL DEFAULT 0 COMMENT '来源 0未知 1好友聊天 2群聊天 3群公告 4商家 5充值',
|
||||
`form` int(11) NULL DEFAULT 0 COMMENT '来源 \r\n0 未知\r\n1 点赞\r\n2 朋友圈同步\r\n3 朋友圈发布\r\n4 群发微信\r\n5 群发群消息\r\n6 群发群公告\r\n7 海报获客\r\n8 订单获客\r\n9 电话获客\r\n10 微信群获客\r\n11 API获客\r\n12 AI改写\r\n13 AI客服\r\n14 生成群公告\r\n\r\n1001 商家 \r\n1002 充值 \r\n1003 系统',
|
||||
`type` tinyint(2) NULL DEFAULT 0 COMMENT '类型 0减少 1增加',
|
||||
`tokens` int(11) NULL DEFAULT NULL COMMENT '消耗tokens',
|
||||
`balanceTokens` int(11) NULL DEFAULT NULL COMMENT '剩余tokens',
|
||||
`remarks` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
|
||||
`createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 273 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '算力明细记录' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 336 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '算力明细记录' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_traffic_order
|
||||
@@ -1072,7 +1188,7 @@ CREATE TABLE `ck_traffic_pool` (
|
||||
INDEX `idx_wechatId`(`wechatId`) USING BTREE,
|
||||
INDEX `idx_mobile`(`mobile`) USING BTREE,
|
||||
INDEX `idx_create_time`(`createTime`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1063510 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1201225 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_traffic_profile
|
||||
@@ -1117,7 +1233,7 @@ CREATE TABLE `ck_traffic_source` (
|
||||
INDEX `idx_identifier`(`identifier`) USING BTREE,
|
||||
INDEX `idx_companyId`(`companyId`) USING BTREE,
|
||||
INDEX `idx_company_status_time`(`companyId`, `status`, `updateTime`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 573831 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量来源' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 586456 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量来源' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_traffic_source_package
|
||||
@@ -1244,7 +1360,7 @@ CREATE TABLE `ck_user_portrait` (
|
||||
`createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间',
|
||||
`updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 19014 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户画像' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 22602 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户画像' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_users
|
||||
@@ -1269,7 +1385,7 @@ CREATE TABLE `ck_users` (
|
||||
`updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间',
|
||||
`deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1658 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1666 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_vendor_order
|
||||
@@ -1362,7 +1478,7 @@ CREATE TABLE `ck_wechat_account` (
|
||||
`updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `uni_wechatId`(`wechatId`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 3614968 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 4282931 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_wechat_customer
|
||||
@@ -1380,7 +1496,7 @@ CREATE TABLE `ck_wechat_customer` (
|
||||
`updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `uni_wechatId`(`wechatId`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 154 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信客服信息' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 159 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信客服信息' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_wechat_friendship
|
||||
@@ -1436,7 +1552,7 @@ CREATE TABLE `ck_wechat_group_member` (
|
||||
`deleteTime` int(11) UNSIGNED NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `uk_identifier_chatroomId_groupId`(`identifier`, `chatroomId`, `groupId`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 554147 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 561848 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_wechat_restricts
|
||||
@@ -1453,7 +1569,7 @@ CREATE TABLE `ck_wechat_restricts` (
|
||||
`restrictTime` int(11) NULL DEFAULT NULL COMMENT '限制日期',
|
||||
`recoveryTime` int(11) NULL DEFAULT NULL COMMENT '恢复日期',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1319 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信风险受限记录' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1416 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信风险受限记录' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_wechat_tag
|
||||
@@ -1491,7 +1607,7 @@ CREATE TABLE `ck_workbench` (
|
||||
INDEX `idx_user_id`(`userId`) USING BTREE,
|
||||
INDEX `idx_type`(`type`) USING BTREE,
|
||||
INDEX `idx_status`(`status`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 282 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台主表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 330 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台主表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_workbench_auto_like
|
||||
@@ -1546,6 +1662,7 @@ CREATE TABLE `ck_workbench_group_create` (
|
||||
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`workbenchId` int(11) NOT NULL COMMENT '计划ID',
|
||||
`devices` json NULL COMMENT '目标设备/客服(JSON数组)',
|
||||
`admins` json NULL COMMENT '管理员',
|
||||
`poolGroups` json NULL COMMENT '流量池JSON',
|
||||
`wechatGroups` json NULL COMMENT '微信客服JSON',
|
||||
`startTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开始时间',
|
||||
@@ -1558,7 +1675,7 @@ CREATE TABLE `ck_workbench_group_create` (
|
||||
`createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间',
|
||||
`updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 26 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 27 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_workbench_group_create_item
|
||||
@@ -1571,9 +1688,17 @@ CREATE TABLE `ck_workbench_group_create_item` (
|
||||
`wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '微信id',
|
||||
`groupId` int(10) NULL DEFAULT NULL COMMENT '群id',
|
||||
`wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id',
|
||||
`status` tinyint(2) NOT NULL DEFAULT 0 COMMENT '状态:0=待创建,1=创建中,2=创建成功,3=创建失败,4=管理员好友已拉入',
|
||||
`memberType` tinyint(2) NOT NULL DEFAULT 1 COMMENT '成员类型:1=群主成员,2=管理员,3=群主好友,4=管理员好友',
|
||||
`retryCount` int(11) NOT NULL DEFAULT 0 COMMENT '重试次数',
|
||||
`chatroomId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群聊ID(用于查询验证)',
|
||||
`verifyTime` int(11) NULL DEFAULT NULL COMMENT '验证时间',
|
||||
`createTime` int(11) NOT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 46 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_status_workbench`(`status`, `workbenchId`) USING BTREE,
|
||||
INDEX `idx_chatroom_id`(`chatroomId`) USING BTREE,
|
||||
INDEX `idx_member_type`(`memberType`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 66 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_workbench_group_push
|
||||
@@ -1686,7 +1811,7 @@ CREATE TABLE `ck_workbench_moments_sync` (
|
||||
`updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_workbench_id`(`workbenchId`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 97 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_workbench_moments_sync_item
|
||||
@@ -1703,7 +1828,7 @@ CREATE TABLE `ck_workbench_moments_sync_item` (
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_workbench_time`(`workbenchId`, `createTime`) USING BTREE,
|
||||
INDEX `idx_workbench_content`(`workbenchId`, `contentId`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1785 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2308 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for ck_workbench_traffic_config
|
||||
@@ -1747,7 +1872,7 @@ CREATE TABLE `ck_workbench_traffic_config_item` (
|
||||
INDEX `deviceId`(`deviceId`) USING BTREE,
|
||||
INDEX `wechatFriendId`(`wechatFriendId`) USING BTREE,
|
||||
INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 54241 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 58212 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for s2_allot_rule
|
||||
@@ -2156,7 +2281,7 @@ CREATE TABLE `s2_wechat_account_score` (
|
||||
INDEX `idx_health_score`(`healthScore`) USING BTREE,
|
||||
INDEX `idx_base_score_calculated`(`baseScoreCalculated`) USING BTREE,
|
||||
INDEX `idx_update_time`(`updateTime`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 363 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号评分记录表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 368 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号评分记录表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for s2_wechat_account_score_log
|
||||
@@ -2180,7 +2305,7 @@ CREATE TABLE `s2_wechat_account_score_log` (
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_account_field`(`accountId`, `field`) USING BTREE,
|
||||
INDEX `idx_wechat_id`(`wechatId`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号健康分加减分日志' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 39 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号健康分加减分日志' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for s2_wechat_chatroom
|
||||
@@ -2211,6 +2336,8 @@ CREATE TABLE `s2_wechat_chatroom` (
|
||||
`accountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称',
|
||||
`groupId` int(11) NULL DEFAULT 0 COMMENT '分组ID',
|
||||
`updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间',
|
||||
`isTop` tinyint(2) NULL DEFAULT 0 COMMENT '是否置顶',
|
||||
`groupIds` int(11) NULL DEFAULT 0 COMMENT '新分组ID',
|
||||
UNIQUE INDEX `uk_chatroom_account`(`chatroomId`, `wechatAccountId`) USING BTREE,
|
||||
INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE,
|
||||
INDEX `chatroomId`(`chatroomId`) USING BTREE,
|
||||
@@ -2237,7 +2364,7 @@ CREATE TABLE `s2_wechat_chatroom_member` (
|
||||
UNIQUE INDEX `uk_chatroom_wechat`(`chatroomId`, `wechatId`) USING BTREE,
|
||||
INDEX `chatroomId`(`chatroomId`) USING BTREE,
|
||||
INDEX `wechatId`(`wechatId`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 495174 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 496929 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for s2_wechat_friend
|
||||
@@ -2288,6 +2415,8 @@ CREATE TABLE `s2_wechat_friend` (
|
||||
`realName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',
|
||||
`company` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '公司',
|
||||
`position` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '职位',
|
||||
`isTop` tinyint(2) NULL DEFAULT 0 COMMENT '是否置顶',
|
||||
`groupIds` int(11) NULL DEFAULT 0 COMMENT '新分组ID',
|
||||
UNIQUE INDEX `uk_owner_wechat_account`(`ownerWechatId`, `wechatId`, `wechatAccountId`) USING BTREE,
|
||||
INDEX `idx_wechat_account_id`(`wechatAccountId`) USING BTREE,
|
||||
INDEX `idx_wechat_id`(`wechatId`) USING BTREE,
|
||||
@@ -2310,6 +2439,8 @@ CREATE TABLE `s2_wechat_group` (
|
||||
`departmentId` int(11) NULL DEFAULT NULL,
|
||||
`accountId` int(11) NULL DEFAULT NULL,
|
||||
`createTime` int(11) NULL DEFAULT NULL,
|
||||
`isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除',
|
||||
`deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
@@ -2387,6 +2518,6 @@ CREATE TABLE `s2_wechat_moments` (
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `idx_sns_account`(`snsId`, `wechatAccountId`) USING BTREE,
|
||||
INDEX `idx_account_friend`(`wechatAccountId`, `wechatFriendId`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 40130 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信朋友圈数据表' ROW_FORMAT = Dynamic;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 40159 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信朋友圈数据表' ROW_FORMAT = Dynamic;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
Reference in New Issue
Block a user