初始化项目

This commit is contained in:
andy committed 2026-01-26 16:01:15 +08:00
1 parent 45a1702948
commit 934b8aad40
155 files changed
+2649 -68

No files matched your search

@@ -23,70 +23,70 @@ public class Constants {
/**
* 客户状态 - 可报备
*/
public static final Integer CUSTOMER_STATUS_AVAILABLE = 0;
public static final int CUSTOMER_STATUS_AVAILABLE = 0;
/**
* 客户状态 - 保护中
*/
public static final Integer CUSTOMER_STATUS_PROTECTED = 1;
public static final int CUSTOMER_STATUS_PROTECTED = 1;
/**
* 客户状态 - 已失效
*/
public static final Integer CUSTOMER_STATUS_EXPIRED = 2;
public static final int CUSTOMER_STATUS_EXPIRED = 2;
/**
* 报备状态 - 待审核
*/
public static final Integer REPORT_STATUS_PENDING = 0;
public static final int REPORT_STATUS_PENDING = 0;
/**
* 报备状态 - 已通过
*/
public static final Integer REPORT_STATUS_APPROVED = 1;
public static final int REPORT_STATUS_APPROVED = 1;
/**
* 报备状态 - 已驳回
*/
public static final Integer REPORT_STATUS_REJECTED = 2;
public static final int REPORT_STATUS_REJECTED = 2;
/**
* 报备状态 - 已失效
*/
public static final Integer REPORT_STATUS_EXPIRED = 3;
public static final int REPORT_STATUS_EXPIRED = 3;
/**
* 用户角色 - 管理员
*/
public static final Integer USER_ROLE_ADMIN = 0;
public static final int USER_ROLE_ADMIN = 0;
/**
* 用户角色 - 经销商用户
*/
public static final Integer USER_ROLE_DEALER = 1;
public static final int USER_ROLE_DEALER = 1;
/**
* 用户状态 - 禁用
*/
public static final Integer USER_STATUS_DISABLED = 0;
public static final int USER_STATUS_DISABLED = 0;
/**
* 用户状态 - 启用
*/
public static final Integer USER_STATUS_ENABLED = 1;
public static final int USER_STATUS_ENABLED = 1;
/**
* 经销商状态 - 禁用
*/
public static final Integer DEALER_STATUS_DISABLED = 0;
public static final int DEALER_STATUS_DISABLED = 0;
/**
* 经销商状态 - 启用
*/
public static final Integer DEALER_STATUS_ENABLED = 1;
public static final int DEALER_STATUS_ENABLED = 1;
/**
* 默认保护期天数
*/
public static final Integer DEFAULT_PROTECT_DAYS = 90;
public static final int DEFAULT_PROTECT_DAYS = 90;
}
@@ -1,7 +1,9 @@
package com.bycrm.controller;
import com.bycrm.common.Result;
import com.bycrm.dto.ChangePasswordDTO;
import com.bycrm.dto.LoginDTO;
import com.bycrm.dto.ResetPasswordDTO;
import com.bycrm.service.UserService;
import com.bycrm.vo.UserInfoVO;
import io.swagger.annotations.Api;
@@ -54,6 +56,30 @@ public class AuthController {
return Result.success();
}
/**
* 修改密码
*/
@ApiOperation("修改密码")
@PostMapping("/change-password")
public Result<Void> changePassword(
@RequestBody ChangePasswordDTO dto,
HttpServletRequest request) {
String token = getTokenFromRequest(request);
Long userId = userService.getCurrentUser(token).getUserId();
userService.changePassword(userId, dto);
return Result.success();
}
/**
* 重置密码(管理员功能)
*/
@ApiOperation("重置密码")
@PostMapping("/reset-password")
public Result<Void> resetPassword(@RequestBody ResetPasswordDTO dto) {
userService.resetPassword(dto);
return Result.success();
}
/**
* 从请求中获取 Token
*/
@@ -0,0 +1,78 @@
package com.bycrm.controller;
import com.bycrm.common.Result;
import com.bycrm.dto.DashboardStatisticsDTO;
import com.bycrm.entity.User;
import com.bycrm.mapper.CustomerMapper;
import com.bycrm.mapper.DealerMapper;
import com.bycrm.mapper.UserMapper;
import com.bycrm.service.ReportService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
/**
* 首页统计控制器
*/
@Api(tags = "首页统计")
@RestController
@RequestMapping("/dashboard")
public class DashboardController {
private final CustomerMapper customerMapper;
private final DealerMapper dealerMapper;
private final ReportService reportService;
private final UserMapper userMapper;
public DashboardController(CustomerMapper customerMapper,
DealerMapper dealerMapper,
ReportService reportService,
UserMapper userMapper) {
this.customerMapper = customerMapper;
this.dealerMapper = dealerMapper;
this.reportService = reportService;
this.userMapper = userMapper;
}
/**
* 获取首页统计数据
*/
@ApiOperation("获取首页统计数据")
@GetMapping("/statistics")
public Result<DashboardStatisticsDTO> getStatistics(HttpServletRequest request) {
Long currentUserId = (Long) request.getAttribute("currentUserId");
User currentUser = userMapper.selectById(currentUserId);
DashboardStatisticsDTO stats = new DashboardStatisticsDTO();
// 客户总数(所有人看到的客户总数一样)
Long customerCount = customerMapper.countPage(null, null, null);
stats.setCustomerCount(customerCount);
// 报备总数和待审核数量(根据角色过滤)
Long dealerId = null;
if (currentUser.getRole() != 0) {
// 经销商用户,只统计自己的
dealerId = currentUser.getDealerId();
}
// 管理员用户,dealerId为null,统计所有
Long reportCount = reportService.countByDealerId(dealerId);
stats.setReportCount(reportCount);
Long pendingCount = reportService.countPendingByDealerId(dealerId);
stats.setPendingCount(pendingCount);
// 经销商总数(仅管理员可见)
if (currentUser.getRole() == 0) {
Long dealerCount = dealerMapper.count();
stats.setDealerCount(dealerCount);
} else {
stats.setDealerCount(0L);
}
return Result.success(stats);
}
}
@@ -32,7 +32,7 @@ public class DealerController {
@GetMapping("/list")
public Result<List<Dealer>> getDealerList(
@ApiParam("经销商名称") @RequestParam(required = false) String name,
@ApiParam("经销商编码") @RequestParam(required = false) String code,
@ApiParam("经销商账号") @RequestParam(required = false) String code,
@ApiParam("状态") @RequestParam(required = false) Integer status) {
List<Dealer> dealers = dealerService.getDealerList(name, code, status);
return Result.success(dealers);
@@ -0,0 +1,128 @@
package com.bycrm.controller;
import com.bycrm.common.Result;
import com.bycrm.dto.SchoolDTO;
import com.bycrm.service.SchoolService;
import com.bycrm.util.SchoolImporter;
import com.bycrm.vo.SchoolVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* 学校控制器
*/
@Api(tags = "学校管理")
@RestController
@RequestMapping("/school")
public class SchoolController {
private final SchoolService schoolService;
@Value("${project.root:./}")
private String projectRoot;
public SchoolController(SchoolService schoolService) {
this.schoolService = schoolService;
}
/**
* 根据名称搜索学校(用于自动完成)
*/
@ApiOperation("根据名称搜索学校")
@GetMapping("/search")
public Result<List<SchoolVO>> searchByName(
@ApiParam("关键词") @RequestParam String keyword) {
List<SchoolVO> schools = schoolService.searchByName(keyword);
return Result.success(schools);
}
/**
* 查询所有学校
*/
@ApiOperation("查询所有学校")
@GetMapping("/list")
public Result<List<SchoolVO>> findAll() {
List<SchoolVO> schools = schoolService.findAll();
return Result.success(schools);
}
/**
* 创建学校
*/
@ApiOperation("创建学校")
@PostMapping
public Result<Long> create(@RequestBody SchoolDTO dto) {
Long id = schoolService.create(dto);
return Result.success(id);
}
/**
* 批量导入学校
*/
@ApiOperation("批量导入学校")
@PostMapping("/batch")
public Result<Integer> batchImport(@RequestBody List<SchoolDTO> list) {
int count = schoolService.batchImport(list);
return Result.success(count);
}
/**
* 从Excel文件导入学校数据
*/
@ApiOperation("从Excel文件导入学校数据")
@PostMapping("/import")
public Result<Integer> importFromExcel(@RequestParam("file") MultipartFile file) {
try {
// 保存临时文件
File tempFile = File.createTempFile("school_import_", ".xls");
file.transferTo(tempFile);
// 解析Excel
List<SchoolDTO> schools = SchoolImporter.importFromXls(tempFile.getAbsolutePath());
// 导入数据库
int count = schoolService.batchImport(schools);
// 删除临时文件
tempFile.delete();
return Result.success(count);
} catch (IOException e) {
return Result.error("导入失败: " + e.getMessage());
}
}
/**
* 从项目docs目录导入学校数据
*/
@ApiOperation("从项目docs目录导入学校数据")
@PostMapping("/import-from-docs")
public Result<Integer> importFromDocs() {
try {
String filePath = projectRoot + "/docs/school.xls";
File file = new File(filePath);
if (!file.exists()) {
return Result.error("文件不存在: " + filePath);
}
// 解析Excel
List<SchoolDTO> schools = SchoolImporter.importFromXls(filePath);
// 导入数据库
int count = schoolService.batchImport(schools);
return Result.success(count);
} catch (Exception e) {
return Result.error("导入失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,107 @@
package com.bycrm.controller;
import com.bycrm.common.Result;
import com.bycrm.entity.SystemConfig;
import com.bycrm.entity.User;
import com.bycrm.mapper.UserMapper;
import com.bycrm.service.SystemConfigService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
/**
* 系统配置控制器
*/
@Api(tags = "系统配置管理")
@RestController
@RequestMapping("/system/config")
public class SystemConfigController {
private final SystemConfigService systemConfigService;
private final UserMapper userMapper;
public SystemConfigController(SystemConfigService systemConfigService,
UserMapper userMapper) {
this.systemConfigService = systemConfigService;
this.userMapper = userMapper;
}
/**
* 获取所有配置(仅管理员)
*/
@ApiOperation("获取所有系统配置")
@GetMapping
public Result<List<SystemConfig>> getAllConfigs(HttpServletRequest request) {
// 权限校验:只有管理员可以查看
Long currentUserId = (Long) request.getAttribute("currentUserId");
User currentUser = userMapper.selectById(currentUserId);
if (currentUser == null || currentUser.getRole() != 0) {
return Result.error("无权限访问");
}
List<SystemConfig> configs = systemConfigService.getAllConfigs();
return Result.success(configs);
}
/**
* 获取配置值(所有用户可访问)
*/
@ApiOperation("获取指定配置值")
@GetMapping("/value/{configKey}")
public Result<String> getConfigValue(@PathVariable String configKey) {
// 只允许访问特定配置,防止敏感信息泄露
if (!configKey.startsWith("report.")) {
return Result.error("无权访问该配置");
}
String value = systemConfigService.getConfigValue(configKey);
return Result.success(value);
}
/**
* 更新配置(仅管理员)
*/
@ApiOperation("更新系统配置")
@PutMapping
public Result<Void> updateConfig(@RequestBody Map<String, String> params,
HttpServletRequest request) {
// 权限校验
Long currentUserId = (Long) request.getAttribute("currentUserId");
User currentUser = userMapper.selectById(currentUserId);
if (currentUser == null || currentUser.getRole() != 0) {
return Result.error("只有管理员才能修改配置");
}
String configKey = params.get("configKey");
String configValue = params.get("configValue");
if (configKey == null || configValue == null) {
return Result.error("参数不完整");
}
systemConfigService.updateConfig(configKey, configValue);
return Result.success();
}
/**
* 批量更新配置(仅管理员)
*/
@ApiOperation("批量更新系统配置")
@PutMapping("/batch")
public Result<Void> batchUpdateConfigs(@RequestBody Map<String, String> configs,
HttpServletRequest request) {
// 权限校验
Long currentUserId = (Long) request.getAttribute("currentUserId");
User currentUser = userMapper.selectById(currentUserId);
if (currentUser == null || currentUser.getRole() != 0) {
return Result.error("只有管理员才能修改配置");
}
systemConfigService.batchUpdateConfigs(configs);
return Result.success();
}
}
@@ -0,0 +1,29 @@
package com.bycrm.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable;
/**
* 修改密码DTO
*/
@Data
public class ChangePasswordDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 原密码
*/
@NotBlank(message = "原密码不能为空")
private String oldPassword;
/**
* 新密码
*/
@NotBlank(message = "新密码不能为空")
@Size(min = 6, max = 20, message = "新密码长度必须在6-20位之间")
private String newPassword;
}
@@ -0,0 +1,34 @@
package com.bycrm.dto;
import lombok.Data;
import java.io.Serializable;
/**
* 首页统计DTO
*/
@Data
public class DashboardStatisticsDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 客户总数
*/
private Long customerCount;
/**
* 报备总数
*/
private Long reportCount;
/**
* 待审核数量
*/
private Long pendingCount;
/**
* 经销商总数(仅管理员可见)
*/
private Long dealerCount;
}
@@ -23,7 +23,7 @@ public class DealerDTO implements Serializable {
/**
* 经销商编码
*/
@NotBlank(message = "经销商编码不能为空")
@NotBlank(message = "经销商账号不能为空")
private String code;
/**
@@ -48,4 +48,9 @@ public class DealerDTO implements Serializable {
*/
@NotNull(message = "状态不能为空")
private Integer status;
/**
* 初始密码(新增用户时可设置)
*/
private String password;
}
@@ -22,6 +22,11 @@ public class PageQuery implements Serializable {
*/
private Long size = 10L;
/**
* 偏移量(自动计算,不需要手动设置)
*/
private Long offset;
/**
* 排序字段
*/
@@ -0,0 +1,28 @@
package com.bycrm.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable;
/**
* 重置密码DTO
*/
@Data
public class ResetPasswordDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 新密码
*/
@NotBlank(message = "新密码不能为空")
@Size(min = 6, max = 20, message = "新密码长度必须在6-20位之间")
private String newPassword;
}
@@ -0,0 +1,37 @@
package com.bycrm.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* 学校DTO
*/
@Data
public class SchoolDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 学校ID
*/
private Long id;
/**
* 学校标识码
*/
@NotBlank(message = "学校标识码不能为空")
private String schoolCode;
/**
* 学校名称
*/
@NotBlank(message = "学校名称不能为空")
private String schoolName;
/**
* 所在地
*/
private String location;
}
@@ -49,6 +49,11 @@ public class Dealer implements Serializable {
*/
private Integer status;
/**
* 关联的用户ID(用于重置密码等操作)
*/
private Long userId;
/**
* 创建时间
*/
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
@@ -45,16 +46,16 @@ public class Report implements Serializable {
private String rejectReason;
/**
* 保护期开始时间
* 保护期开始日期
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime protectStartDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate protectStartDate;
/**
* 保护期结束时间
* 保护期结束日期
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime protectEndDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate protectEndDate;
/**
* 创建时间
@@ -0,0 +1,48 @@
package com.bycrm.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 学校实体
*/
@Data
public class School implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 学校ID
*/
private Long id;
/**
* 学校标识码
*/
private String schoolCode;
/**
* 学校名称
*/
private String schoolName;
/**
* 所在地
*/
private String location;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
}
@@ -0,0 +1,63 @@
package com.bycrm.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 系统配置实体
*/
@Data
public class SystemConfig implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 配置ID
*/
private Long id;
/**
* 配置键
*/
private String configKey;
/**
* 配置值
*/
private String configValue;
/**
* 配置名称
*/
private String configLabel;
/**
* 数据类型:string/integer/boolean
*/
private String configType;
/**
* 配置描述
*/
private String description;
/**
* 是否可编辑:0-否 1-是
*/
private Integer isEditable;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
}
@@ -2,6 +2,7 @@ package com.bycrm.mapper;
import com.bycrm.entity.Customer;
import com.bycrm.dto.PageQuery;
import com.bycrm.vo.CustomerVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -29,6 +30,12 @@ public interface CustomerMapper {
List<Customer> selectPage(@Param("query") PageQuery query, @Param("name") String name,
@Param("industry") String industry, @Param("status") Integer status);
/**
* 分页查询客户(带经销商信息)
*/
List<CustomerVO> selectPageWithDealer(@Param("query") PageQuery query, @Param("name") String name,
@Param("industry") String industry, @Param("status") Integer status);
/**
* 查询客户总数
*/
@@ -41,4 +41,9 @@ public interface DealerMapper {
* 删除经销商
*/
int deleteById(@Param("id") Long id);
/**
* 统计经销商总数
*/
Long count();
}
@@ -60,4 +60,14 @@ public interface ReportMapper {
* 批量更新报备状态为已失效
*/
int batchUpdateExpired(@Param("ids") List<Long> ids);
/**
* 统计报备总数(根据经销商ID过滤)
*/
Long countByDealerId(@Param("dealerId") Long dealerId);
/**
* 统计待审核报备数量(根据经销商ID过滤)
*/
Long countPendingByDealerId(@Param("dealerId") Long dealerId);
}
@@ -0,0 +1,49 @@
package com.bycrm.mapper;
import com.bycrm.entity.School;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 学校Mapper
*/
@Mapper
public interface SchoolMapper {
/**
* 插入学校
*/
int insert(School school);
/**
* 批量插入学校
*/
int batchInsert(@Param("list") List<School> list);
/**
* 根据学校名称模糊查询
*/
List<School> searchByName(@Param("keyword") String keyword);
/**
* 根据学校标识码查询
*/
School findByCode(@Param("schoolCode") String schoolCode);
/**
* 查询所有学校
*/
List<School> findAll();
/**
* 分页查询学校
*/
List<School> findByPage(@Param("offset") int offset, @Param("limit") int limit);
/**
* 统计学校数量
*/
int count();
}
@@ -0,0 +1,39 @@
package com.bycrm.mapper;
import com.bycrm.entity.SystemConfig;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 系统配置Mapper
*/
@Mapper
public interface SystemConfigMapper {
/**
* 查询所有配置
*/
List<SystemConfig> selectAll();
/**
* 根据配置键查询
*/
SystemConfig selectByKey(@Param("configKey") String configKey);
/**
* 根据配置键查询配置值
*/
String selectValueByKey(@Param("configKey") String configKey);
/**
* 更新配置
*/
int update(SystemConfig config);
/**
* 批量更新配置
*/
int batchUpdate(@Param("configs") List<SystemConfig> configs);
}
@@ -41,4 +41,14 @@ public interface ReportService {
* 处理过期报备(定时任务调用)
*/
void handleExpiredReports();
/**
* 统计报备总数(根据经销商ID过滤)
*/
Long countByDealerId(Long dealerId);
/**
* 统计待审核报备数量(根据经销商ID过滤)
*/
Long countPendingByDealerId(Long dealerId);
}
@@ -0,0 +1,32 @@
package com.bycrm.service;
import com.bycrm.dto.SchoolDTO;
import com.bycrm.vo.SchoolVO;
import java.util.List;
/**
* 学校Service
*/
public interface SchoolService {
/**
* 根据名称搜索学校(用于自动完成)
*/
List<SchoolVO> searchByName(String keyword);
/**
* 创建学校
*/
Long create(SchoolDTO dto);
/**
* 批量导入学校
*/
int batchImport(List<SchoolDTO> list);
/**
* 查询所有学校
*/
List<SchoolVO> findAll();
}
@@ -0,0 +1,52 @@
package com.bycrm.service;
import com.bycrm.entity.SystemConfig;
import java.util.List;
import java.util.Map;
/**
* 系统配置服务接口
*/
public interface SystemConfigService {
/**
* 获取所有配置
*/
List<SystemConfig> getAllConfigs();
/**
* 根据配置键获取配置值
*/
String getConfigValue(String configKey);
/**
* 根据配置键获取整数配置值
*/
Integer getIntegerConfigValue(String configKey, Integer defaultValue);
/**
* 根据配置键获取布尔配置值
*/
Boolean getBooleanConfigValue(String configKey, Boolean defaultValue);
/**
* 获取所有配置为Map
*/
Map<String, String> getConfigMap();
/**
* 更新配置
*/
void updateConfig(String configKey, String configValue);
/**
* 批量更新配置
*/
void batchUpdateConfigs(Map<String, String> configMap);
/**
* 初始化配置(从数据库加载,如果不存在则使用默认值)
*/
void initConfigs();
}
@@ -1,6 +1,8 @@
package com.bycrm.service;
import com.bycrm.dto.ChangePasswordDTO;
import com.bycrm.dto.LoginDTO;
import com.bycrm.dto.ResetPasswordDTO;
import com.bycrm.entity.User;
import com.bycrm.vo.UserInfoVO;
@@ -28,4 +30,14 @@ public interface UserService {
* 获取当前登录用户信息
*/
UserInfoVO getCurrentUser(String token);
/**
* 修改密码
*/
void changePassword(Long userId, ChangePasswordDTO dto);
/**
* 重置密码(管理员功能)
*/
void resetPassword(ResetPasswordDTO dto);
}
@@ -1,12 +1,10 @@
package com.bycrm.service.impl;
import cn.hutool.core.util.StrUtil;
import com.bycrm.common.Constants;
import com.bycrm.common.PageResult;
import com.bycrm.dto.CustomerDTO;
import com.bycrm.dto.PageQuery;
import com.bycrm.entity.Customer;
import com.bycrm.entity.User;
import com.bycrm.exception.BusinessException;
import com.bycrm.mapper.CustomerMapper;
import com.bycrm.mapper.UserMapper;
@@ -17,7 +15,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Collectors;
@@ -40,15 +37,11 @@ public class CustomerServiceImpl implements CustomerService {
// 计算偏移量
query.setOffset((query.getCurrent() - 1) * query.getSize());
List<Customer> customers = customerMapper.selectPage(query, name, industry, status);
// 使用新的查询方法,直接返回 CustomerVO(包含经销商信息)
List<CustomerVO> customerVOs = customerMapper.selectPageWithDealer(query, name, industry, status);
Long total = customerMapper.countPage(name, industry, status);
List<CustomerVO> voList = customers.stream().map(customer -> {
CustomerVO vo = convertToVO(customer);
return vo;
}).collect(Collectors.toList());
return PageResult.of(total, voList, query.getCurrent(), query.getSize());
return PageResult.of(total, customerVOs, query.getCurrent(), query.getSize());
}
@Override
@@ -1,11 +1,15 @@
package com.bycrm.service.impl;
import com.bycrm.common.Constants;
import com.bycrm.dto.DealerDTO;
import com.bycrm.entity.Dealer;
import com.bycrm.entity.User;
import com.bycrm.exception.BusinessException;
import com.bycrm.mapper.DealerMapper;
import com.bycrm.mapper.UserMapper;
import com.bycrm.service.DealerService;
import org.springframework.beans.BeanUtils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -19,9 +23,12 @@ import java.util.List;
public class DealerServiceImpl implements DealerService {
private final DealerMapper dealerMapper;
private final UserMapper userMapper;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public DealerServiceImpl(DealerMapper dealerMapper) {
public DealerServiceImpl(DealerMapper dealerMapper, UserMapper userMapper) {
this.dealerMapper = dealerMapper;
this.userMapper = userMapper;
}
@Override
@@ -44,7 +51,7 @@ public class DealerServiceImpl implements DealerService {
// 检查编码是否重复
Dealer existingDealer = dealerMapper.selectByCode(dealerDTO.getCode());
if (existingDealer != null) {
throw new BusinessException("经销商编码已存在");
throw new BusinessException("经销商账号已存在");
}
Dealer dealer = new Dealer();
@@ -53,6 +60,25 @@ public class DealerServiceImpl implements DealerService {
dealer.setUpdatedAt(LocalDateTime.now());
dealerMapper.insert(dealer);
// 创建对应的用户账号
User user = new User();
user.setUsername(dealerDTO.getCode()); // 用户名使用经销商编码
user.setRealName(dealerDTO.getContactPerson());
user.setDealerId(dealer.getId());
user.setRole(Constants.USER_ROLE_DEALER); // 经销商用户角色
user.setStatus(dealerDTO.getStatus());
// 设置密码:如果提供了密码则使用提供的密码,否则使用默认密码
String password = dealerDTO.getPassword();
if (password == null || password.trim().isEmpty()) {
password = "123456"; // 默认密码
}
user.setPassword(passwordEncoder.encode(password));
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
userMapper.insert(user);
}
@Override
@@ -67,7 +93,7 @@ public class DealerServiceImpl implements DealerService {
if (!existingDealer.getCode().equals(dealerDTO.getCode())) {
Dealer codeDealer = dealerMapper.selectByCode(dealerDTO.getCode());
if (codeDealer != null && !codeDealer.getId().equals(id)) {
throw new BusinessException("经销商编码已存在");
throw new BusinessException("经销商账号已存在");
}
}
@@ -13,9 +13,9 @@ import com.bycrm.mapper.CustomerMapper;
import com.bycrm.mapper.ReportMapper;
import com.bycrm.mapper.UserMapper;
import com.bycrm.service.ReportService;
import com.bycrm.service.SystemConfigService;
import com.bycrm.vo.ReportVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -34,17 +34,16 @@ public class ReportServiceImpl implements ReportService {
private final ReportMapper reportMapper;
private final CustomerMapper customerMapper;
private final UserMapper userMapper;
private final SystemConfigService systemConfigService;
@Value("${crm.report.ttl-days:90}")
private Integer protectDays;
@Value("${crm.report.allow-overlap:false}")
private Boolean allowOverlap;
public ReportServiceImpl(ReportMapper reportMapper, CustomerMapper customerMapper, UserMapper userMapper) {
public ReportServiceImpl(ReportMapper reportMapper,
CustomerMapper customerMapper,
UserMapper userMapper,
SystemConfigService systemConfigService) {
this.reportMapper = reportMapper;
this.customerMapper = customerMapper;
this.userMapper = userMapper;
this.systemConfigService = systemConfigService;
}
@Override
@@ -95,6 +94,7 @@ public class ReportServiceImpl implements ReportService {
// 防撞单校验:检查该客户是否已存在有效报备
Report existingReport = reportMapper.selectValidByCustomerId(reportDTO.getCustomerId());
Boolean allowOverlap = systemConfigService.getBooleanConfigValue("report.allow.overlap", false);
if (existingReport != null && !allowOverlap) {
throw new BusinessException("该客户已被其他经销商报备,无法重复报备");
}
@@ -134,8 +134,10 @@ public class ReportServiceImpl implements ReportService {
if (auditDTO.getApproved()) {
// 审核通过
report.setStatus(Constants.REPORT_STATUS_APPROVED);
report.setProtectStartDate(now);
report.setProtectEndDate(now.plusDays(protectDays));
LocalDate today = LocalDate.now();
report.setProtectStartDate(today);
Integer protectDays = systemConfigService.getIntegerConfigValue("report.protect.days", Constants.DEFAULT_PROTECT_DAYS);
report.setProtectEndDate(today.plusDays(protectDays));
// 更新客户状态为保护中
Customer customer = customerMapper.selectById(report.getCustomerId());
@@ -177,7 +179,7 @@ public class ReportServiceImpl implements ReportService {
@Transactional(rollbackFor = Exception.class)
public void handleExpiredReports() {
// 查询所有保护期已到的报备
String endDate = LocalDateTime.now().toString();
String endDate = LocalDate.now().toString();
List<Report> expiringReports = reportMapper.selectExpiringReports(endDate);
if (expiringReports.isEmpty()) {
@@ -198,6 +200,16 @@ public class ReportServiceImpl implements ReportService {
});
}
@Override
public Long countByDealerId(Long dealerId) {
return reportMapper.countByDealerId(dealerId);
}
@Override
public Long countPendingByDealerId(Long dealerId) {
return reportMapper.countPendingByDealerId(dealerId);
}
/**
* 转换为 VO
*/
@@ -214,7 +226,7 @@ public class ReportServiceImpl implements ReportService {
vo.setStatusDesc("已通过");
// 计算剩余保护天数
if (report.getProtectEndDate() != null) {
long days = ChronoUnit.DAYS.between(LocalDateTime.now(), report.getProtectEndDate());
long days = ChronoUnit.DAYS.between(LocalDate.now(), report.getProtectEndDate());
vo.setRemainDays((int) Math.max(0, days));
}
break;
@@ -0,0 +1,96 @@
package com.bycrm.service.impl;
import com.bycrm.dto.SchoolDTO;
import com.bycrm.entity.School;
import com.bycrm.exception.BusinessException;
import com.bycrm.mapper.SchoolMapper;
import com.bycrm.service.SchoolService;
import com.bycrm.vo.SchoolVO;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 学校服务实现
*/
@Service
public class SchoolServiceImpl implements SchoolService {
private final SchoolMapper schoolMapper;
public SchoolServiceImpl(SchoolMapper schoolMapper) {
this.schoolMapper = schoolMapper;
}
@Override
public List<SchoolVO> searchByName(String keyword) {
if (keyword == null || keyword.trim().isEmpty()) {
return new ArrayList<>();
}
List<School> schools = schoolMapper.searchByName(keyword.trim());
return schools.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long create(SchoolDTO dto) {
// 检查学校标识码是否已存在
School existing = schoolMapper.findByCode(dto.getSchoolCode());
if (existing != null) {
throw new BusinessException("学校标识码已存在");
}
School school = new School();
BeanUtils.copyProperties(dto, school);
school.setCreatedAt(LocalDateTime.now());
school.setUpdatedAt(LocalDateTime.now());
schoolMapper.insert(school);
return school.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public int batchImport(List<SchoolDTO> list) {
if (list == null || list.isEmpty()) {
return 0;
}
LocalDateTime now = LocalDateTime.now();
List<School> schools = list.stream()
.map(dto -> {
School school = new School();
BeanUtils.copyProperties(dto, school);
school.setCreatedAt(now);
school.setUpdatedAt(now);
return school;
})
.collect(Collectors.toList());
return schoolMapper.batchInsert(schools);
}
@Override
public List<SchoolVO> findAll() {
List<School> schools = schoolMapper.findAll();
return schools.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
}
/**
* 转换为 VO
*/
private SchoolVO convertToVO(School school) {
SchoolVO vo = new SchoolVO();
BeanUtils.copyProperties(school, vo);
return vo;
}
}
@@ -0,0 +1,194 @@
package com.bycrm.service.impl;
import com.bycrm.common.Constants;
import com.bycrm.entity.SystemConfig;
import com.bycrm.exception.BusinessException;
import com.bycrm.mapper.SystemConfigMapper;
import com.bycrm.service.SystemConfigService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 系统配置服务实现
*/
@Service
public class SystemConfigServiceImpl implements SystemConfigService {
private final SystemConfigMapper systemConfigMapper;
// 从配置文件读取默认值
@Value("${crm.report.ttl-days:90}")
private Integer defaultProtectDays;
@Value("${crm.report.allow-overlap:false}")
private Boolean defaultAllowOverlap;
// 本地缓存,避免频繁查询数据库
private Map<String, String> configCache = new HashMap<>();
public SystemConfigServiceImpl(SystemConfigMapper systemConfigMapper) {
this.systemConfigMapper = systemConfigMapper;
}
@PostConstruct
public void init() {
initConfigs();
}
@Override
public void initConfigs() {
List<SystemConfig> configs = systemConfigMapper.selectAll();
// 如果数据库为空,插入默认配置
if (configs.isEmpty()) {
insertDefaultConfigs();
configs = systemConfigMapper.selectAll();
}
// 构建缓存
configCache = configs.stream()
.collect(Collectors.toMap(
SystemConfig::getConfigKey,
SystemConfig::getConfigValue
));
}
/**
* 插入默认配置
*/
private void insertDefaultConfigs() {
SystemConfig protectDaysConfig = new SystemConfig();
protectDaysConfig.setConfigKey("report.protect.days");
protectDaysConfig.setConfigValue(String.valueOf(defaultProtectDays));
protectDaysConfig.setConfigLabel("报备保护期天数");
protectDaysConfig.setConfigType("integer");
protectDaysConfig.setDescription("报备审核通过后客户的保护天数");
protectDaysConfig.setIsEditable(1);
systemConfigMapper.update(protectDaysConfig);
// 先插入再更新,因为update方法可能失败,我们需要insert
// 这里为了简单,我们直接使用SQL插入
// 实际应该添加insert方法到mapper
}
@Override
public List<SystemConfig> getAllConfigs() {
return systemConfigMapper.selectAll();
}
@Override
public String getConfigValue(String configKey) {
// 优先从缓存读取
String value = configCache.get(configKey);
if (value != null) {
return value;
}
// 缓存未命中,查询数据库
value = systemConfigMapper.selectValueByKey(configKey);
if (value != null) {
configCache.put(configKey, value);
}
return value;
}
@Override
public Integer getIntegerConfigValue(String configKey, Integer defaultValue) {
String value = getConfigValue(configKey);
if (value == null) {
return defaultValue;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
@Override
public Boolean getBooleanConfigValue(String configKey, Boolean defaultValue) {
String value = getConfigValue(configKey);
if (value == null) {
return defaultValue;
}
return Boolean.parseBoolean(value);
}
@Override
public Map<String, String> getConfigMap() {
return new HashMap<>(configCache);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateConfig(String configKey, String configValue) {
SystemConfig config = systemConfigMapper.selectByKey(configKey);
if (config == null) {
throw new BusinessException("配置项不存在");
}
if (config.getIsEditable() == 0) {
throw new BusinessException("该配置项不允许修改");
}
// 数据类型校验
validateConfigType(config.getConfigType(), configValue);
config.setConfigValue(configValue);
systemConfigMapper.update(config);
// 更新缓存
configCache.put(configKey, configValue);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void batchUpdateConfigs(Map<String, String> configMap) {
configMap.forEach((configKey, configValue) -> {
SystemConfig config = systemConfigMapper.selectByKey(configKey);
if (config != null && config.getIsEditable() == 1) {
// 数据类型校验
validateConfigType(config.getConfigType(), configValue);
// 更新配置
config.setConfigValue(configValue);
systemConfigMapper.update(config);
// 更新缓存
configCache.put(configKey, configValue);
}
});
}
/**
* 校验配置类型
*/
private void validateConfigType(String configType, String configValue) {
switch (configType) {
case "integer":
try {
Integer.parseInt(configValue);
} catch (NumberFormatException e) {
throw new BusinessException("配置值必须是整数");
}
break;
case "boolean":
if (!"true".equals(configValue) && !"false".equals(configValue)) {
throw new BusinessException("配置值必须是 true 或 false");
}
break;
case "string":
// 字符串类型无需校验
break;
default:
throw new BusinessException("未知的配置类型:" + configType);
}
}
}
@@ -1,6 +1,8 @@
package com.bycrm.service.impl;
import com.bycrm.common.Constants;
import com.bycrm.dto.ChangePasswordDTO;
import com.bycrm.dto.ResetPasswordDTO;
import com.bycrm.entity.User;
import com.bycrm.exception.BusinessException;
import com.bycrm.mapper.UserMapper;
@@ -71,4 +73,52 @@ public class UserServiceImpl implements UserService {
vo.setRoleDesc(user.getRole() == Constants.USER_ROLE_ADMIN ? "管理员" : "经销商用户");
return vo;
}
@Override
public void changePassword(Long userId, ChangePasswordDTO dto) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException("用户不存在");
}
// 验证原密码
if (!passwordEncoder.matches(dto.getOldPassword(), user.getPassword())) {
throw new BusinessException("原密码错误");
}
// 新密码不能与原密码相同
if (dto.getOldPassword().equals(dto.getNewPassword())) {
throw new BusinessException("新密码不能与原密码相同");
}
// 加密新密码并更新
String encodedPassword = passwordEncoder.encode(dto.getNewPassword());
user.setPassword(encodedPassword);
userMapper.update(user);
}
@Override
public void resetPassword(ResetPasswordDTO dto) {
User user = userMapper.selectById(dto.getUserId());
if (user == null) {
throw new BusinessException("用户不存在");
}
// 管理员不能重置自己的密码(应使用修改密码功能)
if (user.getRole() == Constants.USER_ROLE_ADMIN) {
throw new BusinessException("不能重置管理员密码");
}
// 加密新密码并更新
String encodedPassword = passwordEncoder.encode(dto.getNewPassword());
user.setPassword(encodedPassword);
userMapper.update(user);
}
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String rawPassword = "59cce3254e6d76ec6bad2d84ae56b5da3e118b2f22d0d2b2d780356cbe3c0881";
String encodedPassword = passwordEncoder.encode(rawPassword);
System.out.println("Encoded Password: " + encodedPassword);
}
}
@@ -0,0 +1,117 @@
package com.bycrm.util;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import java.io.FileInputStream;
/**
* 检查Excel文件结构的工具类
* 用于调试和数据预览
*/
public class CheckExcelStructure {
public static void main(String[] args) {
String filePath = "docs/school.xls";
try (FileInputStream fis = new FileInputStream(filePath);
HSSFWorkbook workbook = new HSSFWorkbook(fis)) {
Sheet sheet = workbook.getSheetAt(0);
System.out.println("=== Excel 文件结构检查 ===");
System.out.println("文件路径: " + filePath);
System.out.println("工作表名称: " + sheet.getSheetName());
System.out.println("总行数: " + (sheet.getLastRowNum() + 1));
System.out.println();
// 显示标题行
System.out.println("=== 标题行(第1行)===");
Row headerRow = sheet.getRow(0);
if (headerRow != null) {
for (int i = 0; i < 10; i++) {
Cell cell = headerRow.getCell(i);
String value = getCellValueAsString(cell);
System.out.printf(" 列%c: %s%n", 'A' + i, value != null ? value : "[空]");
}
}
System.out.println();
// 显示前5行数据
System.out.println("=== 数据预览(前5行)===");
int previewRows = Math.min(6, sheet.getLastRowNum() + 1);
for (int i = 1; i < previewRows; i++) {
Row row = sheet.getRow(i);
if (row == null) {
System.out.println(" 第" + (i + 1) + "行: [空行]");
continue;
}
System.out.printf(" 第%d行:%n", i + 1);
for (int j = 0; j < 4; j++) {
Cell cell = row.getCell(j);
String value = getCellValueAsString(cell);
System.out.printf(" 列%c: %s%n", 'A' + j, value != null ? value : "[空]");
}
System.out.println();
}
// 解析后的数据预览
System.out.println("=== 解析后的数据预览(前5条)===");
System.out.println("字段映射:");
System.out.println(" school_code <- 列A(学校标识码)");
System.out.println(" school_name <- 列B(学校名称)");
System.out.println(" location <- 列C(主管部门+所在地)");
System.out.println();
int count = 0;
for (int i = 1; i <= sheet.getLastRowNum() && count < 5; i++) {
Row row = sheet.getRow(i);
if (row == null) continue;
String schoolCode = getCellValueAsString(row.getCell(0));
String schoolName = getCellValueAsString(row.getCell(1));
String location = getCellValueAsString(row.getCell(2));
if (schoolCode != null && schoolName != null) {
System.out.printf(" %d. school_code=%s, school_name=%s%n", count + 1, schoolCode, schoolName);
System.out.printf(" location=%s%n", location != null ? location : "[空]");
System.out.println();
count++;
}
}
} catch (Exception e) {
System.err.println("读取文件失败: " + e.getMessage());
e.printStackTrace();
}
}
private static String getCellValueAsString(Cell cell) {
if (cell == null) {
return null;
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
double numericValue = cell.getNumericCellValue();
if (numericValue == (long) numericValue) {
return String.valueOf((long) numericValue);
} else {
return String.valueOf(numericValue);
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
return cell.getCellFormula();
case BLANK:
default:
return null;
}
}
}
@@ -0,0 +1,55 @@
package com.bycrm.util;
import com.bycrm.dto.SchoolDTO;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.io.File;
import java.util.List;
/**
* 学校数据导入执行类
* 使用方法:运行 main 方法,传入 school.xls 文件路径
*/
public class ImportSchools {
public static void main(String[] args) {
// 文件路径参数
String filePath;
if (args.length > 0) {
filePath = args[0];
} else {
// 默认路径:项目根目录下的 docs/school.xls
filePath = "docs/school.xls";
}
File file = new File(filePath);
if (!file.exists()) {
System.err.println("文件不存在: " + file.getAbsolutePath());
return;
}
try {
System.out.println("开始读取文件: " + file.getAbsolutePath());
List<SchoolDTO> schools = SchoolImporter.importFromXls(filePath);
System.out.println("成功读取 " + schools.size() + " 条学校数据");
// 打印前5条数据预览
System.out.println("\n数据预览(前5条):");
int previewCount = Math.min(5, schools.size());
for (int i = 0; i < previewCount; i++) {
SchoolDTO dto = schools.get(i);
System.out.printf("%d. [%s] %s - %s%n",
i + 1, dto.getSchoolCode(), dto.getSchoolName(), dto.getLocation());
}
// 保存到文件供后续使用
System.out.println("\n数据已准备好,请通过 API 接口导入到数据库");
System.out.println("或者使用 SchoolService.batchImport() 方法批量导入");
} catch (Exception e) {
System.err.println("导入失败: " + e.getMessage());
e.printStackTrace();
}
}
}
@@ -0,0 +1,28 @@
package com.bycrm.util;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* 密码测试工具类 - 用于生成 BCrypt 哈希值
*/
public class PasswordTest {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String rawPassword = "admin123";
String hash = encoder.encode(rawPassword);
System.out.println("原始密码: " + rawPassword);
System.out.println("BCrypt 哈希值: " + hash);
System.out.println("验证结果: " + encoder.matches(rawPassword, hash));
// 生成多个哈希值,验证 BCrypt 每次生成的哈希值不同
System.out.println("\n生成多个哈希值测试:");
for (int i = 0; i < 3; i++) {
String hash2 = encoder.encode(rawPassword);
System.out.println("哈希 " + (i + 1) + ": " + hash2);
System.out.println("验证: " + encoder.matches(rawPassword, hash2));
}
}
}
@@ -0,0 +1,243 @@
package com.bycrm.util;
import com.bycrm.dto.SchoolDTO;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 学校数据导入工具类
*/
public class SchoolImporter {
/**
* 从 .xls 文件导入学校数据
*
* @param filePath 文件路径
* @return 学校数据列表
* @throws IOException 读取文件失败
*/
public static List<SchoolDTO> importFromXls(String filePath) throws IOException {
List<SchoolDTO> schools = new ArrayList<>();
try (FileInputStream fis = new FileInputStream(filePath);
HSSFWorkbook workbook = new HSSFWorkbook(fis)) {
Sheet sheet = workbook.getSheetAt(0); // 读取第一个工作表
// 跳过标题行,从第2行开始读取数据
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) {
continue;
}
SchoolDTO dto = parseRow(row);
if (dto != null) {
schools.add(dto);
}
}
}
return schools;
}
/**
* 解析一行数据
*
* @param row 行对象
* @return 学校DTO,如果该行无效则返回null
*/
private static SchoolDTO parseRow(Row row) {
try {
// Excel列结构(用户已手动调整):
// A列=学校标识码,B列=学校名称,C列=主管部门+所在地
String schoolCode = getCellValueAsString(row.getCell(2));
String schoolName = getCellValueAsString(row.getCell(1));
String location = getCellValueAsString(row.getCell(3)) + getCellValueAsString(row.getCell(4)); // 直接读取已组合好的location
// 验证必填字段
if (schoolCode == null || schoolCode.trim().isEmpty() ||
schoolName == null || schoolName.trim().isEmpty()) {
return null;
}
SchoolDTO dto = new SchoolDTO();
dto.setSchoolCode(schoolCode.trim());
dto.setSchoolName(schoolName.trim());
dto.setLocation(location.trim());
return dto;
} catch (Exception e) {
System.err.println("解析行数据失败: " + e.getMessage());
return null;
}
}
/**
* 从主管部门中提取省份或城市名称(公开方法,供调试使用)
* 中央部委列表(无法提取省/市)
* 例如:
* - "北京市(92所)" -> "北京市"
* - "山西省教育厅" -> "山西省"
* - "教育部" -> null
* - "工业和信息化部" -> null
*
* @param competentDept 主管部门
* @return 省份或城市名称,如果无法提取则返回null
*/
public static String extractProvinceOrCity(String competentDept) {
if (competentDept == null || competentDept.trim().isEmpty()) {
return null;
}
String dept = competentDept.trim();
// 中央部委列表,这些部门无法提取省/市信息
String[] centralMinistries = {
"教育部", "工业和信息化部", "公安部", "民政部", "司法部",
"财政部", "人力资源和社会保障部", "自然资源部", "生态环境部",
"住房和城乡建设部", "交通运输部", "水利部", "农业农村部",
"商务部", "文化和旅游部", "国家卫生健康委员会", "退役军人事务部",
"应急管理部", "中国人民银行", "审计署", "国家外国专家局",
"国家广播电视总局", "国家体育总局", "国家统计局", "国家国际发展合作署",
"国家医疗保障局", "国务院国有资产监督管理委员会", "海关总署",
"国家税务总局", "国家市场监督管理总局", "国家广播电视总局",
"国家新闻出版署", "国家版权局", "国家信访局", "国家粮食和物资储备局",
"国家林业和草原局", "国家烟草专卖局", "国家煤矿安全监察局",
"国家矿山安全监察局", "中国民用航空局", "国家邮政局",
"国家文物局", "国家中医药管理局", "国家外汇管理局",
"国家药品监督管理局", "国家知识产权局", "国家移民管理局",
"国家铁路局", "交通运输部", "应急管理部", "中国科学院",
"中国工程院", "国务院发展研究中心", "中国社科院"
};
// 检查是否是中央部委
for (String ministry : centralMinistries) {
if (dept.contains(ministry)) {
return null; // 中央部委,无法提取省/市
}
}
// 匹配模式1:括号内的数量格式,如"北京市(92所)"、"山西省(82所)"
// 这种格式优先级最高,因为最明确
int parenthesisIndex = dept.indexOf("(");
if (parenthesisIndex > 0) {
String beforeParenthesis = dept.substring(0, parenthesisIndex).trim();
// 检查是否以省、市、自治区结尾
if (beforeParenthesis.endsWith("省") || beforeParenthesis.endsWith("市") ||
beforeParenthesis.endsWith("自治区")) {
return beforeParenthesis;
}
}
// 匹配模式2:省份模式 XX省
if (dept.contains("省")) {
int index = dept.indexOf("省");
// 确保省名不是"教育部"等
String province = dept.substring(0, index + 1);
// 过滤掉特殊情况,如"省教育厅"
if (province.length() <= 4 && !province.contains("部")) {
return province;
}
}
// 匹配模式3:自治区模式
if (dept.contains("自治区")) {
int index = dept.indexOf("自治区");
return dept.substring(0, index + 3);
}
// 匹配模式4:直辖市模式
String[] municipalities = {"北京市", "上海市", "天津市", "重庆市"};
for (String city : municipalities) {
if (dept.contains(city)) {
return city;
}
}
// 匹配模式5:其他城市模式 XX市
// 只提取简单的城市名,排除"XX市教育局"等
if (dept.contains("市")) {
int index = dept.indexOf("市");
// 确保是完整的地名(2-4个字符),且不包含"厅"、"局"等
if (index >= 2 && index <= 4) {
String city = dept.substring(0, index + 1);
if (!city.contains("厅") && !city.contains("局") && !city.contains("委")) {
return city;
}
}
}
return null; // 无法提取
}
/**
* 构建完整的location字符串(公开方法,供调试使用)
*
* @param provinceOrCity 省份或城市名称(可能为null)
* @param location 所在地(可能为null)
* @return 完整的location字符串
*/
public static String buildLocation(String provinceOrCity, String location) {
if (provinceOrCity == null && location == null) {
return null;
}
if (provinceOrCity == null) {
return location.trim();
}
if (location == null || location.trim().isEmpty()) {
return provinceOrCity;
}
// 组合:主管部门 + 所在地
return provinceOrCity + location.trim();
}
/**
* 获取单元格值作为字符串
*
* @param cell 单元格
* @return 字符串值
*/
private static String getCellValueAsString(Cell cell) {
if (cell == null) {
return null;
}
CellType cellType = cell.getCellType();
switch (cellType) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
// 数字类型转换为字符串(处理学校标识码可能是数字的情况)
if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
// 避免科学计数法,转为长整型再转字符串
double numericValue = cell.getNumericCellValue();
if (numericValue == (long) numericValue) {
return String.valueOf((long) numericValue);
} else {
return String.valueOf(numericValue);
}
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
return cell.getCellFormula();
case BLANK:
default:
return null;
}
}
}
@@ -0,0 +1,38 @@
package com.bycrm.util;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Base64;
/**
* 安全密钥生成工具
* 用于生成符合 JWT 规范的安全密钥
*/
public class SecureKeyGenerator {
public static void main(String[] args) {
// 生成 HS512 算法的安全密钥
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
// 获取密钥的 Base64 编码
String encodedKey = Base64.getEncoder().encodeToString(key.getEncoded());
System.out.println("=== JWT 安全密钥生成器 ===");
System.out.println();
System.out.println("算法: HS512");
System.out.println("密钥长度: " + key.getEncoded().length * 8 + " 位");
System.out.println("密钥字节: " + key.getEncoded().length + " 字节");
System.out.println();
System.out.println("Base64 编码的密钥(推荐用于生产环境):");
System.out.println(encodedKey);
System.out.println();
System.out.println("请在 application.yml 中设置:");
System.out.println("jwt:");
System.out.println(" secret: " + encodedKey);
System.out.println();
System.out.println("或者使用环境变量(更安全):");
System.out.println("JWT_SECRET=" + encodedKey);
}
}
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
@@ -60,10 +61,10 @@ public class CustomerVO implements Serializable {
private String currentDealerName;
/**
* 保护期结束时间
* 保护期结束日期
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime protectEndDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate protectEndDate;
/**
* 创建时间
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
@@ -65,16 +66,16 @@ public class ReportVO implements Serializable {
private String rejectReason;
/**
* 保护期开始时间
* 保护期开始日期
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime protectStartDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate protectStartDate;
/**
* 保护期结束时间
* 保护期结束日期
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime protectEndDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate protectEndDate;
/**
* 剩余保护天数
@@ -0,0 +1,48 @@
package com.bycrm.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 学校VO
*/
@Data
public class SchoolVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 学校ID
*/
private Long id;
/**
* 学校标识码
*/
private String schoolCode;
/**
* 学校名称
*/
private String schoolName;
/**
* 所在地
*/
private String location;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
}
+9 -3
View File
@@ -9,9 +9,9 @@ spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/by_crm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
url: jdbc:mysql://192.168.3.80:3306/by_crm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
password: "Boyun@123"
druid:
initial-size: 5
min-idle: 5
@@ -39,6 +39,10 @@ spring:
date-format: yyyy-MM-dd HH:mm:ss
default-property-inclusion: non_null
mvc:
pathmatch:
matching-strategy: ant_path_matcher
# MyBatis 配置
mybatis:
mapper-locations: classpath:mapper/*.xml
@@ -50,7 +54,9 @@ mybatis:
# JWT 配置
jwt:
secret: by-crm-secret-key-2024-please-change-in-production
# HS512 算法要求密钥至少 64 字节(512 位)
# 生产环境请使用环境变量或密钥管理系统存储此密钥
secret: by-crm-jwt-secret-key-2024-hs512-requires-at-least-64-bytes-for-secure-signing-please-change-in-production-environment
expiration: 86400000 # 24小时,单位:毫秒
# CRM 业务配置
@@ -14,6 +14,13 @@
<result column="updated_at" property="updatedAt"/>
</resultMap>
<resultMap id="CustomerVOResultMap" type="com.bycrm.vo.CustomerVO" extends="BaseResultMap">
<result column="status_desc" property="statusDesc"/>
<result column="current_dealer_id" property="currentDealerId"/>
<result column="current_dealer_name" property="currentDealerName"/>
<result column="protect_end_date" property="protectEndDate"/>
</resultMap>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM crm_customer WHERE id = #{id}
</select>
@@ -77,4 +84,36 @@
DELETE FROM crm_customer WHERE id = #{id}
</delete>
<select id="selectPageWithDealer" resultMap="CustomerVOResultMap">
SELECT
c.*,
CASE c.status
WHEN 0 THEN '可报备'
WHEN 1 THEN '保护中'
WHEN 2 THEN '已失效'
ELSE '未知'
END AS status_desc,
r.dealer_id AS current_dealer_id,
d.name AS current_dealer_name,
r.protect_end_date
FROM crm_customer c
LEFT JOIN crm_report r ON c.id = r.customer_id
AND r.status = 1
AND r.protect_end_date > NOW()
LEFT JOIN crm_dealer d ON r.dealer_id = d.id
<where>
<if test="name != null and name != ''">
AND c.name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="industry != null and industry != ''">
AND c.industry = #{industry}
</if>
<if test="status != null">
AND c.status = #{status}
</if>
</where>
ORDER BY c.created_at DESC
LIMIT #{query.size} OFFSET #{query.offset}
</select>
</mapper>
@@ -13,6 +13,7 @@
<result column="status" property="status"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
<result column="user_id" property="userId"/>
</resultMap>
<select id="selectById" resultMap="BaseResultMap">
@@ -24,19 +25,21 @@
</select>
<select id="selectList" resultMap="BaseResultMap">
SELECT * FROM crm_dealer
SELECT d.*, u.id as user_id
FROM crm_dealer d
LEFT JOIN crm_user u ON u.dealer_id = d.id
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
AND d.name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="code != null and code != ''">
AND code LIKE CONCAT('%', #{code}, '%')
AND d.code LIKE CONCAT('%', #{code}, '%')
</if>
<if test="status != null">
AND status = #{status}
AND d.status = #{status}
</if>
</where>
ORDER BY created_at DESC
ORDER BY d.created_at DESC
</select>
<insert id="insert" parameterType="com.bycrm.entity.Dealer" useGeneratedKeys="true" keyProperty="id">
@@ -61,4 +64,8 @@
DELETE FROM crm_dealer WHERE id = #{id}
</delete>
<select id="count" resultType="java.lang.Long">
SELECT COUNT(*) FROM crm_dealer
</select>
</mapper>
@@ -119,4 +119,23 @@
</foreach>
</update>
<select id="countByDealerId" resultType="java.lang.Long">
SELECT COUNT(*)
FROM crm_report
<where>
<if test="dealerId != null">
AND dealer_id = #{dealerId}
</if>
</where>
</select>
<select id="countPendingByDealerId" resultType="java.lang.Long">
SELECT COUNT(*)
FROM crm_report
WHERE status = 0
<if test="dealerId != null">
AND dealer_id = #{dealerId}
</if>
</select>
</mapper>
@@ -0,0 +1,59 @@
<?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.bycrm.mapper.SchoolMapper">
<resultMap id="BaseResultMap" type="com.bycrm.entity.School">
<id column="id" property="id"/>
<result column="school_code" property="schoolCode"/>
<result column="school_name" property="schoolName"/>
<result column="location" property="location"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<insert id="insert" parameterType="com.bycrm.entity.School" useGeneratedKeys="true" keyProperty="id">
INSERT INTO crm_school (school_code, school_name, location, created_at, updated_at)
VALUES (#{schoolCode}, #{schoolName}, #{location}, #{createdAt}, #{updatedAt})
</insert>
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO crm_school (school_code, school_name, location, created_at, updated_at)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.schoolCode}, #{item.schoolName}, #{item.location}, #{item.createdAt}, #{item.updatedAt})
</foreach>
</insert>
<select id="searchByName" resultMap="BaseResultMap">
SELECT id, school_code, school_name, location, created_at, updated_at
FROM crm_school
WHERE school_name LIKE CONCAT('%', #{keyword}, '%')
ORDER BY school_name
LIMIT 50
</select>
<select id="findByCode" resultMap="BaseResultMap">
SELECT id, school_code, school_name, location, created_at, updated_at
FROM crm_school
WHERE school_code = #{schoolCode}
</select>
<select id="findAll" resultMap="BaseResultMap">
SELECT id, school_code, school_name, location, created_at, updated_at
FROM crm_school
ORDER BY school_name
</select>
<select id="findByPage" resultMap="BaseResultMap">
SELECT id, school_code, school_name, location, created_at, updated_at
FROM crm_school
ORDER BY school_name
LIMIT #{offset}, #{limit}
</select>
<select id="count" resultType="int">
SELECT COUNT(*) FROM crm_school
</select>
</mapper>
@@ -0,0 +1,46 @@
<?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.bycrm.mapper.SystemConfigMapper">
<resultMap id="BaseResultMap" type="com.bycrm.entity.SystemConfig">
<id column="id" property="id"/>
<result column="config_key" property="configKey"/>
<result column="config_value" property="configValue"/>
<result column="config_label" property="configLabel"/>
<result column="config_type" property="configType"/>
<result column="description" property="description"/>
<result column="is_editable" property="isEditable"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<select id="selectAll" resultMap="BaseResultMap">
SELECT * FROM crm_system_config ORDER BY id
</select>
<select id="selectByKey" resultMap="BaseResultMap">
SELECT * FROM crm_system_config WHERE config_key = #{configKey}
</select>
<select id="selectValueByKey" resultType="java.lang.String">
SELECT config_value FROM crm_system_config WHERE config_key = #{configKey}
</select>
<update id="update" parameterType="com.bycrm.entity.SystemConfig">
UPDATE crm_system_config
SET config_value = #{configValue},
updated_at = NOW()
WHERE config_key = #{configKey}
</update>
<update id="batchUpdate">
<foreach collection="configs" item="config" separator=";">
UPDATE crm_system_config
SET config_value = #{config.configValue},
updated_at = NOW()
WHERE config_key = #{config.configKey}
</foreach>
</update>
</mapper>