社区feed流/topsales

This commit is contained in:
spllzh
2025-09-25 20:55:31 +08:00
parent 8286e68dd2
commit 9554631571
17 changed files with 1352 additions and 1 deletions

View File

@@ -0,0 +1,115 @@
package com.rj.controller.biz;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.rj.dto.CommunityFeedDTO;
import com.rj.dto.DifyWorkflowResponseDto;
import com.rj.entity.bz.CommunityFeedAnalysisResult;
import com.rj.service.biz.ICommunityFeedAnalysisResultService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/community-feed")
@Tag(name = "社区Feed分析", description = "社区Feed分析结果API")
public class CommunityFeedAnalysisResultController {
@Autowired
private ICommunityFeedAnalysisResultService resultService;
/**
* AI-Feed 流社区Feed流优质内容
*/
@PostMapping("/callAIFeedWorkflow")
@Operation(summary = "AI-Feed 工作流", description = "调用Dify AI-Feed 工作流(社区优质内容评估)")
public ResponseEntity<DifyWorkflowResponseDto> aiFeedWorkflow(
@Valid @RequestBody CommunityFeedDTO requestDto) {
try {
log.info("开始调用AI-Feed工作流参数 {}", requestDto);
return ResponseEntity.ok(resultService.callAiFeedWorkflow(requestDto));
} catch (Exception e) {
log.error("调用AI-Feed工作流失败", e);
DifyWorkflowResponseDto errorResult = DifyWorkflowResponseDto.failure(
"AI-Feed 工作流调用失败: " + e.getMessage(),
e.getClass().getSimpleName()
);
return ResponseEntity.status(500).body(errorResult);
}
}
@PostMapping("/save")
@Operation(summary = "保存分析结果")
public boolean save(@RequestBody CommunityFeedAnalysisResult result) {
return resultService.saveResult(result);
}
@GetMapping("/by-workflow/{workflowId}")
@Operation(summary = "按workflowId查询")
public CommunityFeedAnalysisResult getByWorkflowId(@PathVariable String workflowId) {
return resultService.getByWorkflowId(workflowId);
}
@GetMapping("/page")
@Operation(summary = "分页查询Feed分析结果")
public ResponseEntity<Map<String, Object>> page(
@Parameter(description = "页码", example = "1")
@RequestParam(defaultValue = "1") Integer current,
@Parameter(description = "每页大小", example = "10")
@RequestParam(defaultValue = "10") Integer size,
@Parameter(description = "工作流ID模糊查询")
@RequestParam(required = false) String workflowId,
@Parameter(description = "状态(精确匹配)")
@RequestParam(required = false) String status
) {
Map<String, Object> result = new HashMap<>();
try {
Page<CommunityFeedAnalysisResult> page = new Page<>(current, size);
LambdaQueryWrapper<CommunityFeedAnalysisResult> wrapper = new LambdaQueryWrapper<>();
if (workflowId != null && !workflowId.trim().isEmpty()) {
wrapper.like(CommunityFeedAnalysisResult::getWorkflowId, workflowId);
}
if (status != null && !status.trim().isEmpty()) {
wrapper.eq(CommunityFeedAnalysisResult::getStatus, status);
}
wrapper.orderByDesc(CommunityFeedAnalysisResult::getCreatedAt);
Page<CommunityFeedAnalysisResult> dataPage = resultService.page(page, wrapper);
result.put("success", true);
result.put("message", "查询成功");
result.put("data", dataPage.getRecords());
result.put("total", dataPage.getTotal());
result.put("current", dataPage.getCurrent());
result.put("size", dataPage.getSize());
result.put("pages", dataPage.getPages());
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("分页查询Feed分析结果失败", e);
result.put("success", false);
result.put("message", "查询失败: " + e.getMessage());
return ResponseEntity.status(500).body(result);
}
}
}

View File

@@ -0,0 +1,70 @@
package com.rj.controller.biz;
import com.rj.dto.TopSalesScoreRequestDto;
import com.rj.dto.DifyWorkflowResponseDto;
import com.rj.service.biz.ITopSalesScoreResultService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.rj.entity.bz.TopSalesScoreResult;
@Slf4j
@RestController
@RequestMapping("/api/topsales")
@Tag(name = "TopSales评分工作流", description = "调用【AI】topsales 所有维度-只打分")
public class TopSalesController {
@Autowired
private ITopSalesScoreResultService topSalesScoreResultService;
@PostMapping("/score")
@Operation(summary = "调用TopSales评分工作流")
public ResponseEntity<DifyWorkflowResponseDto> score(@Valid @RequestBody TopSalesScoreRequestDto requestDto) {
try {
return ResponseEntity.ok(topSalesScoreResultService.callAndSave(requestDto));
} catch (Exception e) {
log.error("调用TopSales评分工作流失败", e);
DifyWorkflowResponseDto error = DifyWorkflowResponseDto.failure(
"TopSales评分工作流调用失败: " + e.getMessage(),
e.getClass().getSimpleName()
);
return ResponseEntity.status(500).body(error);
}
}
@GetMapping("/page")
@Operation(summary = "分页查询TopSales评分结果")
public ResponseEntity<Object> page(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String workflowId,
@RequestParam(required = false) String status
) {
Page<TopSalesScoreResult> page = new Page<>(current, size);
LambdaQueryWrapper<TopSalesScoreResult> wrapper = new LambdaQueryWrapper<>();
if (workflowId != null && !workflowId.trim().isEmpty()) {
wrapper.like(TopSalesScoreResult::getWorkflowId, workflowId);
}
if (status != null && !status.trim().isEmpty()) {
wrapper.eq(TopSalesScoreResult::getStatus, status);
}
wrapper.orderByDesc(TopSalesScoreResult::getCreatedAt);
Page<TopSalesScoreResult> result = topSalesScoreResultService.page(page, wrapper);
return ResponseEntity.ok(result);
}
@GetMapping("/by-workflow/{workflowId}")
@Operation(summary = "按workflowId查询TopSales评分结果")
public ResponseEntity<TopSalesScoreResult> getByWorkflowId(@PathVariable String workflowId) {
return ResponseEntity.ok(topSalesScoreResultService.getByWorkflowId(workflowId));
}
}

