1、修复场景获客数据统计问题

2、修复默认头像破图问题
3、修复健康分展示问题
4、修复获客场景_好友迁移编辑会跳到其他类目问题
This commit is contained in:
wong
2025-12-24 15:27:57 +08:00
parent 79fd133821
commit 302617cd81
12 changed files with 197 additions and 46 deletions

View File

@@ -692,13 +692,13 @@ const WechatAccountDetail: React.FC = () => {
<div className={style["health-score-info"]}>
<div className={style["health-score-status"]}>
<span className={style["status-tag"]}>{overviewData?.healthScoreAssessment?.statusTag || "已添加加人"}</span>
<span className={style["status-time"]}>: {overviewData?.healthScoreAssessment?.lastAddTime || "18:44:14"}</span>
<span className={style["status-time"]}>: {overviewData?.healthScoreAssessment?.lastAddTime || "-"}</span>
</div>
<div className={style["health-score-display"]}>
<div className={style["score-circle-wrapper"]}>
<div className={style["score-circle"]}>
<div className={style["score-number"]}>
{overviewData?.healthScoreAssessment?.score || 67}
{overviewData?.healthScoreAssessment?.score || 0}
</div>
<div className={style["score-label"]}>SCORE</div>
</div>
@@ -810,7 +810,7 @@ const WechatAccountDetail: React.FC = () => {
<div className={style["score-circle-wrapper"]}>
<div className={style["score-circle"]}>
<div className={style["score-number"]}>
{overviewData?.healthScoreAssessment?.score || 67}
{overviewData?.healthScoreAssessment?.score || 0}
</div>
<div className={style["score-label"]}>SCORE</div>
</div>
@@ -869,7 +869,7 @@ const WechatAccountDetail: React.FC = () => {
<div className={style["health-item"]} key={`record-${index}`}>
<div className={style["health-item-label"]}>
<span className={style["health-item-icon-warning"]}></span>
{record.title || record.description || "记录"}
{record.name || record.description || "记录"}
{record.statusTag && (
<span className={style["health-item-tag"]}>
{record.statusTag}

View File

@@ -36,6 +36,13 @@ export function getUserList(planId: string, type: number) {
}
//获客列表
export function getFriendRequestTaskStats(taskId: string) {
return request(`/v1/dashboard/friendRequestTaskStats`, { taskId }, "GET");
export function getFriendRequestTaskStats(
taskId: string,
params?: { startTime?: string; endTime?: string },
) {
return request(
`/v1/dashboard/friendRequestTaskStats`,
{ taskId, ...params },
"GET",
);
}

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { Popup, SpinLoading } from "antd-mobile";
import { Button, message } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import React, { useEffect, useState, useCallback } from "react";
import { Popup, SpinLoading, DatePicker } from "antd-mobile";
import { Button, message, Input } from "antd";
import { CloseOutlined, CalendarOutlined } from "@ant-design/icons";
import style from "./Popups.module.scss";
import { getFriendRequestTaskStats } from "../api";
import LineChart2 from "@/components/LineChart2";
@@ -39,31 +39,73 @@ const PoolListModal: React.FC<PoolListModalProps> = ({
const [xData, setXData] = useState<any[]>([]);
const [yData, setYData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [startTime, setStartTime] = useState<Date | null>(null);
const [endTime, setEndTime] = useState<Date | null>(null);
const [showStartTimePicker, setShowStartTimePicker] = useState(false);
const [showEndTimePicker, setShowEndTimePicker] = useState(false);
// 当弹窗打开且有ruleId时获取数据
// 格式化日期为 YYYY-MM-DD
const formatDate = useCallback((date: Date | null): string => {
if (!date) return "";
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}, []);
// 初始化默认时间近7天
useEffect(() => {
if (visible && ruleId) {
setLoading(true);
getFriendRequestTaskStats(ruleId.toString())
.then(res => {
console.log(res);
setXData(res.dateArray);
setYData([
res.allNumArray,
res.errorNumArray,
res.passNumArray,
res.passRateArray,
res.successNumArray,
res.successRateArray,
]);
setStatistics(res.totalStats);
setLoading(false);
})
.finally(() => {
setLoading(false);
});
if (visible) {
// 如果时间未设置设置默认值为近7天
if (!startTime || !endTime) {
const today = new Date();
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(today.getDate() - 7);
setStartTime(sevenDaysAgo);
setEndTime(today);
}
} else {
// 弹窗关闭时重置时间,下次打开时重新初始化
setStartTime(null);
setEndTime(null);
}
}, [visible, ruleId]);
}, [visible]);
// 当弹窗打开或有ruleId或时间筛选变化时获取数据
useEffect(() => {
if (!visible || !ruleId) return;
setLoading(true);
const params: { startTime?: string; endTime?: string } = {};
if (startTime) {
params.startTime = formatDate(startTime);
}
if (endTime) {
params.endTime = formatDate(endTime);
}
getFriendRequestTaskStats(ruleId.toString(), params)
.then(res => {
console.log(res);
setXData(res.dateArray);
setYData([
res.allNumArray,
res.errorNumArray,
res.passNumArray,
res.passRateArray,
res.successNumArray,
res.successRateArray,
]);
setStatistics(res.totalStats);
})
.catch(error => {
console.error("获取统计数据失败:", error);
message.error("获取统计数据失败");
})
.finally(() => {
setLoading(false);
});
}, [visible, ruleId, startTime, endTime, formatDate]);
const title = ruleName ? `${ruleName} - 累计统计数据` : "累计统计数据";
return (
@@ -89,6 +131,55 @@ const PoolListModal: React.FC<PoolListModalProps> = ({
/>
</div>
{/* 时间筛选 */}
<div className={style.dateFilter}>
<div className={style.dateFilterItem}>
<label className={style.dateFilterLabel}></label>
<Input
readOnly
placeholder="请选择开始时间"
value={startTime ? formatDate(startTime) : ""}
onClick={() => setShowStartTimePicker(true)}
prefix={<CalendarOutlined />}
className={style.dateFilterInput}
/>
<DatePicker
visible={showStartTimePicker}
title="开始时间"
value={startTime}
max={endTime || new Date()}
onClose={() => setShowStartTimePicker(false)}
onConfirm={val => {
setStartTime(val);
setShowStartTimePicker(false);
}}
/>
</div>
<div className={style.dateFilterItem}>
<label className={style.dateFilterLabel}></label>
<Input
readOnly
placeholder="请选择结束时间"
value={endTime ? formatDate(endTime) : ""}
onClick={() => setShowEndTimePicker(true)}
prefix={<CalendarOutlined />}
className={style.dateFilterInput}
/>
<DatePicker
visible={showEndTimePicker}
title="结束时间"
value={endTime}
min={startTime || undefined}
max={new Date()}
onClose={() => setShowEndTimePicker(false)}
onConfirm={val => {
setEndTime(val);
setShowEndTimePicker(false);
}}
/>
</div>
</div>
{/* 统计数据表格 */}
<div className={style.statisticsContent}>
{loading ? (

View File

@@ -663,6 +663,32 @@
color: #666;
}
// 日期筛选样式
.dateFilter {
display: flex;
gap: 12px;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
}
.dateFilterItem {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.dateFilterLabel {
font-size: 14px;
color: #666;
font-weight: 500;
}
.dateFilterInput {
width: 100%;
}
// 统计数据弹窗样式
.statisticsContent {
flex: 1;

View File

@@ -90,7 +90,7 @@ export default function NewPlan() {
setFormData(prev => ({
...prev,
name: detail.name ?? "",
scenario: Number(detail.scenario) || 1,
scenario: Number(detail.sceneId || detail.scenario) || 1,
scenarioTags: detail.scenarioTags ?? [],
customTags: detail.customTags ?? [],
customTagsOptions: detail.customTags ?? [],
@@ -102,7 +102,7 @@ export default function NewPlan() {
startTime: detail.startTime ?? "09:00",
endTime: detail.endTime ?? "18:00",
enabled: detail.enabled ?? true,
sceneId: Number(detail.scenario) || 1,
sceneId: Number(detail.sceneId || detail.scenario) || 1,
remarkFormat: detail.remarkFormat ?? "",
addFriendInterval: detail.addFriendInterval ?? 1,
tips: detail.tips ?? "",

View File

@@ -40,6 +40,7 @@ const generatePosterMaterials = (): Material[] => {
};
const BasicSettings: React.FC<BasicSettingsProps> = ({
isEdit,
formData,
onChange,
sceneList,
@@ -249,16 +250,27 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
) : (
<div className={styles["basic-scene-grid"]}>
{sceneList
.filter(scene => scene.id !== 10)
.filter(scene => {
// 编辑模式下,如果当前选中的场景 id 是 10则显示它
if (isEdit && formData.scenario === 10 && scene.id === 10) {
return true;
}
// 其他情况过滤掉 id 为 10 的场景
return scene.id !== 10;
})
.map(scene => {
const selected = formData.scenario === scene.id;
// 编辑模式下,如果当前场景 id 是 10则禁用所有场景选择
const isDisabled = isEdit && formData.scenario === 10;
return (
<button
key={scene.id}
onClick={() => handleScenarioSelect(scene.id)}
onClick={() => !isDisabled && handleScenarioSelect(scene.id)}
disabled={isDisabled}
className={
styles["basic-scene-btn"] +
(selected ? " " + styles.selected : "")
(selected ? " " + styles.selected : "") +
(isDisabled ? " " + styles.disabled : "")
}
>
{scene.name.replace("获客", "")}

View File

@@ -29,6 +29,17 @@
color: #fff;
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.08);
}
.basic-scene-btn.disabled {
opacity: 0.6;
cursor: not-allowed;
background: rgba(#1677ff, 0.1);
color: #1677ff;
}
.basic-scene-btn.disabled.selected {
background: #1677ff;
color: #fff;
opacity: 0.8;
}
.basic-label {
margin-bottom: 12px;
font-weight: 500;

View File

@@ -308,7 +308,7 @@ class StatsController extends Controller
->field("FROM_UNIXTIME(addTime, '%m-%d') AS d, COUNT(*) AS c")
->where(['task_id' => $taskId])
->where('addTime', 'between', [$startTimestamp, $endTimestamp])
->whereIn('status', [1, 2, 4])
->whereIn('status', [1, 2, 4, 5])
->group('d')
->select();

View File

@@ -84,7 +84,7 @@ class StoreAccountController extends BaseController
'phone' => $phone,
'passwordMd5' => md5($password),
'passwordLocal' => localEncrypt($password),
'avatar' => 'https://img.icons8.com/color/512/circled-user-male-skin-type-7.png',
'avatar' => '',
'isAdmin' => 0,
'companyId' => $companyId,
'typeId' => 2, // 门店端固定为2

View File

@@ -405,8 +405,8 @@ class PlanSceneV1Controller extends BaseController
->field([
'task_id as taskId',
'COUNT(1) as acquiredCount',
"SUM(CASE WHEN status IN (1,2,3,4) THEN 1 ELSE 0 END) as addedCount",
"SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) as passCount",
"SUM(CASE WHEN status IN (1,2,3,4,5) THEN 1 ELSE 0 END) as addedCount",
"SUM(CASE WHEN status IN (4,5) THEN 1 ELSE 0 END) as passCount",
'MAX(updateTime) as lastUpdated'
])
->group('task_id')
@@ -511,7 +511,7 @@ class PlanSceneV1Controller extends BaseController
$query = Db::name('task_customer')->where(['task_id' => $task['id']]);
if ($type == 2){
$query = $query->where('status',4);
$query = $query->whereIn('status',[4,5]);
}
if (!empty($keyword)) {

View File

@@ -179,7 +179,7 @@ class GetWechatController extends BaseController
'activityLevel' => $this->getActivityLevel($wechatId),
'accountWeight' => $this->getAccountWeight($wechatId),
'statistics' => $this->getStatistics($wechatId),
'restrictions' => $this->getRestrict($wechatId),
// 'restrictions' => $this->getRestrict($wechatId),
];

View File

@@ -216,7 +216,7 @@ class Adapter implements WeChatServiceInterface
// 根据健康分判断24h内加的好友数量限制
$healthScoreService = new WechatAccountHealthScoreService();
$healthScoreInfo = $healthScoreService->getHealthScore($accountId);
// 如果健康分记录不存在,先计算一次
if (empty($healthScoreInfo)) {
try {
@@ -228,10 +228,10 @@ class Adapter implements WeChatServiceInterface
$maxAddFriendPerDay = 5;
}
}
// 获取每日最大加人次数(基于健康分)
$maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 5;
// 如果健康分为0或很低不允许添加好友
if ($maxAddFriendPerDay <= 0) {
Log::info("账号健康分过低,不允许添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")");
@@ -367,6 +367,10 @@ class Adapter implements WeChatServiceInterface
if (!empty($wechatId)) {
$isFriend = $this->checkIfIsWeChatFriendByPhone($wechatId, $task['phone']);
if ($isFriend) {
// 更新状态为5已通过未发消息
Db::name('task_customer')
->where('id', $task['id'])
->update(['status' => 5,'passTime' => time(), 'updateTime' => time()]);
$passedWeChatId = $wechatId;
break;
}