Bläddra i källkod

Merge branch 'master' of http://47.100.37.243:10191/wutt/manHourHousekeeper

zhouyy 4 veckor sedan
förälder
incheckning
86fea54679
19 ändrade filer med 918 tillägg och 3 borttagningar
  1. 1 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/router/routerGuards.ts
  2. 70 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/WechatAccountController.java
  3. 272 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/WechatCallbackController.java
  4. 21 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/WechatQrcodeScanController.java
  5. 21 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/WechatUserFollowController.java
  6. 128 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/WechatQrcodeScan.java
  7. 84 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/WechatUserFollow.java
  8. 31 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/WechatQrcodeScanMapper.java
  9. 39 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/WechatUserFollowMapper.java
  10. 16 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/WechatQrcodeScanService.java
  11. 16 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/WechatUserFollowService.java
  12. 54 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/SalesmanStatsService.java
  13. 71 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/WechatApiService.java
  14. 20 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/WechatQrcodeScanServiceImpl.java
  15. 20 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/WechatUserFollowServiceImpl.java
  16. 2 2
      fhKeeper/formulahousekeeper/management-crm/src/main/resources/application.yml
  17. 29 0
      fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/WechatQrcodeScanMapper.xml
  18. 22 0
      fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/WechatUserFollowMapper.xml
  19. 1 0
      fhKeeper/formulahousekeeper/timesheet/src/views/team/index.vue

+ 1 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/router/routerGuards.ts

@@ -53,7 +53,7 @@ export function createRouterGuards(router: Router) {
                 name: item.name,
                 meta: {},
                 component: modules[`/src/pages/${filePath}/index.vue`],
-                redirect: item.path === '/biReport' ? `/biReport/cusReportForm` : '',
+                redirect: item.path === '/biReport' ? `/biReport/cusTotalAnalysis` : '',
                 children: childRoutes
               });
             }

+ 70 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/WechatAccountController.java

@@ -11,6 +11,7 @@ import com.management.platform.service.CompanyService;
 import com.management.platform.service.UserQrCodeService;
 import com.management.platform.service.UserService;
 import com.management.platform.service.WechatAccountService;
+import com.management.platform.service.impl.WechatApiService;
 import com.management.platform.util.FileZipUtil;
 import com.management.platform.util.HttpRespMsg;
 import lombok.extern.slf4j.Slf4j;
@@ -34,6 +35,8 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.net.InetAddress;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -69,6 +72,9 @@ public class WechatAccountController {
     @Resource
     private CompanyService companyService;
 
+    @Resource
+    private WechatApiService wechatApiService;
+
     @Value(value ="${upload.path}")
     private String uploadPath;
 
@@ -416,5 +422,69 @@ public class WechatAccountController {
         }else return;
     }
 
+
+    // 获取用户IP
+    @RequestMapping("/getIP")
+    public HttpRespMsg getIP(HttpServletRequest request)  {
+        HttpRespMsg msg = new HttpRespMsg();
+        String ip = getUserIp(request);
+        String cityByIp = getCityByIp(ip);
+        HashMap<String, Object> map = new HashMap<>();
+        map.put("city_by_ip", cityByIp);
+        map.put("ip", ip);
+        msg.data=map;
+        return msg;
+    }
+
+
+    private String getUserIp(HttpServletRequest request) {
+        // 1. 检查云服务商特殊头部
+        String ip = request.getHeader("X-Real-IP");
+        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("X-Forwarded-For");
+        }
+        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+
+        // 2. 处理多级代理
+        if (ip != null && ip.contains(",")) {
+            ip = Arrays.stream(ip.split(","))
+                    .map(String::trim)
+                    .filter(part -> !"unknown".equalsIgnoreCase(part))
+                    .findFirst()
+                    .orElse(null);
+        }
+
+        // 3. 最终回退
+        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+
+        // 4. 本地测试兼容
+        if ("0:0:0:0:0:0:0:1".equals(ip) || "127.0.0.1".equals(ip)) {
+            // 如果是本地测试,尝试从其他渠道获取(如网络接口)
+            try {
+                ip = InetAddress.getLocalHost().getHostAddress();
+            } catch (Exception e) {
+                ip = "127.0.0.1";
+            }
+        }
+
+        return ip;
+    }
+
+    private String getCityByIp(String ip) {
+        RestTemplate restTemplate = new RestTemplate();
+        String url = "http://ip-api.com/json/" + ip;
+        String response = restTemplate.getForObject(url, String.class);
+        // 解析 JSON 响应并提取城市信息
+        // 这里可以使用 Jackson 或 Gson 进行解析
+        return response; // 返回城市信息
+    }
+
 }
 

