初始化询价智能体新仓:同步本地工程并排除密钥与 .env。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+120
@@ -0,0 +1,120 @@
|
||||
package org.jeecg.modules.inquiry.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryBotReply;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryBotReplyService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 机器人回复语 API。
|
||||
* 支持按分类拉取全部条目(后台九分类页)与分页查询。
|
||||
*/
|
||||
@Slf4j
|
||||
@Api(tags = "询价-机器人回复语")
|
||||
@RestController
|
||||
@RequestMapping("/inquiry/botReply")
|
||||
public class InquiryBotReplyController extends JeecgController<InquiryBotReply, IInquiryBotReplyService> {
|
||||
|
||||
@Autowired
|
||||
private IInquiryBotReplyService botReplyService;
|
||||
|
||||
@AutoLog(value = "回复语-分页列表")
|
||||
@ApiOperation("分页列表")
|
||||
@RequiresPermissions("inquiry:reply:list")
|
||||
@GetMapping("/list")
|
||||
public Result<IPage<InquiryBotReply>> list(InquiryBotReply query,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "100") Integer pageSize) {
|
||||
QueryWrapper<InquiryBotReply> qw = new QueryWrapper<>();
|
||||
if (StringUtils.isNotBlank(query.getCategoryCode())) {
|
||||
qw.eq("category_code", query.getCategoryCode());
|
||||
}
|
||||
if (query.getEnabled() != null) {
|
||||
qw.eq("enabled", query.getEnabled());
|
||||
}
|
||||
qw.orderByAsc("category_code").orderByAsc("sort_no");
|
||||
return Result.OK(botReplyService.page(new Page<>(pageNo, pageSize), qw));
|
||||
}
|
||||
|
||||
@ApiOperation("全量列表(按分类排序,供分类页一次加载)")
|
||||
@RequiresPermissions("inquiry:reply:list")
|
||||
@GetMapping("/listAll")
|
||||
public Result<List<InquiryBotReply>> listAll() {
|
||||
QueryWrapper<InquiryBotReply> qw = new QueryWrapper<>();
|
||||
qw.orderByAsc("category_code").orderByAsc("sort_no");
|
||||
return Result.OK(botReplyService.list(qw));
|
||||
}
|
||||
|
||||
@AutoLog(value = "回复语-新增")
|
||||
@RequiresPermissions("inquiry:reply:add")
|
||||
@PostMapping("/add")
|
||||
public Result<String> add(@RequestBody InquiryBotReply body) {
|
||||
validate(body);
|
||||
body.setCreateTime(new Date());
|
||||
if (body.getEnabled() == null) {
|
||||
body.setEnabled(1);
|
||||
}
|
||||
if (body.getSortNo() == null) {
|
||||
body.setSortNo(0);
|
||||
}
|
||||
botReplyService.save(body);
|
||||
return Result.OK("添加成功");
|
||||
}
|
||||
|
||||
@AutoLog(value = "回复语-编辑")
|
||||
@RequiresPermissions("inquiry:reply:edit")
|
||||
@PutMapping("/edit")
|
||||
public Result<String> edit(@RequestBody InquiryBotReply body) {
|
||||
if (StringUtils.isBlank(body.getId())) {
|
||||
return Result.error("id 不能为空");
|
||||
}
|
||||
validate(body);
|
||||
body.setUpdateTime(new Date());
|
||||
botReplyService.updateById(body);
|
||||
return Result.OK("更新成功");
|
||||
}
|
||||
|
||||
@AutoLog(value = "回复语-启用")
|
||||
@RequiresPermissions("inquiry:reply:edit")
|
||||
@PostMapping("/enable")
|
||||
public Result<String> enable(@RequestParam String id) {
|
||||
botReplyService.enable(id);
|
||||
return Result.OK("已启用");
|
||||
}
|
||||
|
||||
@AutoLog(value = "回复语-停用")
|
||||
@RequiresPermissions("inquiry:reply:disable")
|
||||
@PostMapping("/disable")
|
||||
public Result<String> disable(@RequestParam String id) {
|
||||
botReplyService.disable(id);
|
||||
return Result.OK("已停用");
|
||||
}
|
||||
|
||||
@RequiresPermissions("inquiry:reply:list")
|
||||
@GetMapping("/queryById")
|
||||
public Result<InquiryBotReply> queryById(@RequestParam String id) {
|
||||
return Result.OK(botReplyService.getById(id));
|
||||
}
|
||||
|
||||
private void validate(InquiryBotReply body) {
|
||||
if (StringUtils.isBlank(body.getCategoryCode())
|
||||
|| StringUtils.isBlank(body.getTriggerScene())
|
||||
|| StringUtils.isBlank(body.getReplyText())) {
|
||||
throw new IllegalArgumentException("分类、触发场景与回复正文不能为空");
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package org.jeecg.modules.inquiry.controller;
|
||||
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryDashboardService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 分析看板 API。数字全部来自主账聚合,禁止前端写死生产指标。
|
||||
*/
|
||||
@Slf4j
|
||||
@Api(tags = "询价-分析看板")
|
||||
@RestController
|
||||
@RequestMapping("/inquiry/dashboard")
|
||||
public class InquiryDashboardController {
|
||||
|
||||
@Autowired
|
||||
private IInquiryDashboardService dashboardService;
|
||||
|
||||
@ApiOperation("看板指标")
|
||||
@RequiresPermissions("inquiry:dashboard:view")
|
||||
@GetMapping("/metrics")
|
||||
public Result<Map<String, Object>> metrics(@RequestParam(name = "range", defaultValue = "today") String range) {
|
||||
return Result.OK(dashboardService.metrics(range));
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package org.jeecg.modules.inquiry.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* 询价模块业务异常转 Result,避免 Tomcat 500 页。
|
||||
* 仅处理本模块抛出的非法参数/状态;其它异常仍走 Jeecg 全局处理。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice(basePackages = "org.jeecg.modules.inquiry")
|
||||
public class InquiryExceptionAdvice {
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public Result<?> handleIllegalArgument(IllegalArgumentException e) {
|
||||
log.warn("询价参数错误: {}", e.getMessage());
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalStateException.class)
|
||||
public Result<?> handleIllegalState(IllegalStateException e) {
|
||||
log.warn("询价状态错误: {}", e.getMessage());
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package org.jeecg.modules.inquiry.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryStaffRoleService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 员工岗位管理 API。
|
||||
* <p>
|
||||
* 路径前缀 /inquiry/staffRole;权限码 inquiry:staff:*。
|
||||
* 不做企微换号,wecom_id 由运营手工绑定。
|
||||
*/
|
||||
@Slf4j
|
||||
@Api(tags = "询价-员工岗位")
|
||||
@RestController
|
||||
@RequestMapping("/inquiry/staffRole")
|
||||
public class InquiryStaffRoleController extends JeecgController<InquiryStaffRole, IInquiryStaffRoleService> {
|
||||
|
||||
@Autowired
|
||||
private IInquiryStaffRoleService staffRoleService;
|
||||
|
||||
private static final Map<String, String> ROLE_LABEL = new HashMap<>();
|
||||
|
||||
static {
|
||||
ROLE_LABEL.put("sales", "销售");
|
||||
ROLE_LABEL.put("air", "航线·空运");
|
||||
ROLE_LABEL.put("sea", "产品·海运");
|
||||
ROLE_LABEL.put("land", "产品·陆运");
|
||||
}
|
||||
|
||||
@AutoLog(value = "员工岗位-分页列表")
|
||||
@ApiOperation("分页列表")
|
||||
@RequiresPermissions("inquiry:staff:list")
|
||||
@GetMapping("/list")
|
||||
public Result<IPage<InquiryStaffRole>> list(InquiryStaffRole query,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "20") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<InquiryStaffRole> qw = new QueryWrapper<>();
|
||||
if (StringUtils.isNotBlank(query.getRoleCode())) {
|
||||
qw.eq("role_code", query.getRoleCode());
|
||||
}
|
||||
if (StringUtils.isNotBlank(query.getStatus())) {
|
||||
qw.eq("status", query.getStatus());
|
||||
}
|
||||
String keyword = req.getParameter("keyword");
|
||||
if (StringUtils.isNotBlank(keyword)) {
|
||||
qw.and(w -> w.like("name", keyword).or().like("wecom_id", keyword).or().like("mobile", keyword));
|
||||
}
|
||||
if (StringUtils.isNotBlank(query.getRoutes())) {
|
||||
qw.like("routes", query.getRoutes());
|
||||
}
|
||||
qw.orderByDesc("create_time");
|
||||
return Result.OK(staffRoleService.page(new Page<>(pageNo, pageSize), qw));
|
||||
}
|
||||
|
||||
@ApiOperation("角色人数统计")
|
||||
@RequiresPermissions("inquiry:staff:list")
|
||||
@GetMapping("/counts")
|
||||
public Result<Map<String, Long>> counts() {
|
||||
return Result.OK(staffRoleService.countByRole());
|
||||
}
|
||||
|
||||
@AutoLog(value = "员工岗位-新增")
|
||||
@ApiOperation("新增")
|
||||
@RequiresPermissions("inquiry:staff:add")
|
||||
@PostMapping("/add")
|
||||
public Result<String> add(@RequestBody InquiryStaffRole body) {
|
||||
normalize(body);
|
||||
body.setCreateTime(new Date());
|
||||
if (StringUtils.isBlank(body.getStatus())) {
|
||||
body.setStatus("active");
|
||||
}
|
||||
staffRoleService.save(body);
|
||||
return Result.OK("添加成功");
|
||||
}
|
||||
|
||||
@AutoLog(value = "员工岗位-编辑")
|
||||
@ApiOperation("编辑")
|
||||
@RequiresPermissions("inquiry:staff:edit")
|
||||
@PutMapping("/edit")
|
||||
public Result<String> edit(@RequestBody InquiryStaffRole body) {
|
||||
if (StringUtils.isBlank(body.getId())) {
|
||||
return Result.error("id 不能为空");
|
||||
}
|
||||
normalize(body);
|
||||
body.setUpdateTime(new Date());
|
||||
staffRoleService.updateById(body);
|
||||
return Result.OK("更新成功");
|
||||
}
|
||||
|
||||
@AutoLog(value = "员工岗位-停用")
|
||||
@ApiOperation("停用")
|
||||
@RequiresPermissions("inquiry:staff:disable")
|
||||
@PostMapping("/disable")
|
||||
public Result<String> disable(@RequestParam String id) {
|
||||
staffRoleService.disable(id);
|
||||
return Result.OK("已停用");
|
||||
}
|
||||
|
||||
@ApiOperation("按 id 查询")
|
||||
@RequiresPermissions("inquiry:staff:list")
|
||||
@GetMapping("/queryById")
|
||||
public Result<InquiryStaffRole> queryById(@RequestParam String id) {
|
||||
return Result.OK(staffRoleService.getById(id));
|
||||
}
|
||||
|
||||
private void normalize(InquiryStaffRole body) {
|
||||
if (StringUtils.isBlank(body.getName()) || StringUtils.isBlank(body.getWecomId())) {
|
||||
throw new IllegalArgumentException("姓名与企微 userid 不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(body.getRoleCode())) {
|
||||
body.setRoleCode("sales");
|
||||
}
|
||||
if (StringUtils.isBlank(body.getRoleLabel())) {
|
||||
body.setRoleLabel(ROLE_LABEL.getOrDefault(body.getRoleCode(), body.getRoleCode()));
|
||||
}
|
||||
if (body.getNotifyEventException() == null) {
|
||||
body.setNotifyEventException(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package org.jeecg.modules.inquiry.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.modules.inquiry.dto.TicketAbnormalRequest;
|
||||
import org.jeecg.modules.inquiry.dto.TicketTransitionRequest;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryWorkOrderService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 询价工单 API:列表、聚合详情、六态流转、异常处理/重试。
|
||||
* <p>
|
||||
* 请求线程只做 DB 事务与校验,不调用 LLM / LibreOffice / Graph。
|
||||
* 权限:inquiry:workorder:list / transition 等。
|
||||
*/
|
||||
@Slf4j
|
||||
@Api(tags = "询价-工单")
|
||||
@RestController
|
||||
@RequestMapping("/inquiry/workOrder")
|
||||
public class InquiryWorkOrderController {
|
||||
|
||||
@Autowired
|
||||
private IInquiryWorkOrderService workOrderService;
|
||||
|
||||
@AutoLog(value = "工单-列表视图")
|
||||
@ApiOperation("后台列表")
|
||||
@RequiresPermissions("inquiry:workorder:list")
|
||||
@GetMapping("/view/list")
|
||||
public Result<IPage<InquiryTicket>> viewList(
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "20") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
Map<String, String> filters = new HashMap<>();
|
||||
copyParam(req, filters, "keyword");
|
||||
copyParam(req, filters, "status");
|
||||
copyParam(req, filters, "bizType");
|
||||
copyParam(req, filters, "businessLine");
|
||||
copyParam(req, filters, "eventException");
|
||||
copyParam(req, filters, "systemException");
|
||||
copyParam(req, filters, "dateFrom");
|
||||
copyParam(req, filters, "dateTo");
|
||||
return Result.OK(workOrderService.pageView(new Page<>(pageNo, pageSize), filters));
|
||||
}
|
||||
|
||||
@ApiOperation("兼容 list 路径")
|
||||
@RequiresPermissions("inquiry:workorder:list")
|
||||
@GetMapping("/list")
|
||||
public Result<IPage<InquiryTicket>> list(
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "20") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
return viewList(pageNo, pageSize, req);
|
||||
}
|
||||
|
||||
@AutoLog(value = "工单-聚合详情")
|
||||
@ApiOperation("聚合详情")
|
||||
@RequiresPermissions("inquiry:workorder:list")
|
||||
@GetMapping("/view/detail")
|
||||
public Result<Map<String, Object>> viewDetail(@RequestParam String workOrderNo) {
|
||||
return Result.OK(workOrderService.detailView(workOrderNo));
|
||||
}
|
||||
|
||||
@ApiOperation("按工单号查主表")
|
||||
@RequiresPermissions("inquiry:workorder:list")
|
||||
@GetMapping("/queryById")
|
||||
public Result<InquiryTicket> queryById(@RequestParam(required = false) String id,
|
||||
@RequestParam(required = false) String workOrderNo) {
|
||||
if (workOrderNo != null && !workOrderNo.isEmpty()) {
|
||||
Map<String, Object> detail = workOrderService.detailView(workOrderNo);
|
||||
return Result.OK((InquiryTicket) detail.get("ticket"));
|
||||
}
|
||||
return Result.OK(workOrderService.getById(id));
|
||||
}
|
||||
|
||||
@AutoLog(value = "工单-六态流转")
|
||||
@ApiOperation("六态流转(含关单)")
|
||||
@RequiresPermissions("inquiry:workorder:transition")
|
||||
@PostMapping("/transition")
|
||||
public Result<InquiryTicket> transition(@RequestBody TicketTransitionRequest req) {
|
||||
try {
|
||||
return Result.OK(workOrderService.transition(req, currentUser()));
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@AutoLog(value = "工单-异常处理")
|
||||
@ApiOperation("异常处理")
|
||||
@RequiresPermissions("inquiry:workorder:transition")
|
||||
@PostMapping("/abnormal/handle")
|
||||
public Result<String> abnormalHandle(@RequestBody TicketAbnormalRequest req) {
|
||||
try {
|
||||
workOrderService.handleAbnormal(req, currentUser());
|
||||
return Result.OK("已处理");
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@AutoLog(value = "工单-异常重试")
|
||||
@ApiOperation("系统异常重试")
|
||||
@RequiresPermissions("inquiry:workorder:transition")
|
||||
@PostMapping("/abnormal/retry")
|
||||
public Result<String> abnormalRetry(@RequestBody TicketAbnormalRequest req) {
|
||||
try {
|
||||
workOrderService.retryAbnormal(req, currentUser());
|
||||
return Result.OK("已提交重试");
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyParam(HttpServletRequest req, Map<String, String> filters, String key) {
|
||||
String v = req.getParameter(key);
|
||||
if (v != null && !v.trim().isEmpty()) {
|
||||
filters.put(key, v.trim());
|
||||
}
|
||||
}
|
||||
|
||||
private String currentUser() {
|
||||
try {
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
if (user != null && user.getUsername() != null) {
|
||||
return user.getUsername();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 无登录上下文时记为 system(例如服务账号调用)
|
||||
}
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package org.jeecg.modules.inquiry.domain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 询价工单六态与允许流转规则。
|
||||
* <p>
|
||||
* 职责:集中定义状态字面量与 transition 合法性,避免 Controller/Service 散落魔法字符串。
|
||||
* 副作用:无;纯内存规则。线程安全(不可变 Map)。
|
||||
* 为何这样写:六态是主账 Owner,非法流转必须在服务端拒绝,前端按钮隐藏不能当真相。
|
||||
*/
|
||||
public final class TicketStatuses {
|
||||
|
||||
public static final String INQUIRING = "询价中";
|
||||
public static final String QUOTED = "已报价";
|
||||
public static final String NEGOTIATING = "协商中";
|
||||
public static final String DEAL = "已成交";
|
||||
public static final String LOST = "未成交";
|
||||
public static final String CLOSED = "已关闭";
|
||||
|
||||
private static final Set<String> ALL = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
||||
INQUIRING, QUOTED, NEGOTIATING, DEAL, LOST, CLOSED
|
||||
)));
|
||||
|
||||
/**
|
||||
* from -> 允许的 to 集合。
|
||||
* 终态(已成交/未成交/已关闭)默认不可再流转,除非显式「关闭」从非终态进入已关闭。
|
||||
*/
|
||||
private static final Map<String, Set<String>> ALLOWED;
|
||||
|
||||
static {
|
||||
Map<String, Set<String>> m = new HashMap<>();
|
||||
m.put(INQUIRING, setOf(QUOTED, NEGOTIATING, CLOSED));
|
||||
m.put(QUOTED, setOf(NEGOTIATING, DEAL, LOST, CLOSED));
|
||||
m.put(NEGOTIATING, setOf(QUOTED, DEAL, LOST, CLOSED));
|
||||
m.put(DEAL, Collections.emptySet());
|
||||
m.put(LOST, Collections.emptySet());
|
||||
m.put(CLOSED, Collections.emptySet());
|
||||
ALLOWED = Collections.unmodifiableMap(m);
|
||||
}
|
||||
|
||||
private TicketStatuses() {
|
||||
}
|
||||
|
||||
public static boolean isKnown(String status) {
|
||||
return status != null && ALL.contains(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验是否允许从 from 转到 to。
|
||||
*
|
||||
* @param from 当前状态
|
||||
* @param to 目标状态
|
||||
* @return true 表示允许
|
||||
*/
|
||||
public static boolean canTransit(String from, String to) {
|
||||
if (!isKnown(from) || !isKnown(to)) {
|
||||
return false;
|
||||
}
|
||||
Set<String> next = ALLOWED.get(from);
|
||||
return next != null && next.contains(to);
|
||||
}
|
||||
|
||||
public static Set<String> all() {
|
||||
return ALL;
|
||||
}
|
||||
|
||||
private static Set<String> setOf(String... values) {
|
||||
return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(values)));
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package org.jeecg.modules.inquiry.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 异常处理 / 重试请求。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("异常处理请求")
|
||||
public class TicketAbnormalRequest {
|
||||
|
||||
@ApiModelProperty(value = "工单号", required = true)
|
||||
private String workOrderNo;
|
||||
|
||||
@ApiModelProperty("处理说明")
|
||||
private String remark;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package org.jeecg.modules.inquiry.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 工单六态流转请求。
|
||||
* 参数:工单号 + 目标状态 + 可选备注;由主账校验合法性后落库。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("工单状态流转请求")
|
||||
public class TicketTransitionRequest {
|
||||
|
||||
@ApiModelProperty(value = "工单号", required = true)
|
||||
private String workOrderNo;
|
||||
|
||||
@ApiModelProperty(value = "目标六态", required = true)
|
||||
private String toStatus;
|
||||
|
||||
@ApiModelProperty("备注/关单原因")
|
||||
private String remark;
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package org.jeecg.modules.inquiry.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 工单附件元数据,对应 inq_attachment。只存对象存储键,不存文件字节。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("inq_attachment")
|
||||
@ApiModel(value = "inq_attachment", description = "工单附件元数据")
|
||||
public class InquiryAttachment implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private String id;
|
||||
private String ticketId;
|
||||
private String workOrderNo;
|
||||
private String fileName;
|
||||
private String objectKey;
|
||||
private String contentType;
|
||||
private Long fileSize;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package org.jeecg.modules.inquiry.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 机器人回复语,对应 inq_bot_reply。
|
||||
* 按分类维护触发场景与正文;智能体出站文案可后续读取本表(本期后台 CRUD)。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("inq_bot_reply")
|
||||
@ApiModel(value = "inq_bot_reply", description = "机器人回复语")
|
||||
public class InquiryBotReply implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private String id;
|
||||
|
||||
@ApiModelProperty("分类码")
|
||||
private String categoryCode;
|
||||
|
||||
@ApiModelProperty("分类标题")
|
||||
private String categoryTitle;
|
||||
|
||||
@ApiModelProperty("触发场景")
|
||||
private String triggerScene;
|
||||
|
||||
@ApiModelProperty("回复正文")
|
||||
private String replyText;
|
||||
|
||||
@ApiModelProperty("是否启用 0/1")
|
||||
private Integer enabled;
|
||||
|
||||
@ApiModelProperty("排序")
|
||||
private Integer sortNo;
|
||||
|
||||
private String remark;
|
||||
private String createBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
private String updateBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package org.jeecg.modules.inquiry.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 询价异常单,对应 inq_exception_case。
|
||||
* 后台「处理异常 / 重试」写此表,并回写工单上的异常摘要字段。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("inq_exception_case")
|
||||
@ApiModel(value = "inq_exception_case", description = "询价异常单")
|
||||
public class InquiryExceptionCase implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private String id;
|
||||
private String ticketId;
|
||||
private String workOrderNo;
|
||||
private String caseType;
|
||||
private String title;
|
||||
private String reason;
|
||||
private String status;
|
||||
private Integer retryCount;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date lastRetryAt;
|
||||
|
||||
private String handledBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date handledAt;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package org.jeecg.modules.inquiry.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 报价快照(只读展示),对应 inq_quote。本期不做模板上传/启停。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("inq_quote")
|
||||
@ApiModel(value = "inq_quote", description = "工单报价快照")
|
||||
public class InquiryQuote implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private String id;
|
||||
private String ticketId;
|
||||
private String workOrderNo;
|
||||
private Integer quoteVersion;
|
||||
private String source;
|
||||
private String sourceLabel;
|
||||
private String priceText;
|
||||
private Integer highlighted;
|
||||
private String payloadJson;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package org.jeecg.modules.inquiry.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 询价业务员工岗位,对应 inq_staff_role。
|
||||
* <p>
|
||||
* 与 Jeecg sys_user 分离:本表管销售/航线/产品岗位与企微 wecom_id;
|
||||
* 登录账号走 /sys/user。智能体按入站 sender_id 查本表角色。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("inq_staff_role")
|
||||
@ApiModel(value = "inq_staff_role", description = "询价业务员工岗位")
|
||||
public class InquiryStaffRole implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private String id;
|
||||
|
||||
@ApiModelProperty("姓名")
|
||||
private String name;
|
||||
|
||||
@ApiModelProperty("企微 userid")
|
||||
private String wecomId;
|
||||
|
||||
@ApiModelProperty("手机")
|
||||
private String mobile;
|
||||
|
||||
@ApiModelProperty("部门")
|
||||
private String departName;
|
||||
|
||||
@ApiModelProperty("角色码 sales/air/sea/land")
|
||||
private String roleCode;
|
||||
|
||||
@ApiModelProperty("角色展示名")
|
||||
private String roleLabel;
|
||||
|
||||
@ApiModelProperty("负责线路")
|
||||
private String routes;
|
||||
|
||||
@ApiModelProperty("是否接收事件异常提醒 0/1")
|
||||
private Integer notifyEventException;
|
||||
|
||||
@ApiModelProperty("active/disabled")
|
||||
private String status;
|
||||
|
||||
private String createBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
private String updateBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package org.jeecg.modules.inquiry.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 询价工单实体,对应表 inq_ticket。
|
||||
* <p>
|
||||
* 职责:持久化六态工单主事实;列表/详情/看板聚合均以此表为真相源之一。
|
||||
* 副作用:写库经 Service;禁止前端本地改 status 冒充主账成功。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("inq_ticket")
|
||||
@ApiModel(value = "inq_ticket", description = "询价工单")
|
||||
public class InquiryTicket implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@ApiModelProperty("主键")
|
||||
private String id;
|
||||
|
||||
@ApiModelProperty("工单号")
|
||||
private String workOrderNo;
|
||||
|
||||
@ApiModelProperty("发起销售企微 userid")
|
||||
private String salesWecomId;
|
||||
|
||||
@ApiModelProperty("销售展示名")
|
||||
private String salesName;
|
||||
|
||||
@ApiModelProperty("客户名称")
|
||||
private String customerName;
|
||||
|
||||
@ApiModelProperty("业务线 SEA/AIR/LAND")
|
||||
private String businessLine;
|
||||
|
||||
@ApiModelProperty("展示业务类型")
|
||||
private String bizTypeLabel;
|
||||
|
||||
@ApiModelProperty("起运地")
|
||||
private String origin;
|
||||
|
||||
@ApiModelProperty("目的地")
|
||||
private String destination;
|
||||
|
||||
@ApiModelProperty("需求标题")
|
||||
private String demandTitle;
|
||||
|
||||
@ApiModelProperty("需求详情")
|
||||
private String demandDetail;
|
||||
|
||||
@ApiModelProperty("品名")
|
||||
private String goodsName;
|
||||
|
||||
@ApiModelProperty("件数")
|
||||
private Integer pieces;
|
||||
|
||||
@ApiModelProperty("重量KG")
|
||||
private BigDecimal weightKg;
|
||||
|
||||
@ApiModelProperty("体积CBM")
|
||||
private BigDecimal volumeCbm;
|
||||
|
||||
@ApiModelProperty("六态")
|
||||
private String status;
|
||||
|
||||
@ApiModelProperty("成本价展示")
|
||||
private String costPrice;
|
||||
|
||||
@ApiModelProperty("售价展示")
|
||||
private String salePrice;
|
||||
|
||||
@ApiModelProperty("毛利率展示")
|
||||
private String profitRate;
|
||||
|
||||
@ApiModelProperty("是否命中 TMS")
|
||||
private Integer tmsHit;
|
||||
|
||||
@ApiModelProperty("是否含手工报价")
|
||||
private Integer manualQuote;
|
||||
|
||||
@ApiModelProperty("事件异常摘要")
|
||||
private String eventException;
|
||||
|
||||
@ApiModelProperty("系统异常摘要")
|
||||
private String systemException;
|
||||
|
||||
@ApiModelProperty("系统异常原因")
|
||||
private String systemExceptionReason;
|
||||
|
||||
@ApiModelProperty("关单原因")
|
||||
private String closeReason;
|
||||
|
||||
@ApiModelProperty("业务版本")
|
||||
private Integer version;
|
||||
|
||||
private String createBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
private String updateBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package org.jeecg.modules.inquiry.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 工单状态流转日志,对应 inq_ticket_status_log。
|
||||
* 用于详情时间线;每次 transition / close / retry / abnormal_handle 写一行。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("inq_ticket_status_log")
|
||||
@ApiModel(value = "inq_ticket_status_log", description = "工单状态日志")
|
||||
public class InquiryTicketStatusLog implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private String id;
|
||||
private String ticketId;
|
||||
private String workOrderNo;
|
||||
private String fromStatus;
|
||||
private String toStatus;
|
||||
private String action;
|
||||
private String operator;
|
||||
private String remark;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package org.jeecg.modules.inquiry.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryAttachment;
|
||||
|
||||
@Mapper
|
||||
public interface InquiryAttachmentMapper extends BaseMapper<InquiryAttachment> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package org.jeecg.modules.inquiry.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryBotReply;
|
||||
|
||||
@Mapper
|
||||
public interface InquiryBotReplyMapper extends BaseMapper<InquiryBotReply> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package org.jeecg.modules.inquiry.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryExceptionCase;
|
||||
|
||||
@Mapper
|
||||
public interface InquiryExceptionCaseMapper extends BaseMapper<InquiryExceptionCase> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package org.jeecg.modules.inquiry.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryQuote;
|
||||
|
||||
@Mapper
|
||||
public interface InquiryQuoteMapper extends BaseMapper<InquiryQuote> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package org.jeecg.modules.inquiry.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||
|
||||
@Mapper
|
||||
public interface InquiryStaffRoleMapper extends BaseMapper<InquiryStaffRole> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package org.jeecg.modules.inquiry.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||
|
||||
@Mapper
|
||||
public interface InquiryTicketMapper extends BaseMapper<InquiryTicket> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package org.jeecg.modules.inquiry.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicketStatusLog;
|
||||
|
||||
@Mapper
|
||||
public interface InquiryTicketStatusLogMapper extends BaseMapper<InquiryTicketStatusLog> {
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 友通达新系统 · 询价主账手写模块(从零实现)。
|
||||
* <p>
|
||||
* 本期能力:分析看板、工单六态、员工企微岗位、机器人回复语。
|
||||
* 排除:报价模板、识别任务、报价生成、存档死信。
|
||||
* 数据只写 inquiry_robot;禁止连现网 jeecg-boot。
|
||||
*/
|
||||
package org.jeecg.modules.inquiry;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package org.jeecg.modules.inquiry.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryBotReply;
|
||||
|
||||
/**
|
||||
* 机器人回复语服务。
|
||||
*/
|
||||
public interface IInquiryBotReplyService extends IService<InquiryBotReply> {
|
||||
|
||||
void enable(String id);
|
||||
|
||||
void disable(String id);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package org.jeecg.modules.inquiry.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 分析看板聚合。数字一律来自主账库,禁止前端假数据当生产。
|
||||
*/
|
||||
public interface IInquiryDashboardService {
|
||||
|
||||
/**
|
||||
* 返回看板指标:metrics 列表 + trend + rankings。
|
||||
*
|
||||
* @param range today|7d|30d,默认 today
|
||||
*/
|
||||
Map<String, Object> metrics(String range);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package org.jeecg.modules.inquiry.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 询价业务员工岗位服务。
|
||||
*/
|
||||
public interface IInquiryStaffRoleService extends IService<InquiryStaffRole> {
|
||||
|
||||
/**
|
||||
* 按角色统计人数(含 all)。
|
||||
*/
|
||||
Map<String, Long> countByRole();
|
||||
|
||||
/**
|
||||
* 停用员工(status=disabled),不物理删除。
|
||||
*/
|
||||
void disable(String id);
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package org.jeecg.modules.inquiry.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.inquiry.dto.TicketAbnormalRequest;
|
||||
import org.jeecg.modules.inquiry.dto.TicketTransitionRequest;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 询价工单服务:列表、聚合详情、六态流转、异常处理/重试。
|
||||
* <p>
|
||||
* 重要:仅按单条工单写库,禁止全局大锁串行所有工单。
|
||||
*/
|
||||
public interface IInquiryWorkOrderService extends IService<InquiryTicket> {
|
||||
|
||||
/**
|
||||
* 后台列表视图(支持关键字/状态/业务类型/异常筛选)。
|
||||
*/
|
||||
IPage<InquiryTicket> pageView(Page<InquiryTicket> page, Map<String, String> filters);
|
||||
|
||||
/**
|
||||
* 聚合详情:工单 + 报价列表 + 附件 + 时间线 + 开放异常。
|
||||
*/
|
||||
Map<String, Object> detailView(String workOrderNo);
|
||||
|
||||
/**
|
||||
* 六态流转(含关单到「已关闭」)。
|
||||
*
|
||||
* @param req 请求
|
||||
* @param operator 当前登录用户名
|
||||
*/
|
||||
InquiryTicket transition(TicketTransitionRequest req, String operator);
|
||||
|
||||
/**
|
||||
* 标记异常已处理,并清理工单上对应异常摘要。
|
||||
*/
|
||||
void handleAbnormal(TicketAbnormalRequest req, String operator);
|
||||
|
||||
/**
|
||||
* 系统异常重试:累加 retry_count,记日志;真正重跑任务由 Worker 后续认领(本期只落主账状态)。
|
||||
*/
|
||||
void retryAbnormal(TicketAbnormalRequest req, String operator);
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package org.jeecg.modules.inquiry.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryBotReply;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryBotReplyMapper;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryBotReplyService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 回复语启停与 CRUD。
|
||||
*/
|
||||
@Service
|
||||
public class InquiryBotReplyServiceImpl extends ServiceImpl<InquiryBotReplyMapper, InquiryBotReply>
|
||||
implements IInquiryBotReplyService {
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void enable(String id) {
|
||||
setEnabled(id, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void disable(String id) {
|
||||
setEnabled(id, 0);
|
||||
}
|
||||
|
||||
private void setEnabled(String id, int enabled) {
|
||||
InquiryBotReply row = getById(id);
|
||||
if (row == null) {
|
||||
throw new IllegalArgumentException("回复语不存在: " + id);
|
||||
}
|
||||
row.setEnabled(enabled);
|
||||
row.setUpdateTime(new Date());
|
||||
updateById(row);
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
package org.jeecg.modules.inquiry.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.jeecg.modules.inquiry.domain.TicketStatuses;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryStaffRoleMapper;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryTicketMapper;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryDashboardService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 看板实时聚合实现(无快照依赖)。
|
||||
* <p>
|
||||
* 计算口径对齐后台原型:询价总数、待处理、TMS 命中率、异常完成率、手工报价占比、
|
||||
* 已成交、成交率、反馈率;内部协同时长本期无协同表时返回「-」。
|
||||
* 线程:只读查询,无写锁。
|
||||
*/
|
||||
@Service
|
||||
public class InquiryDashboardServiceImpl implements IInquiryDashboardService {
|
||||
|
||||
private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
|
||||
|
||||
@Autowired
|
||||
private InquiryTicketMapper ticketMapper;
|
||||
@Autowired
|
||||
private InquiryStaffRoleMapper staffRoleMapper;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> metrics(String range) {
|
||||
LocalDate today = LocalDate.now(ZONE);
|
||||
LocalDate fromDate;
|
||||
if ("7d".equalsIgnoreCase(range)) {
|
||||
fromDate = today.minusDays(6);
|
||||
} else if ("30d".equalsIgnoreCase(range)) {
|
||||
fromDate = today.minusDays(29);
|
||||
} else {
|
||||
fromDate = today;
|
||||
}
|
||||
Date from = Date.from(fromDate.atStartOfDay(ZONE).toInstant());
|
||||
Date to = Date.from(LocalDateTime.of(today, LocalTime.MAX).atZone(ZONE).toInstant());
|
||||
|
||||
List<InquiryTicket> tickets = ticketMapper.selectList(new LambdaQueryWrapper<InquiryTicket>()
|
||||
.ge(InquiryTicket::getCreateTime, from)
|
||||
.le(InquiryTicket::getCreateTime, to));
|
||||
|
||||
long total = tickets.size();
|
||||
long deal = tickets.stream().filter(t -> TicketStatuses.DEAL.equals(t.getStatus())).count();
|
||||
long lost = tickets.stream().filter(t -> TicketStatuses.LOST.equals(t.getStatus())).count();
|
||||
long closed = tickets.stream().filter(t -> TicketStatuses.CLOSED.equals(t.getStatus())).count();
|
||||
long quoted = tickets.stream().filter(t -> TicketStatuses.QUOTED.equals(t.getStatus())
|
||||
|| TicketStatuses.DEAL.equals(t.getStatus())
|
||||
|| TicketStatuses.LOST.equals(t.getStatus())
|
||||
|| TicketStatuses.NEGOTIATING.equals(t.getStatus())).count();
|
||||
long pending = total - deal - lost - closed;
|
||||
long tmsHit = tickets.stream().filter(t -> t.getTmsHit() != null && t.getTmsHit() == 1).count();
|
||||
long manual = tickets.stream().filter(t -> t.getManualQuote() != null && t.getManualQuote() == 1).count();
|
||||
long hasEventExc = tickets.stream().filter(t -> hasExc(t.getEventException())).count();
|
||||
long hasSysExc = tickets.stream().filter(t -> hasExc(t.getSystemException())).count();
|
||||
long abnormalTotal = hasEventExc + hasSysExc;
|
||||
// 简化:已关闭且曾有异常视为异常处理完成
|
||||
long abnormalDone = tickets.stream()
|
||||
.filter(t -> TicketStatuses.CLOSED.equals(t.getStatus()))
|
||||
.filter(t -> hasExc(t.getEventException()) || hasExc(t.getSystemException())
|
||||
|| (t.getCloseReason() != null && t.getCloseReason().contains("异常")))
|
||||
.count();
|
||||
|
||||
List<Map<String, Object>> metricCards = new ArrayList<>();
|
||||
metricCards.add(card("inquiry", "询价总数", "周期内询价工单总数", String.valueOf(total), false));
|
||||
metricCards.add(card("pending", "待处理工单", "工单总数 − 已成交 − 未成交 − 已关闭", pending + " 单", false));
|
||||
metricCards.add(card("tms", "标准报价命中率", "命中 TMS 标准价的询价数 ÷ 询价总数 × 100%", pct(tmsHit, total), false));
|
||||
metricCards.add(card("abnormal", "异常处理完成率", "已关闭异常工单数 ÷ 异常工单总数 × 100%",
|
||||
abnormalTotal == 0 ? "100%" : pct(abnormalDone, Math.max(abnormalTotal, 1)), false));
|
||||
metricCards.add(card("manual", "手工报价占比", "人工报价的询价数 ÷ 询价总数 × 100%", pct(manual, total), false));
|
||||
metricCards.add(card("accepted", "已成交工单", "状态=已成交的工单数量", String.valueOf(deal), false));
|
||||
metricCards.add(card("dealRate", "报价成交率", "已成交数 ÷ 已报价相关数 × 100%", pct(deal, Math.max(quoted, 1)), false));
|
||||
long feedbackNumer = deal + lost;
|
||||
// 反馈率口径:已成交+未成交(相对周期工单)
|
||||
metricCards.add(card("feedback", "报价反馈率", "(已成交 + 未成交)÷ 工单数 × 100%", pct(feedbackNumer, total), false));
|
||||
metricCards.add(card("avgResp", "内部协同平均响应时长", "本期无协同明细表,暂无数据", "-", true));
|
||||
|
||||
// 近 7 日趋势(不受 range 限制,便于折线)
|
||||
List<Map<String, Object>> trend = new ArrayList<>();
|
||||
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("MM-dd");
|
||||
for (int i = 6; i >= 0; i--) {
|
||||
LocalDate d = today.minusDays(i);
|
||||
Date dFrom = Date.from(d.atStartOfDay(ZONE).toInstant());
|
||||
Date dTo = Date.from(LocalDateTime.of(d, LocalTime.MAX).atZone(ZONE).toInstant());
|
||||
long cnt = ticketMapper.selectCount(new LambdaQueryWrapper<InquiryTicket>()
|
||||
.ge(InquiryTicket::getCreateTime, dFrom)
|
||||
.le(InquiryTicket::getCreateTime, dTo));
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("date", d.format(fmt));
|
||||
p.put("value", cnt);
|
||||
trend.add(p);
|
||||
}
|
||||
|
||||
// 人员排名:按销售名聚合询价量
|
||||
Map<String, Long> bySales = tickets.stream()
|
||||
.filter(t -> t.getSalesName() != null && !t.getSalesName().isEmpty())
|
||||
.collect(Collectors.groupingBy(InquiryTicket::getSalesName, Collectors.counting()));
|
||||
Map<String, InquiryStaffRole> staffByName = staffRoleMapper.selectList(null).stream()
|
||||
.collect(Collectors.toMap(InquiryStaffRole::getName, s -> s, (a, b) -> a));
|
||||
List<Map<String, Object>> rankings = bySales.entrySet().stream()
|
||||
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
|
||||
.limit(20)
|
||||
.map(e -> {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("name", e.getKey());
|
||||
InquiryStaffRole staff = staffByName.get(e.getKey());
|
||||
if (staff != null) {
|
||||
row.put("role", mapRankRole(staff.getRoleCode()));
|
||||
row.put("dept", staff.getDepartName());
|
||||
} else {
|
||||
row.put("role", "sales");
|
||||
row.put("dept", "-");
|
||||
}
|
||||
row.put("volumeNum", e.getValue());
|
||||
return row;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("range", range == null ? "today" : range);
|
||||
result.put("metrics", metricCards);
|
||||
result.put("trend", trend);
|
||||
result.put("rankings", rankings);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean hasExc(String v) {
|
||||
return v != null && !v.trim().isEmpty() && !"-".equals(v.trim());
|
||||
}
|
||||
|
||||
private static String pct(long numer, long denom) {
|
||||
if (denom <= 0) {
|
||||
return "0.0%";
|
||||
}
|
||||
BigDecimal p = BigDecimal.valueOf(numer * 100.0 / denom).setScale(1, RoundingMode.HALF_UP);
|
||||
return p.toPlainString() + "%";
|
||||
}
|
||||
|
||||
private static Map<String, Object> card(String key, String label, String tip, String value, boolean isTime) {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("key", key);
|
||||
m.put("label", label);
|
||||
m.put("tip", tip);
|
||||
m.put("value", value);
|
||||
m.put("isTime", isTime);
|
||||
return m;
|
||||
}
|
||||
|
||||
private static String mapRankRole(String roleCode) {
|
||||
if ("air".equals(roleCode) || "sea".equals(roleCode) || "land".equals(roleCode)) {
|
||||
return "air".equals(roleCode) ? "route" : "product";
|
||||
}
|
||||
return "sales";
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package org.jeecg.modules.inquiry.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryStaffRoleMapper;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryStaffRoleService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 员工岗位 CRUD 实现。
|
||||
* <p>
|
||||
* 线程:跟随 HTTP 请求线程;无全局锁。wecom_id 唯一约束由库保证。
|
||||
*/
|
||||
@Service
|
||||
public class InquiryStaffRoleServiceImpl extends ServiceImpl<InquiryStaffRoleMapper, InquiryStaffRole>
|
||||
implements IInquiryStaffRoleService {
|
||||
|
||||
private static final String[] ROLE_CODES = {"sales", "air", "sea", "land"};
|
||||
|
||||
@Override
|
||||
public Map<String, Long> countByRole() {
|
||||
List<InquiryStaffRole> all = list();
|
||||
Map<String, Long> map = new HashMap<>();
|
||||
map.put("all", (long) all.size());
|
||||
for (String code : ROLE_CODES) {
|
||||
long c = all.stream().filter(r -> code.equals(r.getRoleCode())).count();
|
||||
map.put(code, c);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void disable(String id) {
|
||||
InquiryStaffRole row = getById(id);
|
||||
if (row == null) {
|
||||
throw new IllegalArgumentException("员工不存在: " + id);
|
||||
}
|
||||
row.setStatus("disabled");
|
||||
row.setUpdateTime(new Date());
|
||||
updateById(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 wecom_id 查一条(智能体/后台可复用)。
|
||||
*/
|
||||
public InquiryStaffRole findByWecomId(String wecomId) {
|
||||
return getOne(new QueryWrapper<InquiryStaffRole>().eq("wecom_id", wecomId).last("LIMIT 1"), false);
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
package org.jeecg.modules.inquiry.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.inquiry.domain.TicketStatuses;
|
||||
import org.jeecg.modules.inquiry.dto.TicketAbnormalRequest;
|
||||
import org.jeecg.modules.inquiry.dto.TicketTransitionRequest;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryAttachment;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryExceptionCase;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryQuote;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicketStatusLog;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryAttachmentMapper;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryExceptionCaseMapper;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryQuoteMapper;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryTicketMapper;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryTicketStatusLogMapper;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryWorkOrderService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 询价工单主账实现。
|
||||
* <p>
|
||||
* 副作用:改 inq_ticket.status、写 inq_ticket_status_log、改 inq_exception_case。
|
||||
* 线程:单请求事务;只锁当前工单行(DB 行级),不串行其它工单。
|
||||
* 为何:Graph/前端成功不能冒充主账六态成功,所有 transition 必须过本服务。
|
||||
*/
|
||||
@Service
|
||||
public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper, InquiryTicket>
|
||||
implements IInquiryWorkOrderService {
|
||||
|
||||
@Autowired
|
||||
private InquiryQuoteMapper quoteMapper;
|
||||
@Autowired
|
||||
private InquiryAttachmentMapper attachmentMapper;
|
||||
@Autowired
|
||||
private InquiryTicketStatusLogMapper statusLogMapper;
|
||||
@Autowired
|
||||
private InquiryExceptionCaseMapper exceptionCaseMapper;
|
||||
|
||||
@Override
|
||||
public IPage<InquiryTicket> pageView(Page<InquiryTicket> page, Map<String, String> filters) {
|
||||
LambdaQueryWrapper<InquiryTicket> qw = new LambdaQueryWrapper<>();
|
||||
String keyword = filters.get("keyword");
|
||||
if (StringUtils.isNotBlank(keyword)) {
|
||||
qw.and(w -> w.like(InquiryTicket::getWorkOrderNo, keyword)
|
||||
.or().like(InquiryTicket::getSalesName, keyword)
|
||||
.or().like(InquiryTicket::getDemandTitle, keyword)
|
||||
.or().like(InquiryTicket::getOrigin, keyword)
|
||||
.or().like(InquiryTicket::getDestination, keyword));
|
||||
}
|
||||
if (StringUtils.isNotBlank(filters.get("status"))) {
|
||||
qw.eq(InquiryTicket::getStatus, filters.get("status"));
|
||||
}
|
||||
if (StringUtils.isNotBlank(filters.get("bizType"))) {
|
||||
qw.eq(InquiryTicket::getBizTypeLabel, filters.get("bizType"));
|
||||
}
|
||||
if (StringUtils.isNotBlank(filters.get("businessLine"))) {
|
||||
qw.eq(InquiryTicket::getBusinessLine, filters.get("businessLine"));
|
||||
}
|
||||
if (StringUtils.isNotBlank(filters.get("eventException"))) {
|
||||
if ("-".equals(filters.get("eventException"))) {
|
||||
qw.and(w -> w.isNull(InquiryTicket::getEventException).or().eq(InquiryTicket::getEventException, "").or().eq(InquiryTicket::getEventException, "-"));
|
||||
} else {
|
||||
qw.eq(InquiryTicket::getEventException, filters.get("eventException"));
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(filters.get("systemException"))) {
|
||||
if ("-".equals(filters.get("systemException"))) {
|
||||
qw.and(w -> w.isNull(InquiryTicket::getSystemException).or().eq(InquiryTicket::getSystemException, "").or().eq(InquiryTicket::getSystemException, "-"));
|
||||
} else {
|
||||
qw.eq(InquiryTicket::getSystemException, filters.get("systemException"));
|
||||
}
|
||||
}
|
||||
String dateFrom = filters.get("dateFrom");
|
||||
String dateTo = filters.get("dateTo");
|
||||
if (StringUtils.isNotBlank(dateFrom)) {
|
||||
qw.ge(InquiryTicket::getCreateTime, dateFrom + " 00:00:00");
|
||||
}
|
||||
if (StringUtils.isNotBlank(dateTo)) {
|
||||
qw.le(InquiryTicket::getCreateTime, dateTo + " 23:59:59");
|
||||
}
|
||||
qw.orderByDesc(InquiryTicket::getCreateTime);
|
||||
return page(page, qw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> detailView(String workOrderNo) {
|
||||
InquiryTicket ticket = findByWorkOrderNo(workOrderNo);
|
||||
if (ticket == null) {
|
||||
throw new IllegalArgumentException("工单不存在: " + workOrderNo);
|
||||
}
|
||||
List<InquiryQuote> quotes = quoteMapper.selectList(new LambdaQueryWrapper<InquiryQuote>()
|
||||
.eq(InquiryQuote::getWorkOrderNo, workOrderNo)
|
||||
.orderByAsc(InquiryQuote::getCreateTime));
|
||||
List<InquiryAttachment> attachments = attachmentMapper.selectList(new LambdaQueryWrapper<InquiryAttachment>()
|
||||
.eq(InquiryAttachment::getWorkOrderNo, workOrderNo)
|
||||
.orderByAsc(InquiryAttachment::getCreateTime));
|
||||
List<InquiryTicketStatusLog> timeline = statusLogMapper.selectList(new LambdaQueryWrapper<InquiryTicketStatusLog>()
|
||||
.eq(InquiryTicketStatusLog::getWorkOrderNo, workOrderNo)
|
||||
.orderByAsc(InquiryTicketStatusLog::getCreateTime));
|
||||
List<InquiryExceptionCase> exceptions = exceptionCaseMapper.selectList(new LambdaQueryWrapper<InquiryExceptionCase>()
|
||||
.eq(InquiryExceptionCase::getWorkOrderNo, workOrderNo)
|
||||
.orderByDesc(InquiryExceptionCase::getCreateTime));
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("ticket", ticket);
|
||||
result.put("quotes", quotes);
|
||||
result.put("attachments", attachments);
|
||||
result.put("timeline", timeline);
|
||||
result.put("exceptions", exceptions);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public InquiryTicket transition(TicketTransitionRequest req, String operator) {
|
||||
if (req == null || StringUtils.isBlank(req.getWorkOrderNo()) || StringUtils.isBlank(req.getToStatus())) {
|
||||
throw new IllegalArgumentException("工单号与目标状态不能为空");
|
||||
}
|
||||
InquiryTicket ticket = findByWorkOrderNo(req.getWorkOrderNo());
|
||||
if (ticket == null) {
|
||||
throw new IllegalArgumentException("工单不存在: " + req.getWorkOrderNo());
|
||||
}
|
||||
String from = ticket.getStatus();
|
||||
String to = req.getToStatus().trim();
|
||||
if (!TicketStatuses.canTransit(from, to)) {
|
||||
throw new IllegalStateException("不允许从「" + from + "」流转到「" + to + "」");
|
||||
}
|
||||
ticket.setStatus(to);
|
||||
if (TicketStatuses.CLOSED.equals(to) && StringUtils.isNotBlank(req.getRemark())) {
|
||||
ticket.setCloseReason(req.getRemark());
|
||||
}
|
||||
ticket.setVersion(ticket.getVersion() == null ? 2 : ticket.getVersion() + 1);
|
||||
ticket.setUpdateBy(operator);
|
||||
ticket.setUpdateTime(new Date());
|
||||
updateById(ticket);
|
||||
appendLog(ticket, from, to, "transition", operator, req.getRemark());
|
||||
return ticket;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void handleAbnormal(TicketAbnormalRequest req, String operator) {
|
||||
InquiryTicket ticket = requireTicket(req);
|
||||
List<InquiryExceptionCase> openCases = exceptionCaseMapper.selectList(new LambdaQueryWrapper<InquiryExceptionCase>()
|
||||
.eq(InquiryExceptionCase::getWorkOrderNo, ticket.getWorkOrderNo())
|
||||
.in(InquiryExceptionCase::getStatus, "OPEN", "RETRYING"));
|
||||
Date now = new Date();
|
||||
for (InquiryExceptionCase c : openCases) {
|
||||
c.setStatus("HANDLED");
|
||||
c.setHandledBy(operator);
|
||||
c.setHandledAt(now);
|
||||
c.setUpdateTime(now);
|
||||
exceptionCaseMapper.updateById(c);
|
||||
}
|
||||
ticket.setEventException(null);
|
||||
ticket.setSystemException(null);
|
||||
ticket.setSystemExceptionReason(null);
|
||||
ticket.setUpdateBy(operator);
|
||||
ticket.setUpdateTime(now);
|
||||
updateById(ticket);
|
||||
appendLog(ticket, ticket.getStatus(), ticket.getStatus(), "abnormal_handle", operator, req.getRemark());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void retryAbnormal(TicketAbnormalRequest req, String operator) {
|
||||
InquiryTicket ticket = requireTicket(req);
|
||||
List<InquiryExceptionCase> openCases = exceptionCaseMapper.selectList(new LambdaQueryWrapper<InquiryExceptionCase>()
|
||||
.eq(InquiryExceptionCase::getWorkOrderNo, ticket.getWorkOrderNo())
|
||||
.eq(InquiryExceptionCase::getCaseType, "SYSTEM")
|
||||
.in(InquiryExceptionCase::getStatus, "OPEN", "HANDLED", "RETRYING"));
|
||||
Date now = new Date();
|
||||
if (openCases.isEmpty()) {
|
||||
// 无异常单时仍允许记一次重试意图,便于空库联调
|
||||
InquiryExceptionCase created = new InquiryExceptionCase();
|
||||
created.setTicketId(ticket.getId());
|
||||
created.setWorkOrderNo(ticket.getWorkOrderNo());
|
||||
created.setCaseType("SYSTEM");
|
||||
created.setTitle(StringUtils.defaultIfBlank(ticket.getSystemException(), "系统任务重试"));
|
||||
created.setReason(ticket.getSystemExceptionReason());
|
||||
created.setStatus("RETRYING");
|
||||
created.setRetryCount(1);
|
||||
created.setLastRetryAt(now);
|
||||
created.setCreateTime(now);
|
||||
created.setUpdateTime(now);
|
||||
exceptionCaseMapper.insert(created);
|
||||
} else {
|
||||
for (InquiryExceptionCase c : openCases) {
|
||||
c.setStatus("RETRYING");
|
||||
c.setRetryCount(c.getRetryCount() == null ? 1 : c.getRetryCount() + 1);
|
||||
c.setLastRetryAt(now);
|
||||
c.setUpdateTime(now);
|
||||
exceptionCaseMapper.updateById(c);
|
||||
}
|
||||
}
|
||||
appendLog(ticket, ticket.getStatus(), ticket.getStatus(), "retry", operator, req.getRemark());
|
||||
}
|
||||
|
||||
private InquiryTicket requireTicket(TicketAbnormalRequest req) {
|
||||
if (req == null || StringUtils.isBlank(req.getWorkOrderNo())) {
|
||||
throw new IllegalArgumentException("工单号不能为空");
|
||||
}
|
||||
InquiryTicket ticket = findByWorkOrderNo(req.getWorkOrderNo());
|
||||
if (ticket == null) {
|
||||
throw new IllegalArgumentException("工单不存在: " + req.getWorkOrderNo());
|
||||
}
|
||||
return ticket;
|
||||
}
|
||||
|
||||
private InquiryTicket findByWorkOrderNo(String workOrderNo) {
|
||||
return getOne(new LambdaQueryWrapper<InquiryTicket>()
|
||||
.eq(InquiryTicket::getWorkOrderNo, workOrderNo)
|
||||
.last("LIMIT 1"), false);
|
||||
}
|
||||
|
||||
private void appendLog(InquiryTicket ticket, String from, String to, String action, String operator, String remark) {
|
||||
InquiryTicketStatusLog log = new InquiryTicketStatusLog();
|
||||
log.setTicketId(ticket.getId());
|
||||
log.setWorkOrderNo(ticket.getWorkOrderNo());
|
||||
log.setFromStatus(from);
|
||||
log.setToStatus(to);
|
||||
log.setAction(action);
|
||||
log.setOperator(operator);
|
||||
log.setRemark(remark);
|
||||
log.setCreateTime(new Date());
|
||||
statusLogMapper.insert(log);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user