View File

@@ -0,0 +1,18 @@
package com.rj.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class TopSalesScoreRequestDto {
@NotBlank(message = "chat不能为空")
private String chat;
// 可选:原始内容与来源信息
private String srcContent; // 待分析内容(不传则使用 chat
private String srcId; // 原内容ID
private String srcDesc; // 来源说明
}

View File

@@ -0,0 +1,77 @@
package com.rj.entity.bz;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 社区 Feed 流分析结果实体
*/
@Data
@TableName("community_feed_analysis_result")
public class CommunityFeedAnalysisResult {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
@TableField("workflow_id")
private String workflowId;
@TableField("length_score")
private Integer lengthScore;
@TableField("image_score")
private Integer imageScore;
@TableField("topics_score")
private Integer topicsScore;
@TableField("emotion_score")
private Integer emotionScore;
@TableField("theme_score")
private Integer themeScore;
@TableField("fluency_score")
private Integer fluencyScore;
@TableField("contentScore")
private Integer contentScore;
@TableField("evaluationReason")
private String evaluationReason;
@TableField("total_tokens")
private Integer totalTokens;
@TableField("elapsed_time")
private BigDecimal elapsedTime;
@TableField("status")
private String status;
@TableField("error")
private String error;
@TableField("src_content")
private String srcContent;
@TableField("src_id")
private String srcId;
@TableField("src_type")
private String srcType;
@TableField("created_at")
private LocalDateTime createdAt;
@TableField("updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,95 @@
package com.rj.entity.bz;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("topsales_score_result")
public class TopSalesScoreResult {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
@TableField("workflow_id")
private String workflowId;
@TableField("naturalness_score")
private Integer naturalnessScore;
@TableField("coherence_score")
private Integer coherenceScore;
@TableField("fluency_score")
private Integer fluencyScore;
@TableField("advisor_aggressiveness_score")
private Integer advisorAggressivenessScore;
@TableField("advisor_consistency_score")
private Integer advisorConsistencyScore;
@TableField("greeting_reception_score")
private Integer greetingReceptionScore;
@TableField("brand_introduction_score")
private Integer brandIntroductionScore;
@TableField("customer_needs_understanding_score")
private Integer customerNeedsUnderstandingScore;
@TableField("budget_understanding_score")
private Integer budgetUnderstandingScore;
@TableField("recommendation_score")
private Integer recommendationScore;
@TableField("safety_introduction_score")
private Integer safetyIntroductionScore;
@TableField("health_environment_introduction_score")
private Integer healthEnvironmentIntroductionScore;
@TableField("technology_introduction_score")
private Integer technologyIntroductionScore;
@TableField("test_drive_invitation_score")
private Integer testDriveInvitationScore;
@TableField("negotiation_deal_score")
private Integer negotiationDealScore;
@TableField("total_tokens")
private Integer totalTokens;
@TableField("elapsed_time")
private BigDecimal elapsedTime;
@TableField("status")
private String status;
@TableField("error")
private String error;
@TableField("src_content")
private String srcContent;
@TableField("src_id")
private String srcId;
@TableField("src_desc")
private String srcDesc;
@TableField("created_at")
private LocalDateTime createdAt;
@TableField("updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,12 @@
package com.rj.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.rj.entity.bz.CommunityFeedAnalysisResult;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CommunityFeedAnalysisResultMapper extends BaseMapper<CommunityFeedAnalysisResult> {
}

View File

@@ -0,0 +1,11 @@
package com.rj.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.rj.entity.bz.TopSalesScoreResult;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TopSalesScoreResultMapper extends BaseMapper<TopSalesScoreResult> {
}

View File

@@ -47,6 +47,11 @@ public class DifyWorkflowService {
@Value("${dify.api.ai-feed-token}")
private String aiFeedToken; // AI-Feed流社区优质内容评估
@Value("${dify.api.top-sales-alldim-token}")
private String topSalesAllDimToken; // 【AI】topsales 所有维度-只打分
private final RestTemplate restTemplate;
@Autowired
@@ -259,6 +264,61 @@ public class DifyWorkflowService {
}
}
/**
* 调用【AI】TopSales 所有维度-只打分 工作流
* 输入chat
* 输出data.outputs.text可能是纯文本或JSON字符串
*/
public DifyWorkflowResponse callTopSalesScoringWorkflow(DifyWorkflowRequest request) {
try {
String url = difyBaseUrl + workflowEndpoint;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + topSalesAllDimToken);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("inputs", request.getInputs());
requestBody.put("response_mode", "blocking");
requestBody.put("user", request.getUserId());
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
log.info("调用TopSales评分工作流API: {}", url);
log.info("TopSales评分请求参数: {}", JSON.toJSONString(requestBody));
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.POST,
entity,
String.class
);
log.info("TopSales评分工作流响应状态: {}", response.getStatusCode());
log.info("TopSales评分工作流响应内容: {}", response.getBody());
if (response.getStatusCode() == HttpStatus.OK) {
JSONObject responseJson = JSON.parseObject(response.getBody());
DifyWorkflowResponse result = new DifyWorkflowResponse();
if (responseJson.containsKey("data")) {
JSONObject data = responseJson.getJSONObject("data");
result.setWorkflowRunId(data.getString("workflow_run_id"));
result.setTaskId(data.getString("task_id"));
result.setData(data);
}
if (responseJson.containsKey("metadata")) {
result.setMetadata(responseJson.getJSONObject("metadata"));
}
return result;
} else {
throw new RuntimeException("TopSales评分工作流调用失败状态码: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("调用TopSales评分工作流异常", e);
throw new RuntimeException("调用TopSales评分工作流异常: " + e.getMessage(), e);
}
}
/**
* 解析工作流响应并保存数据
*/

View File

@@ -0,0 +1,14 @@
package com.rj.service.biz;
import com.baomidou.mybatisplus.extension.service.IService;
import com.rj.entity.bz.CommunityFeedAnalysisResult;
import com.rj.dto.CommunityFeedDTO;
import com.rj.dto.DifyWorkflowResponseDto;
public interface ICommunityFeedAnalysisResultService extends IService<CommunityFeedAnalysisResult> {
boolean saveResult(CommunityFeedAnalysisResult result);
CommunityFeedAnalysisResult getByWorkflowId(String workflowId);
DifyWorkflowResponseDto callAiFeedWorkflow(CommunityFeedDTO requestDto);
}

View File

@@ -0,0 +1,13 @@
package com.rj.service.biz;
import com.baomidou.mybatisplus.extension.service.IService;
import com.rj.dto.TopSalesScoreRequestDto;
import com.rj.dto.DifyWorkflowResponseDto;
import com.rj.entity.bz.TopSalesScoreResult;
public interface ITopSalesScoreResultService extends IService<TopSalesScoreResult> {
DifyWorkflowResponseDto callAndSave(TopSalesScoreRequestDto requestDto);
TopSalesScoreResult getByWorkflowId(String workflowId);
}

View File

@@ -0,0 +1,143 @@
package com.rj.service.biz.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rj.dto.CommunityFeedDTO;
import com.rj.dto.DifyWorkflowResponseDto;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rj.entity.bz.CommunityFeedAnalysisResult;
import com.rj.service.DifyWorkflowService;
import com.rj.mapper.CommunityFeedAnalysisResultMapper;
import com.rj.service.biz.ICommunityFeedAnalysisResultService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashMap;
import java.util.Map;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Slf4j
@Service
public class CommunityFeedAnalysisResultServiceImpl
extends ServiceImpl<CommunityFeedAnalysisResultMapper, CommunityFeedAnalysisResult>
implements ICommunityFeedAnalysisResultService {
@Autowired
private DifyWorkflowService difyWorkflowService;
@Override
public boolean saveResult(CommunityFeedAnalysisResult result) {
try {
log.info("保存社区Feed分析结果{}", result);
return this.save(result);
} catch (Exception e) {
log.error("保存社区Feed分析结果异常", e);
return false;
}
}
@Override
public CommunityFeedAnalysisResult getByWorkflowId(String workflowId) {
return this.getOne(new LambdaQueryWrapper<CommunityFeedAnalysisResult>()
.eq(CommunityFeedAnalysisResult::getWorkflowId, workflowId)
.last("limit 1"));
}
@Override
public DifyWorkflowResponseDto callAiFeedWorkflow(CommunityFeedDTO requestDto) {
Map<String, Object> inputs = new HashMap<>();
inputs.put("targetContent", requestDto.getTargetContent());
inputs.put("targetId", requestDto.getTargetId());
inputs.put("targetType", requestDto.getTargetType());
DifyWorkflowService.DifyWorkflowRequest request =
new DifyWorkflowService.DifyWorkflowRequest(inputs, requestDto.getTargetId());
DifyWorkflowService.DifyWorkflowResponse response =
difyWorkflowService.callAiFeedWorkflow(request);
try {
JSONObject data = response.getData();
if (data != null) {
CommunityFeedAnalysisResult entity = new CommunityFeedAnalysisResult();
// 顶层通用信息
entity.setWorkflowId(data.getString("workflow_id"));
entity.setStatus(data.getString("status"));
entity.setTotalTokens(data.getInteger("total_tokens"));
if (data.containsKey("elapsed_time")) {
try {
entity.setElapsedTime(data.getBigDecimal("elapsed_time"));
} catch (Exception ex) {
try {
entity.setElapsedTime(new BigDecimal(String.valueOf(data.get("elapsed_time"))));
} catch (Exception ig) {
log.warn("elapsed_time 解析失败: {}", data.get("elapsed_time"));
}
}
}
entity.setError(data.getString("error"));
// 解析 outputs.feedAiResult可能是 ```json 包裹的字符串)
JSONObject outputs = data.getJSONObject("outputs");
if (outputs != null && outputs.containsKey("feedAiResult")) {
String feedAiResult = outputs.getString("feedAiResult");
if (feedAiResult != null) {
// 清理 ```json 代码块
String cleaned = feedAiResult.trim();
if (cleaned.startsWith("```json")) {
cleaned = cleaned.substring(7).trim();
} else if (cleaned.startsWith("```")) {
cleaned = cleaned.substring(3).trim();
}
if (cleaned.endsWith("```")) {
cleaned = cleaned.substring(0, cleaned.length() - 3).trim();
}
try {
JSONObject scoreJson = JSON.parseObject(cleaned);
entity.setLengthScore(scoreJson.getInteger("length"));
entity.setImageScore(scoreJson.getInteger("image"));
entity.setTopicsScore(scoreJson.getInteger("topics"));
entity.setEmotionScore(scoreJson.getInteger("emotion"));
entity.setThemeScore(scoreJson.getInteger("theme"));
entity.setFluencyScore(scoreJson.getInteger("fluency"));
entity.setContentScore(scoreJson.getInteger("contentScore"));
entity.setEvaluationReason(scoreJson.getString("evaluationReason"));
} catch (Exception ignore) {
log.warn("解析feedAiResult为JSON失败cleaned: {}", cleaned);
}
}
}
// 源内容:从请求与 outputs 回填
entity.setSrcContent(requestDto.getTargetContent());
if (outputs != null) {
entity.setSrcId(outputs.getString("targetId"));
entity.setSrcType(outputs.getString("targetType"));
}
// 时间字段
LocalDateTime now = LocalDateTime.now();
entity.setCreatedAt(now);
entity.setUpdatedAt(now);
boolean saved = this.save(entity);
log.info("AI-Feed分析结果保存{}: {}", saved ? "成功" : "失败", entity);
}
} catch (Exception e) {
log.error("保存AI-Feed分析结果到DB失败", e);
}
return DifyWorkflowResponseDto.success(
response.getWorkflowRunId(),
response.getTaskId(),
response.getData(),
response.getMetadata()
);
}
}

View File

@@ -0,0 +1,140 @@
package com.rj.service.biz.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rj.dto.DifyWorkflowResponseDto;
import com.rj.dto.TopSalesScoreRequestDto;
import com.rj.entity.bz.TopSalesScoreResult;
import com.rj.mapper.TopSalesScoreResultMapper;
import com.rj.service.DifyWorkflowService;
import com.rj.service.biz.ITopSalesScoreResultService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Slf4j
@Service
public class TopSalesScoreResultServiceImpl
extends ServiceImpl<TopSalesScoreResultMapper, TopSalesScoreResult>
implements ITopSalesScoreResultService {
@Autowired
private DifyWorkflowService difyWorkflowService;
@Override
public DifyWorkflowResponseDto callAndSave(TopSalesScoreRequestDto requestDto) {
DifyWorkflowService.DifyWorkflowResponse response = null;
try {
log.info("[TopSales] 开始调用工作流chat长度={}", requestDto.getChat() == null ? 0 : requestDto.getChat().length());
JSONObject inputs = new JSONObject();
inputs.put("chat", requestDto.getChat());
DifyWorkflowService.DifyWorkflowRequest req = new DifyWorkflowService.DifyWorkflowRequest(inputs, "topsales_user");
response = difyWorkflowService.callTopSalesScoringWorkflow(req);
JSONObject data = response.getData();
if (data != null) {
log.info("[TopSales] 响应data: {}", data);
TopSalesScoreResult entity = new TopSalesScoreResult();
entity.setWorkflowId(data.getString("workflow_id"));
entity.setStatus(data.getString("status"));
entity.setTotalTokens(data.getInteger("total_tokens"));
try {
entity.setElapsedTime(data.getBigDecimal("elapsed_time"));
} catch (Exception ex) {
Object et = data.get("elapsed_time");
if (et != null) {
entity.setElapsedTime(new BigDecimal(String.valueOf(et)));
}
}
// 输出在 data.outputs.text
JSONObject outputs = data.getJSONObject("outputs");
if (outputs != null) {
String text = outputs.getString("text");
if (text != null) {
String cleaned = text.trim();
if (cleaned.startsWith("```json")) cleaned = cleaned.substring(7).trim();
if (cleaned.startsWith("```")) cleaned = cleaned.substring(3).trim();
if (cleaned.endsWith("```")) cleaned = cleaned.substring(0, cleaned.length() - 3).trim();
log.info("[TopSales] 清理后的text: {}", cleaned);
try {
JSONObject score = JSON.parseObject(cleaned);
// 同时兼容中文与英文snake_case键
entity.setNaturalnessScore(firstInt(score, "自然度分数", "naturalness_score"));
entity.setCoherenceScore(firstInt(score, "连贯性分数", "coherence_score"));
entity.setFluencyScore(firstInt(score, "流畅度分数", "fluency_score"));
entity.setAdvisorAggressivenessScore(firstInt(score, "顾问攻击性分数", "advisor_aggressiveness_score"));
entity.setAdvisorConsistencyScore(firstInt(score, "顾问一致性分数", "advisor_consistency_score"));
entity.setGreetingReceptionScore(firstInt(score, "欢迎接待分数", "greeting_reception_score"));
entity.setBrandIntroductionScore(firstInt(score, "品牌介绍分数", "brand_introduction_score"));
entity.setCustomerNeedsUnderstandingScore(firstInt(score, "了解客户用车需求分数", "customer_needs_understanding_score"));
entity.setBudgetUnderstandingScore(firstInt(score, "了解客户经济预算分数", "budget_understanding_score"));
entity.setRecommendationScore(firstInt(score, "推荐配置或车型分数", "recommendation_score"));
entity.setSafetyIntroductionScore(firstInt(score, "重点介绍安全性分数", "safety_introduction_score"));
entity.setHealthEnvironmentIntroductionScore(firstInt(score, "重点介绍健康环保分数", "health_environment_introduction_score"));
entity.setTechnologyIntroductionScore(firstInt(score, "重点介绍科技性分数", "technology_introduction_score"));
entity.setTestDriveInvitationScore(firstInt(score, "邀请试驾分数", "test_drive_invitation_score"));
entity.setNegotiationDealScore(firstInt(score, "商谈成交分数", "negotiation_deal_score"));
} catch (Exception ex) {
log.warn("TopSales 输出解析失败text= {}", text);
}
}
// 回填源内容字段(允许外部覆盖)
String srcContent = requestDto.getSrcContent() == null || requestDto.getSrcContent().trim().isEmpty()
? requestDto.getChat()
: requestDto.getSrcContent();
entity.setSrcContent(srcContent);
entity.setSrcId(requestDto.getSrcId() != null ? requestDto.getSrcId() : data.getString("id"));
entity.setSrcDesc(requestDto.getSrcDesc() != null ? requestDto.getSrcDesc() : "topsales_all_dim_score");
}
LocalDateTime now = LocalDateTime.now();
entity.setCreatedAt(now);
entity.setUpdatedAt(now);
boolean saved = this.save(entity);
log.info("TopSales评分结果保存{}: {}", saved ? "成功" : "失败", entity);
}
} catch (Exception e) {
log.error("调用并保存TopSales评分结果异常", e);
}
return DifyWorkflowResponseDto.success(
response != null ? response.getWorkflowRunId() : null,
response != null ? response.getTaskId() : null,
response != null ? response.getData() : null,
response != null ? response.getMetadata() : null
);
}
private Integer parseInt(String s) {
try {
if (s == null) return null;
return Integer.parseInt(s.trim());
} catch (Exception e) {
return null;
}
}
private Integer firstInt(JSONObject obj, String... keys) {
for (String k : keys) {
String v = obj.getString(k);
Integer parsed = parseInt(v);
if (parsed != null) return parsed;
}
return null;
}
@Override
public TopSalesScoreResult getByWorkflowId(String workflowId) {
return this.getOne(new LambdaQueryWrapper<TopSalesScoreResult>()
.eq(TopSalesScoreResult::getWorkflowId, workflowId)
.last("limit 1"));
}
}

View File

@@ -135,3 +135,4 @@ dify:
summary-nameplate-token: app-cv5glaYrY4zgjq0XIidSoJea
portrait-3in1-token: app-jlrV6lLDlhy1YDEdUiK4hdVZ
ai-feed-token: app-b6kFOvLvAsAOxB1vDgSNa0OO
top-sales-alldim-token: app-99nVuuKfcRR2vikxgOTiNQm0

View File

@@ -0,0 +1,372 @@
app:
description: 生产环境运行,请勿动!
icon: 🤖
icon_background: '#FFEAD5'
mode: workflow
name: 【AI-Feed流】社区Feed流优质内容
use_icon_as_answer_icon: false
dependencies:
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/siliconflow:0.0.26@fb0da47fbd9113a2f7c1a70f4683be043df705e9318ff0aca4fc1d6fc9f9fe3d
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/tongyi:0.0.47@c6d9e43800aee0a9b71940310d320b8dd2214f98a725f8c8d79442aa5842dc77
kind: app
version: 0.3.1
workflow:
conversation_variables: []
environment_variables: []
features:
file_upload:
allowed_file_extensions:
- .JPG
- .JPEG
- .PNG
- .GIF
- .WEBP
- .SVG
allowed_file_types:
- image
allowed_file_upload_methods:
- local_file
- remote_url
enabled: false
fileUploadConfig:
audio_file_size_limit: 50
batch_count_limit: 5
file_size_limit: 15
image_file_size_limit: 10
video_file_size_limit: 100
workflow_file_upload_limit: 10
image:
enabled: false
number_limits: 3
transfer_methods:
- local_file
- remote_url
number_limits: 3
opening_statement: ''
retriever_resource:
enabled: true
sensitive_word_avoidance:
enabled: false
speech_to_text:
enabled: false
suggested_questions: []
suggested_questions_after_answer:
enabled: false
text_to_speech:
enabled: false
language: ''
voice: ''
graph:
edges:
- data:
isInIteration: false
sourceType: llm
targetType: end
id: 1743489785452-source-1745640429913-target
selected: false
source: '1743489785452'
sourceHandle: source
target: '1745640429913'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: llm
targetType: llm
id: 1745806378959-source-1743489785452-target
selected: false
source: '1745806378959'
sourceHandle: source
target: '1743489785452'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: llm
targetType: llm
id: 1745810872275-source-1743489785452-target
selected: false
source: '1745810872275'
sourceHandle: source
target: '1743489785452'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: start
targetType: llm
id: 1743489580997-source-1745806378959-target
selected: false
source: '1743489580997'
sourceHandle: source
target: '1745806378959'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: start
targetType: llm
id: 1743489580997-source-1745810872275-target
selected: false
source: '1743489580997'
sourceHandle: source
target: '1745810872275'
targetHandle: target
type: custom
zIndex: 0
nodes:
- data:
desc: ''
selected: false
title: 开始
type: start
variables:
- label: targetContent
max_length: null
options: []
required: true
type: paragraph
variable: targetContent
- label: targetId
max_length: null
options: []
required: true
type: text-input
variable: targetId
- label: targetType
max_length: null
options: []
required: true
type: text-input
variable: targetType
height: 142
id: '1743489580997'
position:
x: 30
y: 271
positionAbsolute:
x: 30
y: 271
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
context:
enabled: false
variable_selector: []
desc: ''
model:
completion_params:
temperature: 0.6
mode: chat
name: deepseek-ai/DeepSeek-V3
provider: langgenius/siliconflow/siliconflow
prompt_template:
- id: 0ee31105-e8cc-4a89-8abf-e9318b431d38
role: system
text: ''
- id: 28b19102-ba61-4ab1-ab23-999d21c4ad40
role: user
text: "###角色设定\n你是一位经验丰富的内容审核专家专注于用户生成内容UGC质量评估具备舆情分析、社区管理、品牌维护的专业知识。请严格按照标准评估禁止主观发挥\n\
\n###文本说明\n1. 需分析内容:{{#1743489580997.targetContent#}}\n2. 话题标签:#话题#格式的都是话题\n\
3. 图片说明【图片推荐】xxx【图片分析】xxx格式的是图片说明信息\n\n###内容评估维度\n1. 字数:{{#1745806378959.text#}}\n\
2. 图片数量:{{#1745810872275.text#}}\n3. 话题相关性:评估插入的话题和内容本身相关性\n4. 内容正负面属性(请结合图片说明信息和内容一起分析):正面、中性、负面,内容、图片任一有负面或者合规风险,即为负面内容\n\
5. 内容主题是否符合以下任一\n -知识分享(用车知识、技巧、技术讲解)\n -试驾体验(试驾感受、评价)\n -用车体验\n\
\ -车友互动(探店打卡、参与活动、节日祝福、俱乐部车友会、提车作业、旅行日记、景点推荐)\n -品牌文化(沃尔沃历史、安全理念分享)\n\
6. 内容流畅性\n -观点鲜明,能引发思考和互动,包含实质干货\n -情感真挚,语言通顺,调理清晰,有逻辑,不机械\n\n###评分标准总分100\n\
1. 字数{{#1745806378959.text#}}小于250字0分250至300字10分300以上20分\n2. 图片:\n\
\ -图片数量:{{#1745810872275.text#}}\n -图片数量小于4张图片得分 0分若存在一张不合规图片图片得分\
\ 0分\n -图片数量大于等于4张\n   - 若高推荐图片数量大于等于4图片得分 20分。\n   - 若高推荐图片数量少于4张根据图片数量给5-10分\n\
3. 话题相关性10分0个话题3分1个话题以上10分每个不相关的话题扣1分\n4. 内容正负面属性10分正面10分中性5分负面不合规0分\n\
5. 内容主题20分符合任意一种或多个且正面给20分不符合则根据主题吸引力酌情给5-10分主题负面0分\n6. 内容价值20分根据要求可在0-20分浮动打分\n\
\n###回复校验\n1. 校验输出是否为json格式是则输出则重新按照json格式输出\n2. 输出字段必须严格参照示例,只要结果,不要解释说明"
- id: 8c40ccef-1a8f-447a-9fce-fab7504f0892
role: assistant
text: "###输出示例(严格以json格式输出\n{\n    \"length\": 10, // 字数得分\n    \"image\"\
: 7, // 图片得分\n    \"topics\": 9, // 话题相关性得分\n    \"emotion\": 6, // 正负面得分\n\
    \"theme\": 8, // 主题得分\n    \"fluency\": 7, // 内容流畅度得分\n \"contentScore\"\
: 47, //总得分\n “evaluationReason”:\"(简单总结内容是否值得推荐)“\n}"
selected: false
title: 内容分析
type: llm
variables: []
vision:
configs:
detail: high
variable_selector:
- sys
- files
enabled: false
height: 90
id: '1743489785452'
position:
x: 742.6070949556324
y: 271
positionAbsolute:
x: 742.6070949556324
y: 271
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
outputs:
- value_selector:
- '1743489580997'
- targetId
variable: targetId
- value_selector:
- '1743489580997'
- targetType
variable: targetType
- value_selector:
- '1743489785452'
- text
variable: feedAiResult
selected: false
title: 结束 2
type: end
height: 142
id: '1745640429913'
position:
x: 1167.0995027951408
y: 221.25425352593578
positionAbsolute:
x: 1167.0995027951408
y: 221.25425352593578
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
context:
enabled: false
variable_selector: []
desc: ''
model:
completion_params:
temperature: 0.4
mode: chat
name: qwen2.5-72b-instruct
provider: langgenius/tongyi/tongyi
prompt_template:
- id: e7784a4d-d466-4acf-b588-107331269ea8
role: system
text: '请你作为一个精确的文字分析助手,帮我完成以下任务:
###文本说明
1. 话题标签:#话题#格式的都是
2. 图片说明【图片推荐】xxx【图片分析】xxx格式的是
###正文字数统计
1. 识别文本中不同部分(正文、图片说明、话题标签)
2. 仅统计正文部分的字数
3. 最后清晰地给出最终结果
###下面是需要分析的文本:
{{#1743489580997.targetContent#}}
严格按照实际输入内容分析,禁止联想发挥,随意生成
'
- id: 3826e25e-fc3c-4288-b6c7-4224f632686e
role: assistant
text: '输出正文字数,如果没有需要输出的内容,请给默认值 0 示例
{"num"}
回复校验:
1. 输出的字数是否是正文部分的字数,如不是,请重新分析输出
2. 输出格式是否和示例一致,不一致请输出一致
3. 仅输出最终结果即可
'
selected: false
title: 正文字数
type: llm
variables: []
vision:
enabled: false
height: 90
id: '1745806378959'
position:
x: 465.42857142857144
y: 190.65556981915387
positionAbsolute:
x: 465.42857142857144
y: 190.65556981915387
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
context:
enabled: false
variable_selector: []
desc: ''
model:
completion_params:
temperature: 0.3
mode: chat
name: qwen2.5-14b-instruct
provider: langgenius/tongyi/tongyi
prompt_template:
- id: 8925f396-1deb-4ab5-b0ae-f6b878a7accd
role: system
text: "请你作为一个精确的文字分析助手,帮我完成以下任务:\n###文本说明\n2. 图片说明【图片推荐】xxx【图片分析】xxx格式的是\n\
###图片数量统计\n1. 识识别文本中不同部分(正文、图片说明、话题标签)\n2. 仅考虑有几条图片说明信息每一条算1张图片\n3. 最后清晰地给出最终结果\n\
###下面是需要分析的文本:\n{{#1743489580997.targetContent#}}\n严格按照实际输入内容分析禁止联想发挥随意生成\n\
\n###输出示例,如果没有需要输出的内容,请给默认值 0 \n{图片数量:}\n\n###回复校验:\n1.输出格式是否和示例一致,不一致请输出一致\n\
2. 仅输出最终结果即可\n"
selected: false
title: 图片数量
type: llm
variables: []
vision:
enabled: false
height: 90
id: '1745810872275'
position:
x: 348.28571428571433
y: 373.2857142857143
positionAbsolute:
x: 348.28571428571433
y: 373.2857142857143
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
viewport:
x: 218.700086255442
y: 144.6111724133774
zoom: 0.5305007982786393

View File

@@ -0,0 +1,185 @@
app:
description: ''
icon: 🤖
icon_background: '#FFEAD5'
mode: workflow
name: 【AI】topsales 所有维度-只打分
use_icon_as_answer_icon: false
kind: app
version: 0.1.5
workflow:
conversation_variables: []
environment_variables: []
features:
file_upload:
allowed_file_extensions:
- .JPG
- .JPEG
- .PNG
- .GIF
- .WEBP
- .SVG
allowed_file_types:
- image
allowed_file_upload_methods:
- local_file
- remote_url
enabled: false
fileUploadConfig:
audio_file_size_limit: 50
batch_count_limit: 5
file_size_limit: 15
image_file_size_limit: 10
video_file_size_limit: 100
workflow_file_upload_limit: 50
image:
enabled: false
number_limits: 3
transfer_methods:
- local_file
- remote_url
number_limits: 3
opening_statement: ''
retriever_resource:
enabled: true
sensitive_word_avoidance:
enabled: false
speech_to_text:
enabled: false
suggested_questions: []
suggested_questions_after_answer:
enabled: false
text_to_speech:
enabled: false
language: ''
voice: ''
graph:
edges:
- data:
isInIteration: false
sourceType: start
targetType: llm
id: 1741767329780-source-1741767359679-target
source: '1741767329780'
sourceHandle: source
target: '1741767359679'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: llm
targetType: end
id: 1741767359679-source-1741767361631-target
source: '1741767359679'
sourceHandle: source
target: '1741767361631'
targetHandle: target
type: custom
zIndex: 0
nodes:
- data:
desc: ''
selected: false
title: 开始
type: start
variables:
- label: chat
max_length: null
options: []
required: true
type: paragraph
variable: chat
height: 90
id: '1741767329780'
position:
x: 28.571428571428555
y: 248.5
positionAbsolute:
x: 28.571428571428555
y: 248.5
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
context:
enabled: false
variable_selector: []
desc: ''
model:
completion_params:
temperature: 0.7
mode: chat
name: qwen-plus-latest
provider: tongyi
prompt_template:
- id: 104c7afc-d68b-4c57-a1a6-19e357531ec4
role: system
text: "##以下是“沃尔沃汽车销售顾问与潜在客户的现场接待语音转义语料”{{#1741767329780.chat#}},销售顾问的目标是有效挖掘客户需求、顺利引导对话至车辆介绍及销售流程,并最终促成客户下单交易。\n\
请深度剖析对话语料内容,并进行 0-100 分的连续评分,分数越高,代表销售顾问在该环节的表现越优秀。\n\n\n##评分维度及分数定义:\n\
1、自然度分数越高越自然 对话内容整体上自然且接近日常交流,使客户感到舒适和被理解。\n\n2、连贯性分数越高越连贯 对话逻辑清晰、信息传递有序,易于客户跟随并理解。\n\
\n3、流畅度分数越高越流畅 对话过程中销售顾问能够无缝地将话题导向到产品介绍或客户需求探索,进而推进至促成交易的话题,而不会让客户感觉到突兀。\n\
\n4、顾问攻击性分数越高代表越攻击性越低分数越低代表攻击性越强 如果顾问在对话中表现得过于急切或者不尊重客户的节奏,例如过早地推销产品或施加压力,则被视为具有攻击性。\n\
\n5、顾问一致性分数越高越一致 销售顾问在整场对话中保持专业形象和服务态度,信息准确一致,有助于建立信任。\n\n6、欢迎接待 销售顾问应让客户感受到重视与热情,例如“主动问候、自我介绍,并邀请客户入座,提供品味沃茶点”。\n\
\n7、品牌介绍详细介绍沃尔沃的品牌价值观、造车理念、品牌安全、品牌健康以及正面车主形象等方面。\n\n\n8、了解客户用车需求通过询问了解客户的购车需求和最重要的决策因素例如1了解客户的具体用车场景是城市通勤为主还是长途出行为主、2了解用车人是家庭为主还是个人为主、3了解用户关注内饰外观为主还是操作性能为主、4了解用户当前的持有车辆信息以及关注的其他竞品车型。\n\
\n\n9、了解客户经济预算例如购车预算、是否有金融贷款需求等\n\n\n10、推荐配置或车型根据客户需求推荐适合的车型或配置并进行详细介绍突出其设计风格和特色功能\n\
例如考虑客户主要个人用车日常通勤居多想买油车且重点考虑舒适性和安全性可推荐沃尔沃XC40。它配备了先进的安全系统如自动紧急制动和车道保持辅助非常适合日常驾驶。同时XC40燃油经济性好节省油费。车内空间宽敞周末出游携带行李也很方便。\n\
\n11、重点介绍安全性能够结合车辆不同的动力类型如电车、燃油车、混动全面介绍车辆的安全特性包括城市智能安全系统例如前向自动刹车、对向车辆智能避让、后向自动刹车、车道保持辅助、硼钢笼式车身、领航辅助、插混安全与品质、电池安全等。\
\ 介绍时可以结合用户使用场景,通过专业的技术名词和具体数据,同时结合各种修饰方式使对话生动。\n\n\n12、重点介绍健康环保介绍沃尔沃车辆的健康材质例如皮革、织物、塑料、金属、阻尼材料、座椅骨架及清洁座舱例如双效增强型空气净化、主动式预净化、智能空气循环、空气质量监测突出沃尔沃有远超行业的测试标准。\n\
\n13、重点介绍科技性能够结合车辆不同的动力类型如电车、燃油车、混动介绍车辆的动力性能例如底盘操控、插混续航、插混性能、车辆的自动泊车、全景影像、车机系统、灵巧车身、回音壁音响、续航扎实等优势可以结合用户场景通过专业的技术名词和具体数据说明车辆的科技功能给用户带来的价值。\n\
\n\n14、邀请试驾1主动邀请客户进行试驾体验介绍试驾的好处、流程及所需时间向客户介绍体验路线及其特点。 2在试驾过程中介绍车辆功能操作例如手机APP远程控制、体验动力加速性和领航系统、音响系统、盲点信息系统、启动安静省油、多种驾驶模式、自动泊车与灵巧车身等。\n\
\n15、商谈成交在商谈过程中销售顾问应结合销售政策及延伸业务促进客户成交清晰解释报价单及交付细节。\n例如沃尔沃60期金融方案、保险、延保、二手车置换、附件精品等介绍库存信息、生产状况及交车日期结合客户需求/预算/期望车型等制定《报价单》,并清晰地为客户讲解各项费用及有效期限。\n\
\n\n##输出示例(只打分,不要输出理由、分析过程。务必严格按照示例输出):\n{\n \"自然度分数\": \"\",\n \"连贯性分数\"\
: \"\",\n \"流畅度分数\": \"\",\n \"顾问攻击性分数\": \"\",\n \"顾问一致性分数\": \"\"\
,\n \"欢迎接待分数\": \"\",\n \"品牌介绍分数\": \"\",\n \"了解客户用车需求分数\": \"\",\n\
\ \"了解客户经济预算分数\": \"\",\n \"推荐配置或车型分数\": \"\",\n \"重点介绍安全性分数\": \"\"\
,\n \"重点介绍健康环保分数\": \"\",\n \"重点介绍科技性分数\": \"\",\n \"邀请试驾分数\": \"\"\
,\n \"商谈成交分数\": \"\"\n}"
selected: true
title: LLM
type: llm
variables: []
vision:
enabled: false
height: 98
id: '1741767359679'
position:
x: 345.42857142857144
y: 248.5
positionAbsolute:
x: 345.42857142857144
y: 248.5
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
outputs:
- value_selector:
- '1741767359679'
- text
variable: text
selected: false
title: 结束
type: end
height: 90
id: '1741767361631'
position:
x: 638
y: 248.5
positionAbsolute:
x: 638
y: 248.5
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
viewport:
x: 62.99090169418105
y: 189.2453904900185
zoom: 0.8418581379299643

View File

@@ -0,0 +1,25 @@
CREATE TABLE `community_feed_analysis_result` (
`id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULLCOMMENT '主键',
`workflow_id` varchar(128) NOT NULL COMMENT '工作流运行ID',
`length_score` int NOT NULL DEFAULT 0 COMMENT '字数得分',
`image_score` int NOT NULL DEFAULT 0 COMMENT '图片得分',
`topics_score` int NOT NULL DEFAULT 0 COMMENT '话题相关性得分',
`emotion_score` int NOT NULL DEFAULT 0 COMMENT '正负面得分',
`theme_score` int NOT NULL DEFAULT 0 COMMENT '主题得分',
`fluency_score` int NOT NULL DEFAULT 0 COMMENT '内容流畅度得分',
`contentScore` int NOT NULL DEFAULT 0 COMMENT '总得分',
`evaluationReason` varchar(1000) DEFAULT NULL COMMENT '推荐理由简述',
`src_content` text DEFAULT NULL COMMENT '待分析内容',
`src_id` varchar(128) DEFAULT NULL COMMENT '原内容ID',
`src_type` varchar(64) DEFAULT NULL COMMENT '原分类',
`total_tokens` int DEFAULT NULL COMMENT '总tokens',
`elapsed_time` decimal(10,2) DEFAULT NULL COMMENT '耗时(秒)',
`status` varchar(32) DEFAULT NULL COMMENT '运行状态',
`error` text DEFAULT NULL COMMENT '错误信息',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_workflow_id` (`workflow_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='社区feed流分析结果';