+ 272 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/WechatCallbackController.java

@@ -0,0 +1,272 @@
+package com.management.platform.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.management.platform.entity.Custom;
+import com.management.platform.entity.User;
+import com.management.platform.entity.WechatQrcodeScan;
+import com.management.platform.entity.WechatUserFollow;
+import com.management.platform.mapper.WechatQrcodeScanMapper;
+import com.management.platform.mapper.WechatUserFollowMapper;
+import com.management.platform.service.CustomService;
+import com.management.platform.service.UserService;
+import com.management.platform.service.impl.WechatApiService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.dom4j.Document;
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.net.InetAddress;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/wechat")
+@Slf4j
+public class WechatCallbackController {
+    private final String TOKEN = "FireRockCRM2025";
+    private final WechatQrcodeScanMapper scanMapper;
+    private final WechatUserFollowMapper followMapper;
+    private final WechatApiService wechatApiService;
+    
+    public WechatCallbackController(WechatQrcodeScanMapper scanMapper, 
+                                  WechatUserFollowMapper followMapper,
+                                  WechatApiService wechatApiService) {
+        this.scanMapper = scanMapper;
+        this.followMapper = followMapper;
+        this.wechatApiService = wechatApiService;
+    }
+
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private CustomService customService;
+
+    // 微信配置验证接口(GET请求)
+    @GetMapping("/callback")
+    public String validate(@RequestParam("signature") String signature,
+                           @RequestParam("timestamp") String timestamp,
+                           @RequestParam("nonce") String nonce,
+                           @RequestParam("echostr") String echostr) {
+        log.info("进入了微信配置验证接口");
+
+        // 1. 将Token、timestamp、nonce按字典序排序
+        String[] arr = new String[]{TOKEN, timestamp, nonce};
+        Arrays.sort(arr);
+
+        // 2. 拼接后SHA1加密
+        String joined = String.join("", arr);
+        String calculatedSignature = DigestUtils.sha1Hex(joined);
+
+        // 3. 验证签名
+        if (calculatedSignature.equals(signature)) {
+            log.info("Invalid signature==>GET请求验证签名成功");
+            return echostr; // 验证成功返回echostr
+        }
+        log.info("Invalid signature==>GET请求验证失败");
+        return "Invalid signature"; // 验证失败
+    }
+    
+    @PostMapping(produces = "application/xml;charset=UTF-8",value = "/callback")
+    public String handleMessage(@RequestBody String xmlData, HttpServletRequest request) {
+        log.info("进入微信接口回调处理逻辑");
+        try {
+            Document document = DocumentHelper.parseText(xmlData);
+            Element root = document.getRootElement();
+            
+            String msgType = root.elementText("MsgType");
+            if (!"event".equals(msgType)) {
+                return successResponse(root);
+            }
+            
+            String event = root.elementText("Event");
+            String openId = root.elementText("FromUserName");
+            String eventKey = root.elementText("EventKey");
+            log.info("场景值ID==>"+(StringUtils.isEmpty(eventKey)?"空":eventKey));
+            String ticket = root.elementText("Ticket");
+            log.info("ticket==>"+(StringUtils.isEmpty(ticket)?"空":ticket));
+            String createTime = root.elementText("CreateTime");
+            
+            // 处理关注/扫码事件
+            if ("subscribe".equals(event) || "SCAN".equals(event)) {
+                handleSubscribeOrScan(event, openId, eventKey, ticket, createTime, request);
+            } 
+            // 处理取消关注事件
+            else if ("unsubscribe".equals(event)) {
+                handleUnsubscribe(openId);
+            }
+
+            log.info("回调处理逻辑成功");
+            log.info("回调处理逻辑返回==>"+successResponse(root));
+            return successResponse(root);
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.info("回调处理逻辑失败==>"+e.getMessage());
+            return "";
+        }
+    }
+    
+    private void handleSubscribeOrScan(String event, String openId, String eventKey, 
+                                     String ticket, String createTime, 
+                                     HttpServletRequest request) {
+        // 提取销售人员ID
+        String salesmanId = eventKey.startsWith("qrscene_") ? 
+            eventKey.substring(8) : eventKey;
+        
+        // 获取用户IP
+        String ipAddress = getClientIp(request);
+        
+        // 获取用户信息
+        Map<String, Object> userInfo = wechatApiService.getUserInfo(openId,salesmanId);//用户的openId,员工的userId
+        
+        // 保存扫码记录
+        WechatQrcodeScan scanRecord = new WechatQrcodeScan();
+        scanRecord.setOpenId(openId);
+        scanRecord.setSalesmanId(salesmanId);
+        scanRecord.setEventType(event);
+        scanRecord.setTicket(ticket);
+        scanRecord.setScanTime(fromUnixTime(Long.parseLong(createTime)));
+        scanRecord.setIpAddress(ipAddress);
+        scanRecord.setIsNewFollower("subscribe".equals(event));
+        
+        if (userInfo != null) {
+            scanRecord.setNickname((String) userInfo.get("nickname"));
+            scanRecord.setGender((Integer) userInfo.get("sex"));
+            scanRecord.setCity((String) userInfo.get("city"));
+            scanRecord.setProvince((String) userInfo.get("province"));
+            scanRecord.setCountry((String) userInfo.get("country"));
+            scanRecord.setAvatarUrl((String) userInfo.get("headimgurl"));
+        }
+        
+        scanMapper.insert(scanRecord);
+        log.info("保存扫码记录成功");
+        
+        // 如果是新关注,更新用户关注表
+        if ("subscribe".equals(event)) {
+            WechatUserFollow followRecord = followMapper.findByOpenId(openId);
+            if (followRecord == null) {
+                followRecord = new WechatUserFollow();
+                followRecord.setOpenId(openId);
+                followRecord.setIsFollow(true);
+                followRecord.setFollowTime(LocalDateTime.now());
+                followRecord.setSalesmanId(salesmanId);
+                followMapper.insert(followRecord);
+
+                Custom serviceOne = customService.getOne(new QueryWrapper<Custom>().eq("plate4", openId));
+                if(serviceOne==null) {
+                    Custom custom = new Custom();
+                    custom.setCustomName(openId);//用户的openId
+                    custom.setIsDelete(0);
+                    custom.setInchargerId(salesmanId);
+                    if (StringUtils.isNotEmpty(salesmanId)) {
+                        User user = userService.getById(salesmanId);
+                        custom.setCompanyId(user != null ? user.getCompanyId() : null);
+                    }
+                    customService.save(custom);
+                }
+                log.info("用户新关注成功");
+            } else {
+                followRecord.setIsFollow(true);
+                followRecord.setFollowTime(LocalDateTime.now());
+                followRecord.setSalesmanId(salesmanId);
+                followMapper.update(followRecord);
+                log.info("更新用户关注成功");
+            }
+        }
+    }
+    
+    private void handleUnsubscribe(String openId) {
+        WechatUserFollow followRecord = followMapper.findByOpenId(openId);
+        if (followRecord != null) {
+            followRecord.setIsFollow(false);
+            followRecord.setUnfollowTime(LocalDateTime.now());
+            followMapper.update(followRecord);
+
+            customService.remove(new QueryWrapper<Custom>().eq("custom_name", openId));
+            log.info("用户取消关注成功");
+        }
+    }
+    
+    private String successResponse(Element root) {
+        String fromUser = root.elementText("FromUserName");
+        String toUser = root.elementText("ToUserName");
+        
+        return String.format(
+            "<xml>" +
+            "<ToUserName><![CDATA[%s]]></ToUserName>" +
+            "<FromUserName><![CDATA[%s]]></FromUserName>" +
+            "<CreateTime>%d</CreateTime>" +
+            "<MsgType><![CDATA[text]]></MsgType>" +
+            "<Content><![CDATA[success]]></Content>" +
+            "</xml>",
+            fromUser, toUser, System.currentTimeMillis() / 1000
+        );
+    }
+    
+    private String getClientIp(HttpServletRequest request) {
+        // 1. 检查云服务商特殊头部
+        String ip = request.getHeader("X-Real-IP");
+        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("X-Forwarded-For");
+        }
+        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+
+        // 2. 处理多级代理
+        if (ip != null && ip.contains(",")) {
+            ip = Arrays.stream(ip.split(","))
+                    .map(String::trim)
+                    .filter(part -> !"unknown".equalsIgnoreCase(part))
+                    .findFirst()
+                    .orElse(null);
+        }
+
+        // 3. 最终回退
+        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+
+        // 4. 本地测试兼容
+        if ("0:0:0:0:0:0:0:1".equals(ip) || "127.0.0.1".equals(ip)) {
+            // 如果是本地测试,尝试从其他渠道获取(如网络接口)
+            try {
+                ip = InetAddress.getLocalHost().getHostAddress();
+            } catch (Exception e) {
+                ip = "127.0.0.1";
+            }
+        }
+
+        return ip;
+    }
+
+    // Unix时间戳(秒) -> LocalDateTime
+    public static LocalDateTime fromUnixTime(long unixTime) {
+        return LocalDateTime.ofInstant(
+                Instant.ofEpochSecond(unixTime),
+                ZoneId.systemDefault()
+        );
+    }
+
+    //根据IP获取用户的省份,城市信息
+    private String getCityByIp(String ip) {
+        RestTemplate restTemplate = new RestTemplate();
+        String url = "http://ip-api.com/json/" + ip;
+        String response = restTemplate.getForObject(url, String.class);
+        // 解析 JSON 响应并提取城市信息
+        // 这里可以使用 Jackson 或 Gson 进行解析
+        return response; // 返回城市信息
+    }
+}

