feat(contact): 添加联系表单销售线索功能支持
- 新增联系表单数据库表结构设计与实体类定义 - 实现联系表单提交接口,支持公开访问无需登录验证 - 添加后台管理接口,支持线索查询、状态更新和删除操作 - 集成企业微信和飞书机器人通知功能,实时推送新线索 - 在安全配置中开放联系表单提交接口访问权限 - 添加应用配置中的通知机器人Webhook配置项
This commit is contained in:
@@ -0,0 +1,142 @@
|
|||||||
|
package com.gxwebsoft.cms.controller;
|
||||||
|
|
||||||
|
import com.gxwebsoft.cms.entity.CmsContactLead;
|
||||||
|
import com.gxwebsoft.cms.param.CmsContactLeadParam;
|
||||||
|
import com.gxwebsoft.cms.service.CmsContactLeadService;
|
||||||
|
import com.gxwebsoft.common.core.web.ApiResult;
|
||||||
|
import com.gxwebsoft.common.core.web.BaseController;
|
||||||
|
import com.gxwebsoft.common.core.web.BatchParam;
|
||||||
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系表单/销售线索控制器
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-03-30
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Validated
|
||||||
|
@Tag(name = "联系表单/销售线索管理")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/cms/cms-contact-lead")
|
||||||
|
public class CmsContactLeadController extends BaseController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CmsContactLeadService cmsContactLeadService;
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// 公开接口(无需登录)
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交联系表单(官网 /contact 页调用,不需要登录)
|
||||||
|
*/
|
||||||
|
@Operation(summary = "提交联系表单")
|
||||||
|
@PostMapping("/submit")
|
||||||
|
public ApiResult<?> submit(@RequestBody @Valid CmsContactLead lead, HttpServletRequest request) {
|
||||||
|
// 获取客户端真实IP
|
||||||
|
String ip = getClientIp(request);
|
||||||
|
if (cmsContactLeadService.submit(lead, ip)) {
|
||||||
|
return success("提交成功,我们将尽快与您联系!");
|
||||||
|
}
|
||||||
|
return fail("提交失败,请稍后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// 后台管理接口(需要权限)
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
@Operation(summary = "分页查询线索列表")
|
||||||
|
@PreAuthorize("hasAuthority('cms:contactLead:list')")
|
||||||
|
@GetMapping("/page")
|
||||||
|
public ApiResult<PageResult<CmsContactLead>> page(CmsContactLeadParam param) {
|
||||||
|
return success(cmsContactLeadService.pageRel(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "查询全部线索")
|
||||||
|
@PreAuthorize("hasAuthority('cms:contactLead:list')")
|
||||||
|
@GetMapping()
|
||||||
|
public ApiResult<List<CmsContactLead>> list(CmsContactLeadParam param) {
|
||||||
|
return success(cmsContactLeadService.listRel(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "根据ID查询线索")
|
||||||
|
@PreAuthorize("hasAuthority('cms:contactLead:list')")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResult<CmsContactLead> get(@PathVariable("id") Integer id) {
|
||||||
|
return success(cmsContactLeadService.getById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "更新跟进状态/备注")
|
||||||
|
@PreAuthorize("hasAuthority('cms:contactLead:update')")
|
||||||
|
@PutMapping()
|
||||||
|
public ApiResult<?> update(@RequestBody CmsContactLead lead) {
|
||||||
|
if (cmsContactLeadService.updateById(lead)) {
|
||||||
|
return success("更新成功");
|
||||||
|
}
|
||||||
|
return fail("更新失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "删除线索")
|
||||||
|
@PreAuthorize("hasAuthority('cms:contactLead:remove')")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResult<?> remove(@PathVariable("id") Integer id) {
|
||||||
|
if (cmsContactLeadService.removeById(id)) {
|
||||||
|
return success("删除成功");
|
||||||
|
}
|
||||||
|
return fail("删除失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "批量修改")
|
||||||
|
@PreAuthorize("hasAuthority('cms:contactLead:update')")
|
||||||
|
@PutMapping("/batch")
|
||||||
|
public ApiResult<?> updateBatch(@RequestBody BatchParam<CmsContactLead> batchParam) {
|
||||||
|
if (batchParam.update(cmsContactLeadService, "lead_id")) {
|
||||||
|
return success("批量修改成功");
|
||||||
|
}
|
||||||
|
return fail("批量修改失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "批量删除")
|
||||||
|
@PreAuthorize("hasAuthority('cms:contactLead:remove')")
|
||||||
|
@DeleteMapping("/batch")
|
||||||
|
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
|
||||||
|
if (cmsContactLeadService.removeByIds(ids)) {
|
||||||
|
return success("删除成功");
|
||||||
|
}
|
||||||
|
return fail("删除失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// 工具方法
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户端真实IP(兼容反向代理)
|
||||||
|
*/
|
||||||
|
private String getClientIp(HttpServletRequest request) {
|
||||||
|
String ip = request.getHeader("X-Forwarded-For");
|
||||||
|
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getHeader("X-Real-IP");
|
||||||
|
}
|
||||||
|
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
// X-Forwarded-For 多个IP取第一个
|
||||||
|
if (ip != null && ip.contains(",")) {
|
||||||
|
ip = ip.split(",")[0].trim();
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/main/java/com/gxwebsoft/cms/entity/CmsContactLead.java
Normal file
83
src/main/java/com/gxwebsoft/cms/entity/CmsContactLead.java
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package com.gxwebsoft.cms.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系表单/销售线索
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-03-30
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Schema(name = "CmsContactLead对象", description = "联系表单/销售线索")
|
||||||
|
public class CmsContactLead implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "线索ID")
|
||||||
|
@TableId(value = "lead_id", type = IdType.AUTO)
|
||||||
|
private Integer leadId;
|
||||||
|
|
||||||
|
@Schema(description = "联系人姓名")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "手机号")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Schema(description = "单位/公司名称")
|
||||||
|
private String company;
|
||||||
|
|
||||||
|
@Schema(description = "交付方式: saas/private/hybrid")
|
||||||
|
private String delivery;
|
||||||
|
|
||||||
|
@Schema(description = "需求描述")
|
||||||
|
private String need;
|
||||||
|
|
||||||
|
@Schema(description = "来源渠道: web/miniapp/app")
|
||||||
|
private String source;
|
||||||
|
|
||||||
|
@Schema(description = "提交IP")
|
||||||
|
private String ip;
|
||||||
|
|
||||||
|
@Schema(description = "跟进状态: 0待跟进 1跟进中 2已成交 3无效")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@Schema(description = "销售备注")
|
||||||
|
private String remarks;
|
||||||
|
|
||||||
|
@Schema(description = "是否删除, 0否, 1是")
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
|
private Integer tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
public String getStatusText() {
|
||||||
|
if (this.status == null) return "";
|
||||||
|
return switch (this.status) {
|
||||||
|
case 0 -> "待跟进";
|
||||||
|
case 1 -> "跟进中";
|
||||||
|
case 2 -> "已成交";
|
||||||
|
case 3 -> "无效";
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.gxwebsoft.cms.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.gxwebsoft.cms.entity.CmsContactLead;
|
||||||
|
import com.gxwebsoft.cms.param.CmsContactLeadParam;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系表单/销售线索Mapper
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-03-30
|
||||||
|
*/
|
||||||
|
public interface CmsContactLeadMapper extends BaseMapper<CmsContactLead> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询
|
||||||
|
*
|
||||||
|
* @param page 分页对象
|
||||||
|
* @param param 查询参数
|
||||||
|
* @return List<CmsContactLead>
|
||||||
|
*/
|
||||||
|
List<CmsContactLead> selectPageRel(@Param("page") IPage<CmsContactLead> page,
|
||||||
|
@Param("param") CmsContactLeadParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询全部
|
||||||
|
*
|
||||||
|
* @param param 查询参数
|
||||||
|
* @return List<CmsContactLead>
|
||||||
|
*/
|
||||||
|
List<CmsContactLead> selectListRel(@Param("param") CmsContactLeadParam param);
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.gxwebsoft.cms.mapper.CmsContactLeadMapper">
|
||||||
|
|
||||||
|
<!-- 关联查询sql -->
|
||||||
|
<sql id="selectSql">
|
||||||
|
SELECT a.*
|
||||||
|
FROM cms_contact_lead a
|
||||||
|
<where>
|
||||||
|
<if test="param.leadId != null">
|
||||||
|
AND a.lead_id = #{param.leadId}
|
||||||
|
</if>
|
||||||
|
<if test="param.name != null">
|
||||||
|
AND a.name LIKE CONCAT('%', #{param.name}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.phone != null">
|
||||||
|
AND a.phone LIKE CONCAT('%', #{param.phone}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.company != null">
|
||||||
|
AND a.company LIKE CONCAT('%', #{param.company}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.delivery != null">
|
||||||
|
AND a.delivery = #{param.delivery}
|
||||||
|
</if>
|
||||||
|
<if test="param.source != null">
|
||||||
|
AND a.source = #{param.source}
|
||||||
|
</if>
|
||||||
|
<if test="param.status != null">
|
||||||
|
AND a.status = #{param.status}
|
||||||
|
</if>
|
||||||
|
<if test="param.keywords != null">
|
||||||
|
AND (
|
||||||
|
a.name LIKE CONCAT('%', #{param.keywords}, '%')
|
||||||
|
OR a.phone LIKE CONCAT('%', #{param.keywords}, '%')
|
||||||
|
OR a.company LIKE CONCAT('%', #{param.keywords}, '%')
|
||||||
|
OR a.need LIKE CONCAT('%', #{param.keywords}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="param.deleted != null">
|
||||||
|
AND a.deleted = #{param.deleted}
|
||||||
|
</if>
|
||||||
|
<if test="param.deleted == null">
|
||||||
|
AND a.deleted = 0
|
||||||
|
</if>
|
||||||
|
<if test="param.createTimeStart != null">
|
||||||
|
AND a.create_time >= #{param.createTimeStart}
|
||||||
|
</if>
|
||||||
|
<if test="param.createTimeEnd != null">
|
||||||
|
AND a.create_time <= #{param.createTimeEnd}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<!-- 分页查询 -->
|
||||||
|
<select id="selectPageRel" resultType="com.gxwebsoft.cms.entity.CmsContactLead">
|
||||||
|
<include refid="selectSql"/>
|
||||||
|
ORDER BY a.create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询全部 -->
|
||||||
|
<select id="selectListRel" resultType="com.gxwebsoft.cms.entity.CmsContactLead">
|
||||||
|
<include refid="selectSql"/>
|
||||||
|
ORDER BY a.create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.gxwebsoft.cms.param;
|
||||||
|
|
||||||
|
import com.gxwebsoft.common.core.annotation.QueryField;
|
||||||
|
import com.gxwebsoft.common.core.annotation.QueryType;
|
||||||
|
import com.gxwebsoft.common.core.web.BaseParam;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系表单/销售线索查询参数
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-03-30
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
@Schema(name = "CmsContactLeadParam对象", description = "联系表单/销售线索查询参数")
|
||||||
|
public class CmsContactLeadParam extends BaseParam {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "线索ID")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer leadId;
|
||||||
|
|
||||||
|
@Schema(description = "联系人姓名")
|
||||||
|
@QueryField(type = QueryType.LIKE)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "手机号")
|
||||||
|
@QueryField(type = QueryType.LIKE)
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Schema(description = "单位/公司名称")
|
||||||
|
@QueryField(type = QueryType.LIKE)
|
||||||
|
private String company;
|
||||||
|
|
||||||
|
@Schema(description = "交付方式: saas/private/hybrid")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private String delivery;
|
||||||
|
|
||||||
|
@Schema(description = "来源渠道")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private String source;
|
||||||
|
|
||||||
|
@Schema(description = "跟进状态: 0待跟进 1跟进中 2已成交 3无效")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@Schema(description = "是否删除, 0否, 1是")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.gxwebsoft.cms.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.gxwebsoft.cms.entity.CmsContactLead;
|
||||||
|
import com.gxwebsoft.cms.param.CmsContactLeadParam;
|
||||||
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系表单/销售线索Service
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-03-30
|
||||||
|
*/
|
||||||
|
public interface CmsContactLeadService extends IService<CmsContactLead> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页关联查询
|
||||||
|
*
|
||||||
|
* @param param 查询参数
|
||||||
|
* @return PageResult<CmsContactLead>
|
||||||
|
*/
|
||||||
|
PageResult<CmsContactLead> pageRel(CmsContactLeadParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联查询全部
|
||||||
|
*
|
||||||
|
* @param param 查询参数
|
||||||
|
* @return List<CmsContactLead>
|
||||||
|
*/
|
||||||
|
List<CmsContactLead> listRel(CmsContactLeadParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交联系表单(不需要登录)
|
||||||
|
*
|
||||||
|
* @param lead 表单数据
|
||||||
|
* @param ip 客户端IP
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
boolean submit(CmsContactLead lead, String ip);
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package com.gxwebsoft.cms.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.http.HttpUtil;
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.gxwebsoft.cms.entity.CmsContactLead;
|
||||||
|
import com.gxwebsoft.cms.mapper.CmsContactLeadMapper;
|
||||||
|
import com.gxwebsoft.cms.param.CmsContactLeadParam;
|
||||||
|
import com.gxwebsoft.cms.service.CmsContactLeadService;
|
||||||
|
import com.gxwebsoft.common.core.web.PageParam;
|
||||||
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系表单/销售线索Service实现
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-03-30
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class CmsContactLeadServiceImpl extends ServiceImpl<CmsContactLeadMapper, CmsContactLead>
|
||||||
|
implements CmsContactLeadService {
|
||||||
|
|
||||||
|
/** 企业微信群机器人 Webhook,留空则不发送 */
|
||||||
|
@Value("${notify.wecom-webhook:}")
|
||||||
|
private String wecomWebhook;
|
||||||
|
|
||||||
|
/** 飞书群机器人 Webhook,留空则不发送 */
|
||||||
|
@Value("${notify.feishu-webhook:}")
|
||||||
|
private String feishuWebhook;
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// 分页 / 列表
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<CmsContactLead> pageRel(CmsContactLeadParam param) {
|
||||||
|
PageParam<CmsContactLead, CmsContactLeadParam> page = new PageParam<>(param);
|
||||||
|
page.setDefaultOrder("create_time desc");
|
||||||
|
List<CmsContactLead> list = baseMapper.selectPageRel(page, param);
|
||||||
|
return new PageResult<>(list, page.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CmsContactLead> listRel(CmsContactLeadParam param) {
|
||||||
|
return baseMapper.selectListRel(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// 公开提交接口
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean submit(CmsContactLead lead, String ip) {
|
||||||
|
// 补充默认值
|
||||||
|
if (StrUtil.isBlank(lead.getSource())) {
|
||||||
|
lead.setSource("web");
|
||||||
|
}
|
||||||
|
lead.setIp(ip);
|
||||||
|
lead.setStatus(0); // 默认待跟进
|
||||||
|
|
||||||
|
boolean result = save(lead);
|
||||||
|
if (result) {
|
||||||
|
log.info("[联系表单] 新线索提交成功,name={}, phone={}", lead.getName(), lead.getPhone());
|
||||||
|
// 异步推送通知,不阻塞接口响应
|
||||||
|
sendNotifyAsync(lead);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// 异步通知(企业微信 + 飞书)
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void sendNotifyAsync(CmsContactLead lead) {
|
||||||
|
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("MM-dd HH:mm"));
|
||||||
|
String deliveryLabel = deliveryLabel(lead.getDelivery());
|
||||||
|
|
||||||
|
if (StrUtil.isNotBlank(wecomWebhook)) {
|
||||||
|
try {
|
||||||
|
sendWecom(lead, deliveryLabel, now);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[联系表单] 企业微信通知发送失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StrUtil.isNotBlank(feishuWebhook)) {
|
||||||
|
try {
|
||||||
|
sendFeishu(lead, deliveryLabel, now);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[联系表单] 飞书通知发送失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企业微信群机器人 - markdown 消息
|
||||||
|
* 文档:https://developer.work.weixin.qq.com/document/path/91770
|
||||||
|
*/
|
||||||
|
private void sendWecom(CmsContactLead lead, String deliveryLabel, String now) {
|
||||||
|
String content = String.format(
|
||||||
|
"## 📋 新销售线索(%s)\n" +
|
||||||
|
"> **姓名:** %s\n" +
|
||||||
|
"> **手机:** <font color=\"warning\">%s</font>\n" +
|
||||||
|
"> **公司:** %s\n" +
|
||||||
|
"> **交付:** %s\n" +
|
||||||
|
"> **需求:** %s\n" +
|
||||||
|
"> **来源:** %s",
|
||||||
|
now,
|
||||||
|
nullSafe(lead.getName()),
|
||||||
|
nullSafe(lead.getPhone()),
|
||||||
|
nullSafe(lead.getCompany()),
|
||||||
|
deliveryLabel,
|
||||||
|
truncate(lead.getNeed(), 100),
|
||||||
|
nullSafe(lead.getSource())
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object> textMap = new HashMap<>();
|
||||||
|
textMap.put("content", content);
|
||||||
|
|
||||||
|
Map<String, Object> payload = new HashMap<>();
|
||||||
|
payload.put("msgtype", "markdown");
|
||||||
|
payload.put("markdown", textMap);
|
||||||
|
|
||||||
|
String body = JSON.toJSONString(payload);
|
||||||
|
String resp = HttpUtil.post(wecomWebhook, body);
|
||||||
|
log.info("[联系表单] 企业微信通知结果: {}", resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞书群机器人 - 富文本(post)消息
|
||||||
|
* 文档:https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
|
||||||
|
*/
|
||||||
|
private void sendFeishu(CmsContactLead lead, String deliveryLabel, String now) {
|
||||||
|
// 标题行
|
||||||
|
String title = "📋 新销售线索(" + now + ")";
|
||||||
|
|
||||||
|
// 正文每行:[{"tag":"text","text":"xxx"}]
|
||||||
|
List<List<Map<String, String>>> content = List.of(
|
||||||
|
line("姓 名:" + nullSafe(lead.getName())),
|
||||||
|
line("手 机:" + nullSafe(lead.getPhone())),
|
||||||
|
line("公 司:" + nullSafe(lead.getCompany())),
|
||||||
|
line("交 付:" + deliveryLabel),
|
||||||
|
line("需 求:" + truncate(lead.getNeed(), 100)),
|
||||||
|
line("来 源:" + nullSafe(lead.getSource()))
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object> zhCn = new HashMap<>();
|
||||||
|
zhCn.put("title", title);
|
||||||
|
zhCn.put("content", content);
|
||||||
|
|
||||||
|
Map<String, Object> postContent = new HashMap<>();
|
||||||
|
postContent.put("zh_cn", zhCn);
|
||||||
|
|
||||||
|
Map<String, Object> post = new HashMap<>();
|
||||||
|
post.put("content", postContent);
|
||||||
|
|
||||||
|
Map<String, Object> payload = new HashMap<>();
|
||||||
|
payload.put("msg_type", "post");
|
||||||
|
payload.put("content", post);
|
||||||
|
|
||||||
|
String body = JSON.toJSONString(payload);
|
||||||
|
String resp = HttpUtil.post(feishuWebhook, body);
|
||||||
|
log.info("[联系表单] 飞书通知结果: {}", resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// 工具方法
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
private String deliveryLabel(String delivery) {
|
||||||
|
if (delivery == null) return "未选择";
|
||||||
|
return switch (delivery) {
|
||||||
|
case "saas" -> "SaaS(云端)";
|
||||||
|
case "private" -> "私有化部署";
|
||||||
|
case "hybrid" -> "混合部署";
|
||||||
|
default -> delivery;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nullSafe(String s) {
|
||||||
|
return StrUtil.isBlank(s) ? "—" : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncate(String s, int max) {
|
||||||
|
if (StrUtil.isBlank(s)) return "—";
|
||||||
|
return s.length() > max ? s.substring(0, max) + "…" : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 飞书 post 消息单行 */
|
||||||
|
private List<Map<String, String>> line(String text) {
|
||||||
|
Map<String, String> node = new HashMap<>();
|
||||||
|
node.put("tag", "text");
|
||||||
|
node.put("text", text);
|
||||||
|
return List.of(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/com/gxwebsoft/cms/sql/cms_contact_lead.sql
Normal file
25
src/main/java/com/gxwebsoft/cms/sql/cms_contact_lead.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-- ----------------------------
|
||||||
|
-- 联系表单/销售线索表
|
||||||
|
-- 存放官网 /contact 页面提交的咨询需求
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `cms_contact_lead` (
|
||||||
|
`lead_id` INT NOT NULL AUTO_INCREMENT COMMENT '线索ID',
|
||||||
|
`name` VARCHAR(50) NOT NULL COMMENT '联系人姓名',
|
||||||
|
`phone` VARCHAR(20) NOT NULL COMMENT '手机号',
|
||||||
|
`company` VARCHAR(100) DEFAULT NULL COMMENT '单位/公司名称',
|
||||||
|
`delivery` VARCHAR(20) DEFAULT NULL COMMENT '交付方式: saas/private/hybrid',
|
||||||
|
`need` TEXT DEFAULT NULL COMMENT '需求描述',
|
||||||
|
`source` VARCHAR(50) DEFAULT 'web' COMMENT '来源渠道: web/miniapp/app',
|
||||||
|
`ip` VARCHAR(50) DEFAULT NULL COMMENT '提交IP',
|
||||||
|
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '跟进状态: 0待跟进 1跟进中 2已成交 3无效',
|
||||||
|
`remarks` VARCHAR(500) DEFAULT NULL COMMENT '销售备注',
|
||||||
|
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0否 1是',
|
||||||
|
`tenant_id` INT DEFAULT NULL COMMENT '租户ID',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`lead_id`),
|
||||||
|
KEY `idx_tenant_id` (`tenant_id`),
|
||||||
|
KEY `idx_phone` (`phone`),
|
||||||
|
KEY `idx_status` (`status`),
|
||||||
|
KEY `idx_create_time` (`create_time`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='联系表单/销售线索';
|
||||||
@@ -80,7 +80,8 @@ public class SecurityConfig {
|
|||||||
"/api/shop/getShopInfo",
|
"/api/shop/getShopInfo",
|
||||||
"/api/shop/shop-order/test",
|
"/api/shop/shop-order/test",
|
||||||
"/api/qr-code/**",
|
"/api/qr-code/**",
|
||||||
"/api/shop/order-delivery/notify"
|
"/api/shop/order-delivery/notify",
|
||||||
|
"/api/cms/cms-contact-lead/submit"
|
||||||
)
|
)
|
||||||
.permitAll()
|
.permitAll()
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
|
|||||||
@@ -258,9 +258,12 @@ payment:
|
|||||||
# 开发环境是否启用环境感知
|
# 开发环境是否启用环境感知
|
||||||
environment-aware: true
|
environment-aware: true
|
||||||
|
|
||||||
# 生产环境配置
|
|
||||||
prod:
|
# 通知配置(企业微信/飞书机器人 Webhook)
|
||||||
# 生产环境回调地址
|
notify:
|
||||||
notify-url: "https://cms-api.websoft.top/api/shop/shop-order/notify"
|
# 企业微信群机器人 Webhook,格式:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY
|
||||||
# 生产环境是否启用环境感知
|
# 不启用时留空或删除此行
|
||||||
environment-aware: false
|
wecom-webhook: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=aa0d2f30-b785-44a2-ad19-05834569b7c5"
|
||||||
|
# 飞书群机器人 Webhook,格式:https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_TOKEN
|
||||||
|
feishu-webhook: ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user