分销功能提交

This commit is contained in:
wong
2025-12-17 16:20:46 +08:00
parent 8e4ce2aee2
commit 7dda34a779
34 changed files with 8959 additions and 105 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'];
// 渠道IDcid对应 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' => '新增成功',

View File

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

View File

@@ -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([
// 渠道IDcid对应 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', ''));
// 渠道IDcid对应 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'])

View File

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

View 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';
}

View 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';
}

View File

@@ -0,0 +1,276 @@
<?php
namespace app\cunkebao\service;
use think\Db;
use think\Exception;
/**
* 分销奖励服务类
* 处理获客和添加好友时的分销收益记录
*/
class DistributionRewardService
{
/**
* 记录获客奖励
*
* @param int $taskId 获客计划ID
* @param int $customerId 客户IDtask_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'] ?? '未知场景';
// 如果指定了 channelIdcid仅允许该渠道获得分佣
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 客户IDtask_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'] ?? '未知场景';
// 如果指定了 channelIdcid仅允许该渠道获得分佣
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;
}
}
}

View 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'],
];
}

View File

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

View File

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