+ 21 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/WechatQrcodeScanController.java

@@ -0,0 +1,21 @@
+package com.management.platform.controller;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * 微信二维码扫码记录表 前端控制器
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-21
+ */
+@RestController
+@RequestMapping("/wechat-qrcode-scan")
+public class WechatQrcodeScanController {
+
+}
+

+ 21 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/WechatUserFollowController.java

@@ -0,0 +1,21 @@
+package com.management.platform.controller;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * 微信用户关注状态表 前端控制器
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-21
+ */
+@RestController
+@RequestMapping("/wechat-user-follow")
+public class WechatUserFollowController {
+
+}
+

+ 128 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/WechatQrcodeScan.java

@@ -0,0 +1,128 @@
+package com.management.platform.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 微信二维码扫码记录表
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+public class WechatQrcodeScan extends Model<WechatQrcodeScan> {
+
+    private static final long serialVersionUID=1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 用户OpenID
+     */
+    @TableField("open_id")
+    private String openId;
+
+    /**
+     * 销售人员ID(场景值)
+     */
+    @TableField("salesman_id")
+    private String salesmanId;
+
+    /**
+     * 事件类型(subscribe/SCAN/unsubscribe)
+     */
+    @TableField("event_type")
+    private String eventType;
+
+    /**
+     * 二维码ticket
+     */
+    @TableField("ticket")
+    private String ticket;
+
+    /**
+     * 扫码时间
+     */
+    @TableField("scan_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime scanTime;
+
+    /**
+     * 用户IP地址
+     */
+    @TableField("ip_address")
+    private String ipAddress;
+
+    /**
+     * 是否新关注用户
+     */
+    @TableField("is_new_follower")
+    private Boolean isNewFollower;
+
+    /**
+     * 用户昵称
+     */
+    @TableField("nickname")
+    private String nickname;
+
+    /**
+     * 性别:0未知,1男,2女
+     */
+    @TableField("gender")
+    private Integer gender;
+
+    /**
+     * 城市
+     */
+    @TableField("city")
+    private String city;
+
+    /**
+     * 省份
+     */
+    @TableField("province")
+    private String province;
+
+    /**
+     * 国家
+     */
+    @TableField("country")
+    private String country;
+
+    /**
+     * 头像URL
+     */
+    @TableField("avatar_url")
+    private String avatarUrl;
+
+    /**
+     * 创建时间
+     */
+    @TableField("create_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+
+    @Override
+    protected Serializable pkVal() {
+        return this.id;
+    }
+
+}

+ 84 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/WechatUserFollow.java

@@ -0,0 +1,84 @@
+package com.management.platform.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 微信用户关注状态表
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+public class WechatUserFollow extends Model<WechatUserFollow> {
+
+    private static final long serialVersionUID=1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 用户OpenID
+     */
+    @TableField("open_id")
+    private String openId;
+
+    /**
+     * 是否关注:0否,1是
+     */
+    @TableField("is_follow")
+    private Boolean isFollow;
+
+    /**
+     * 关注时间
+     */
+    @TableField("follow_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime followTime;
+
+    /**
+     * 取消关注时间
+     */
+    @TableField("unfollow_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime unfollowTime;
+
+    /**
+     * 首次关注的销售人员ID
+     */
+    @TableField("salesman_id")
+    private String salesmanId;
+
+    @TableField("create_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    @TableField("update_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+
+
+    @Override
+    protected Serializable pkVal() {
+        return this.id;
+    }
+
+}

+ 31 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/WechatQrcodeScanMapper.java

@@ -0,0 +1,31 @@
+package com.management.platform.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.management.platform.entity.WechatQrcodeScan;
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Options;
+import org.apache.ibatis.annotations.Select;
+
+/**
+ * <p>
+ * 微信二维码扫码记录表 Mapper 接口
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-21
+ */
+public interface WechatQrcodeScanMapper extends BaseMapper<WechatQrcodeScan> {
+    @Insert("INSERT INTO wechat_qrcode_scan(open_id, salesman_id, event_type, ticket, scan_time, ip_address, " +
+            "is_new_follower, nickname, gender, city, province, country, avatar_url, create_time) " +
+            "VALUES(#{openId}, #{salesmanId}, #{eventType}, #{ticket}, #{scanTime}, #{ipAddress}, " +
+            "#{isNewFollower}, #{nickname}, #{gender}, #{city}, #{province}, #{country}, #{avatarUrl}, NOW())")
+    @Options(useGeneratedKeys = true, keyProperty = "id")
+    int insert(WechatQrcodeScan record);
+
+    @Select("SELECT COUNT(*) FROM wechat_qrcode_scan WHERE salesman_id = #{salesmanId} AND is_new_follower = 1")
+    int countNewFollowersBySalesmanId(String salesmanId);
+
+    @Select("SELECT COUNT(*) FROM wechat_qrcode_scan WHERE salesman_id = #{salesmanId}")
+    int countBySalesmanId(String salesmanId);
+
+}

+ 39 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/WechatUserFollowMapper.java

@@ -0,0 +1,39 @@
+package com.management.platform.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.management.platform.entity.WechatUserFollow;
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Options;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+/**
+ * <p>
+ * 微信用户关注状态表 Mapper 接口
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-21
+ */
+public interface WechatUserFollowMapper extends BaseMapper<WechatUserFollow> {
+    @Select("SELECT * FROM wechat_user_follow WHERE open_id = #{openId}")
+    WechatUserFollow findByOpenId(String openId);
+
+    @Insert("INSERT INTO wechat_user_follow(open_id, is_follow, follow_time, salesman_id) " +
+            "VALUES(#{openId}, #{isFollow}, #{followTime}, #{salesmanId})")
+    @Options(useGeneratedKeys = true, keyProperty = "id")
+    int insert(WechatUserFollow record);
+
+    @Update("UPDATE wechat_user_follow SET is_follow = #{isFollow}, " +
+            "follow_time = #{followTime}, unfollow_time = #{unfollowTime}, " +
+            "salesman_id = #{salesmanId} WHERE open_id = #{openId}")
+    int update(WechatUserFollow record);
+
+    @Select("SELECT COUNT(*) FROM wechat_user_follow " +
+            "WHERE salesman_id = #{salesmanId} AND is_follow = 1")
+    int countActiveFollowersBySalesmanId(String salesmanId);
+
+    @Select("SELECT COUNT(*) FROM wechat_user_follow " +
+            "WHERE salesman_id = #{salesmanId} AND is_follow = 0")
+    int countUnfollowersBySalesmanId(String salesmanId);
+}

+ 16 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/WechatQrcodeScanService.java

@@ -0,0 +1,16 @@
+package com.management.platform.service;
+
+import com.management.platform.entity.WechatQrcodeScan;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 微信二维码扫码记录表 服务类
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-21
+ */
+public interface WechatQrcodeScanService extends IService<WechatQrcodeScan> {
+
+}

+ 16 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/WechatUserFollowService.java

@@ -0,0 +1,16 @@
+package com.management.platform.service;
+
+import com.management.platform.entity.WechatUserFollow;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 微信用户关注状态表 服务类
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-21
+ */
+public interface WechatUserFollowService extends IService<WechatUserFollow> {
+
+}

+ 54 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/SalesmanStatsService.java

@@ -0,0 +1,54 @@
+package com.management.platform.service.impl;
+
+import com.management.platform.mapper.WechatQrcodeScanMapper;
+import com.management.platform.mapper.WechatUserFollowMapper;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Service
+public class SalesmanStatsService {
+
+    private final WechatQrcodeScanMapper scanMapper;
+    private final WechatUserFollowMapper followMapper;
+
+    public SalesmanStatsService(WechatQrcodeScanMapper scanMapper,
+                                WechatUserFollowMapper followMapper) {
+        this.scanMapper = scanMapper;
+        this.followMapper = followMapper;
+    }
+
+    /**
+     * 获取销售人员客户统计详情
+     */
+    public Map<String, Object> getSalesmanStatsDetail(String salesmanId) {
+        Map<String, Object> stats = new HashMap<>();
+
+        // 1. 扫码总次数
+        int totalScans = scanMapper.countBySalesmanId(salesmanId);
+
+        // 2. 新关注客户数(通过扫码关注)
+        int newFollowers = scanMapper.countNewFollowersBySalesmanId(salesmanId);
+
+        // 3. 当前有效客户数(关注中)
+        int activeFollowers = followMapper.countActiveFollowersBySalesmanId(salesmanId);
+
+        // 4. 已流失客户数(取消关注)
+        int unfollowers = followMapper.countUnfollowersBySalesmanId(salesmanId);
+
+        // 5. 扫码但未关注数
+        int scanButNotFollow = totalScans - newFollowers;
+
+        stats.put("salesmanId", salesmanId);
+        stats.put("totalScans", totalScans);
+        stats.put("newFollowers", newFollowers);
+        stats.put("activeFollowers", activeFollowers);
+        stats.put("unfollowers", unfollowers);
+        stats.put("scanButNotFollow", scanButNotFollow);
+        stats.put("conversionRate", totalScans > 0 ?
+                String.format("%.2f%%", newFollowers * 100.0 / totalScans) : "0%");
+
+        return stats;
+    }
+}

+ 71 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/WechatApiService.java

@@ -0,0 +1,71 @@
+package com.management.platform.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.management.platform.entity.User;
+import com.management.platform.entity.WechatAccount;
+import com.management.platform.service.CompanyService;
+import com.management.platform.service.UserService;
+import com.management.platform.service.WechatAccountService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+@Service
+@Slf4j
+public class WechatApiService {
+    
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private CompanyService companyService;
+
+    @Resource
+    private WechatAccountService wechatAccountService;
+    
+    private final ObjectMapper objectMapper = new ObjectMapper();
+    
+    public Map<String, Object> getUserInfo(String openId,String userId) {
+        String accessToken = getAccessToken(userId);
+        if (accessToken == null) {
+            return null;
+        }
+        
+        String url = String.format(
+            "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN",
+            accessToken, openId);
+        
+        try (CloseableHttpClient client = HttpClients.createDefault()) {
+            HttpGet request = new HttpGet(url);
+            String response = EntityUtils.toString(client.execute(request).getEntity());
+            log.info("获取用户信息===>"+response);
+            return objectMapper.readValue(response, Map.class);
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.info("获取用户信息失败==>"+e.getMessage());
+            return null;
+        }
+    }
+    
+    private String getAccessToken(String userId) {
+        User user = userService.getById(userId);
+        if (user==null){
+            log.info("获取企业的微信Token时,未获取该用户");
+            return null;
+        }else {
+            WechatAccount wechatAccount = wechatAccountService.getOne(new QueryWrapper<WechatAccount>().eq("company_id", user.getCompanyId()));
+            if (wechatAccount==null){
+                log.info("该公司没有配置公众号相关的参数");
+                return null;
+            }
+            return wechatAccountService.getAccessToken(user.getCompanyId(), wechatAccount.getAppId());
+        }
+    }
+}

+ 20 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/WechatQrcodeScanServiceImpl.java

@@ -0,0 +1,20 @@
+package com.management.platform.service.impl;
+
+import com.management.platform.entity.WechatQrcodeScan;
+import com.management.platform.mapper.WechatQrcodeScanMapper;
+import com.management.platform.service.WechatQrcodeScanService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 微信二维码扫码记录表 服务实现类
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-21
+ */
+@Service
+public class WechatQrcodeScanServiceImpl extends ServiceImpl<WechatQrcodeScanMapper, WechatQrcodeScan> implements WechatQrcodeScanService {
+
+}

+ 20 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/WechatUserFollowServiceImpl.java

@@ -0,0 +1,20 @@
+package com.management.platform.service.impl;
+
+import com.management.platform.entity.WechatUserFollow;
+import com.management.platform.mapper.WechatUserFollowMapper;
+import com.management.platform.service.WechatUserFollowService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 微信用户关注状态表 服务实现类
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-21
+ */
+@Service
+public class WechatUserFollowServiceImpl extends ServiceImpl<WechatUserFollowMapper, WechatUserFollow> implements WechatUserFollowService {
+
+}

+ 2 - 2
fhKeeper/formulahousekeeper/management-crm/src/main/resources/application.yml

@@ -128,8 +128,8 @@ referer:
     - crm.ttkuaiban.com
     - mobcrm.ttkuaiban.com
     - 47.100.37.243
-    - 47.101.180.183
-excludeUrls: /wxcorp/*,/wxcorp/*/*,/dingding/*,/feishu-info/*,/error,/testClient,/corpWXAuth,/corpWXScanningAuth,/corpInsideWXAuth,/wx-corp-info/*,/clean/*,/innerRoles/*,/project/getProjectListByToken,/project/getTimeCostByToken,/report/getReportListByToken,/report/getProcessErrorData,/project/synchronizationProject,/user/updateUserDeptHierarchy,/report/getUserTimeCostByThird,/report/getProjectTimeCostByThird
+    - 1.94.62.58
+excludeUrls: /wxcorp/*,/wxcorp/*/*,/dingding/*,/feishu-info/*,/error,/testClient,/corpWXAuth,/corpWXScanningAuth,/corpInsideWXAuth,/wx-corp-info/*,/clean/*,/innerRoles/*,/wechat/*
 
 #企业微信相关参数
 suitId: wwdd1137a65ce0fc87

+ 29 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/WechatQrcodeScanMapper.xml

@@ -0,0 +1,29 @@
+<?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.management.platform.mapper.WechatQrcodeScanMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.management.platform.entity.WechatQrcodeScan">
+        <id column="id" property="id" />
+        <result column="open_id" property="openId" />
+        <result column="salesman_id" property="salesmanId" />
+        <result column="event_type" property="eventType" />
+        <result column="ticket" property="ticket" />
+        <result column="scan_time" property="scanTime" />
+        <result column="ip_address" property="ipAddress" />
+        <result column="is_new_follower" property="isNewFollower" />
+        <result column="nickname" property="nickname" />
+        <result column="gender" property="gender" />
+        <result column="city" property="city" />
+        <result column="province" property="province" />
+        <result column="country" property="country" />
+        <result column="avatar_url" property="avatarUrl" />
+        <result column="create_time" property="createTime" />
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id, open_id, salesman_id, event_type, ticket, scan_time, ip_address, is_new_follower, nickname, gender, city, province, country, avatar_url, create_time
+    </sql>
+
+</mapper>

+ 22 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/WechatUserFollowMapper.xml

@@ -0,0 +1,22 @@
+<?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.management.platform.mapper.WechatUserFollowMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.management.platform.entity.WechatUserFollow">
+        <id column="id" property="id" />
+        <result column="open_id" property="openId" />
+        <result column="is_follow" property="isFollow" />
+        <result column="follow_time" property="followTime" />
+        <result column="unfollow_time" property="unfollowTime" />
+        <result column="salesman_id" property="salesmanId" />
+        <result column="create_time" property="createTime" />
+        <result column="update_time" property="updateTime" />
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id, open_id, is_follow, follow_time, unfollow_time, salesman_id, create_time, update_time
+    </sql>
+
+</mapper>

+ 1 - 0
fhKeeper/formulahousekeeper/timesheet/src/views/team/index.vue

@@ -3443,6 +3443,7 @@ export default {
                   this.depData.label = form.name;
                   this.depData.managerId = form.managerId;
                   this.depData.otherManagerIds = form.otherManagerIds;
+                  this.depData.deptCode = form.deptCode;
                 }
 
                 this.getDepartment();