初始化项目

This commit is contained in:
wanglongjie 2026-01-26 16:01:15 +08:00
parent 45a1702948
commit 934b8aad40
155 changed files with 2649 additions and 68 deletions

View File

@ -26,6 +26,7 @@
<swagger.version>3.0.0</swagger.version> <swagger.version>3.0.0</swagger.version>
<commons-lang3.version>3.12.0</commons-lang3.version> <commons-lang3.version>3.12.0</commons-lang3.version>
<hutool.version>5.8.23</hutool.version> <hutool.version>5.8.23</hutool.version>
<poi.version>5.2.5</poi.version>
</properties> </properties>
<dependencies> <dependencies>
@ -50,9 +51,9 @@
<!-- MySQL Driver --> <!-- MySQL Driver -->
<dependency> <dependency>
<groupId>mysql</groupId> <groupId>com.mysql</groupId>
<artifactId>mysql-connector-java</artifactId> <artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version> <scope>runtime</scope>
</dependency> </dependency>
<!-- Druid 数据源 --> <!-- Druid 数据源 -->
@ -101,6 +102,24 @@
<version>${hutool.version}</version> <version>${hutool.version}</version>
</dependency> </dependency>
<!-- Apache POI for Excel processing -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<!-- Spring Security Crypto (密码加密) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- Lombok --> <!-- Lombok -->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
@ -160,7 +179,6 @@
<version>3.3.0</version> <version>3.3.0</version>
<configuration> <configuration>
<configLocation>checkstyle.xml</configLocation> <configLocation>checkstyle.xml</configLocation>
<encoding>UTF-8</encoding>
<consoleOutput>true</consoleOutput> <consoleOutput>true</consoleOutput>
<failsOnError>false</failsOnError> <failsOnError>false</failsOnError>
</configuration> </configuration>

View File

@ -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;
} }

View File

@ -1,7 +1,9 @@
package com.bycrm.controller; package com.bycrm.controller;
import com.bycrm.common.Result; import com.bycrm.common.Result;
import com.bycrm.dto.ChangePasswordDTO;
import com.bycrm.dto.LoginDTO; import com.bycrm.dto.LoginDTO;
import com.bycrm.dto.ResetPasswordDTO;
import com.bycrm.service.UserService; import com.bycrm.service.UserService;
import com.bycrm.vo.UserInfoVO; import com.bycrm.vo.UserInfoVO;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
@ -54,6 +56,30 @@ public class AuthController {
return Result.success(); 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 * 从请求中获取 Token
*/ */

View File

@ -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);
}
}

View File

@ -32,7 +32,7 @@ public class DealerController {
@GetMapping("/list") @GetMapping("/list")
public Result<List<Dealer>> getDealerList( public Result<List<Dealer>> getDealerList(
@ApiParam("经销商名称") @RequestParam(required = false) String name, @ApiParam("经销商名称") @RequestParam(required = false) String name,
@ApiParam("经销商编码") @RequestParam(required = false) String code, @ApiParam("经销商账号") @RequestParam(required = false) String code,
@ApiParam("状态") @RequestParam(required = false) Integer status) { @ApiParam("状态") @RequestParam(required = false) Integer status) {
List<Dealer> dealers = dealerService.getDealerList(name, code, status); List<Dealer> dealers = dealerService.getDealerList(name, code, status);
return Result.success(dealers); return Result.success(dealers);

View File

@ -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());
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -23,7 +23,7 @@ public class DealerDTO implements Serializable {
/** /**
* 经销商编码 * 经销商编码
*/ */
@NotBlank(message = "经销商编码不能为空") @NotBlank(message = "经销商账号不能为空")
private String code; private String code;
/** /**
@ -48,4 +48,9 @@ public class DealerDTO implements Serializable {
*/ */
@NotNull(message = "状态不能为空") @NotNull(message = "状态不能为空")
private Integer status; private Integer status;
/**
* 初始密码新增用户时可设置
*/
private String password;
} }

View File

@ -22,6 +22,11 @@ public class PageQuery implements Serializable {
*/ */
private Long size = 10L; private Long size = 10L;
/**
* 偏移量自动计算不需要手动设置
*/
private Long offset;
/** /**
* 排序字段 * 排序字段
*/ */

View File

@ -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;
}

View File

@ -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;
}

View File

@ -49,6 +49,11 @@ public class Dealer implements Serializable {
*/ */
private Integer status; private Integer status;
/**
* 关联的用户ID用于重置密码等操作
*/
private Long userId;
/** /**
* 创建时间 * 创建时间
*/ */

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
@ -45,16 +46,16 @@ public class Report implements Serializable {
private String rejectReason; private String rejectReason;
/** /**
* 保护期开始时间 * 保护期开始日期
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd")
private LocalDateTime protectStartDate; private LocalDate protectStartDate;
/** /**
* 保护期结束时间 * 保护期结束日期
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd")
private LocalDateTime protectEndDate; private LocalDate protectEndDate;
/** /**
* 创建时间 * 创建时间

View File

@ -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;
}

View File

@ -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;
}

View File

@ -2,6 +2,7 @@ package com.bycrm.mapper;
import com.bycrm.entity.Customer; import com.bycrm.entity.Customer;
import com.bycrm.dto.PageQuery; import com.bycrm.dto.PageQuery;
import com.bycrm.vo.CustomerVO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@ -29,6 +30,12 @@ public interface CustomerMapper {
List<Customer> selectPage(@Param("query") PageQuery query, @Param("name") String name, List<Customer> selectPage(@Param("query") PageQuery query, @Param("name") String name,
@Param("industry") String industry, @Param("status") Integer status); @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);
/** /**
* 查询客户总数 * 查询客户总数
*/ */

View File

@ -41,4 +41,9 @@ public interface DealerMapper {
* 删除经销商 * 删除经销商
*/ */
int deleteById(@Param("id") Long id); int deleteById(@Param("id") Long id);
/**
* 统计经销商总数
*/
Long count();
} }

View File

@ -60,4 +60,14 @@ public interface ReportMapper {
* 批量更新报备状态为已失效 * 批量更新报备状态为已失效
*/ */
int batchUpdateExpired(@Param("ids") List<Long> ids); int batchUpdateExpired(@Param("ids") List<Long> ids);
/**
* 统计报备总数根据经销商ID过滤
*/
Long countByDealerId(@Param("dealerId") Long dealerId);
/**
* 统计待审核报备数量根据经销商ID过滤
*/
Long countPendingByDealerId(@Param("dealerId") Long dealerId);
} }

View File

@ -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();
}

View File

@ -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);
}

View File

@ -41,4 +41,14 @@ public interface ReportService {
* 处理过期报备定时任务调用 * 处理过期报备定时任务调用
*/ */
void handleExpiredReports(); void handleExpiredReports();
/**
* 统计报备总数根据经销商ID过滤
*/
Long countByDealerId(Long dealerId);
/**
* 统计待审核报备数量根据经销商ID过滤
*/
Long countPendingByDealerId(Long dealerId);
} }

View File

@ -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();
}

View File

@ -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();
}

View File

@ -1,6 +1,8 @@
package com.bycrm.service; package com.bycrm.service;
import com.bycrm.dto.ChangePasswordDTO;
import com.bycrm.dto.LoginDTO; import com.bycrm.dto.LoginDTO;
import com.bycrm.dto.ResetPasswordDTO;
import com.bycrm.entity.User; import com.bycrm.entity.User;
import com.bycrm.vo.UserInfoVO; import com.bycrm.vo.UserInfoVO;
@ -28,4 +30,14 @@ public interface UserService {
* 获取当前登录用户信息 * 获取当前登录用户信息
*/ */
UserInfoVO getCurrentUser(String token); UserInfoVO getCurrentUser(String token);
/**
* 修改密码
*/
void changePassword(Long userId, ChangePasswordDTO dto);
/**
* 重置密码管理员功能
*/
void resetPassword(ResetPasswordDTO dto);
} }

View File

@ -1,12 +1,10 @@
package com.bycrm.service.impl; package com.bycrm.service.impl;
import cn.hutool.core.util.StrUtil;
import com.bycrm.common.Constants; import com.bycrm.common.Constants;
import com.bycrm.common.PageResult; import com.bycrm.common.PageResult;
import com.bycrm.dto.CustomerDTO; import com.bycrm.dto.CustomerDTO;
import com.bycrm.dto.PageQuery; import com.bycrm.dto.PageQuery;
import com.bycrm.entity.Customer; import com.bycrm.entity.Customer;
import com.bycrm.entity.User;
import com.bycrm.exception.BusinessException; import com.bycrm.exception.BusinessException;
import com.bycrm.mapper.CustomerMapper; import com.bycrm.mapper.CustomerMapper;
import com.bycrm.mapper.UserMapper; import com.bycrm.mapper.UserMapper;
@ -17,7 +15,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -40,15 +37,11 @@ public class CustomerServiceImpl implements CustomerService {
// 计算偏移量 // 计算偏移量
query.setOffset((query.getCurrent() - 1) * query.getSize()); 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); Long total = customerMapper.countPage(name, industry, status);
List<CustomerVO> voList = customers.stream().map(customer -> { return PageResult.of(total, customerVOs, query.getCurrent(), query.getSize());
CustomerVO vo = convertToVO(customer);
return vo;
}).collect(Collectors.toList());
return PageResult.of(total, voList, query.getCurrent(), query.getSize());
} }
@Override @Override

View File

@ -1,11 +1,15 @@
package com.bycrm.service.impl; package com.bycrm.service.impl;
import com.bycrm.common.Constants;
import com.bycrm.dto.DealerDTO; import com.bycrm.dto.DealerDTO;
import com.bycrm.entity.Dealer; import com.bycrm.entity.Dealer;
import com.bycrm.entity.User;
import com.bycrm.exception.BusinessException; import com.bycrm.exception.BusinessException;
import com.bycrm.mapper.DealerMapper; import com.bycrm.mapper.DealerMapper;
import com.bycrm.mapper.UserMapper;
import com.bycrm.service.DealerService; import com.bycrm.service.DealerService;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -19,9 +23,12 @@ import java.util.List;
public class DealerServiceImpl implements DealerService { public class DealerServiceImpl implements DealerService {
private final DealerMapper dealerMapper; 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.dealerMapper = dealerMapper;
this.userMapper = userMapper;
} }
@Override @Override
@ -44,7 +51,7 @@ public class DealerServiceImpl implements DealerService {
// 检查编码是否重复 // 检查编码是否重复
Dealer existingDealer = dealerMapper.selectByCode(dealerDTO.getCode()); Dealer existingDealer = dealerMapper.selectByCode(dealerDTO.getCode());
if (existingDealer != null) { if (existingDealer != null) {
throw new BusinessException("经销商编码已存在"); throw new BusinessException("经销商账号已存在");
} }
Dealer dealer = new Dealer(); Dealer dealer = new Dealer();
@ -53,6 +60,25 @@ public class DealerServiceImpl implements DealerService {
dealer.setUpdatedAt(LocalDateTime.now()); dealer.setUpdatedAt(LocalDateTime.now());
dealerMapper.insert(dealer); 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 @Override
@ -67,7 +93,7 @@ public class DealerServiceImpl implements DealerService {
if (!existingDealer.getCode().equals(dealerDTO.getCode())) { if (!existingDealer.getCode().equals(dealerDTO.getCode())) {
Dealer codeDealer = dealerMapper.selectByCode(dealerDTO.getCode()); Dealer codeDealer = dealerMapper.selectByCode(dealerDTO.getCode());
if (codeDealer != null && !codeDealer.getId().equals(id)) { if (codeDealer != null && !codeDealer.getId().equals(id)) {
throw new BusinessException("经销商编码已存在"); throw new BusinessException("经销商账号已存在");
} }
} }

View File

@ -13,9 +13,9 @@ import com.bycrm.mapper.CustomerMapper;
import com.bycrm.mapper.ReportMapper; import com.bycrm.mapper.ReportMapper;
import com.bycrm.mapper.UserMapper; import com.bycrm.mapper.UserMapper;
import com.bycrm.service.ReportService; import com.bycrm.service.ReportService;
import com.bycrm.service.SystemConfigService;
import com.bycrm.vo.ReportVO; import com.bycrm.vo.ReportVO;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -34,17 +34,16 @@ public class ReportServiceImpl implements ReportService {
private final ReportMapper reportMapper; private final ReportMapper reportMapper;
private final CustomerMapper customerMapper; private final CustomerMapper customerMapper;
private final UserMapper userMapper; private final UserMapper userMapper;
private final SystemConfigService systemConfigService;
@Value("${crm.report.ttl-days:90}") public ReportServiceImpl(ReportMapper reportMapper,
private Integer protectDays; CustomerMapper customerMapper,
UserMapper userMapper,
@Value("${crm.report.allow-overlap:false}") SystemConfigService systemConfigService) {
private Boolean allowOverlap;
public ReportServiceImpl(ReportMapper reportMapper, CustomerMapper customerMapper, UserMapper userMapper) {
this.reportMapper = reportMapper; this.reportMapper = reportMapper;
this.customerMapper = customerMapper; this.customerMapper = customerMapper;
this.userMapper = userMapper; this.userMapper = userMapper;
this.systemConfigService = systemConfigService;
} }
@Override @Override
@ -95,6 +94,7 @@ public class ReportServiceImpl implements ReportService {
// 防撞单校验检查该客户是否已存在有效报备 // 防撞单校验检查该客户是否已存在有效报备
Report existingReport = reportMapper.selectValidByCustomerId(reportDTO.getCustomerId()); Report existingReport = reportMapper.selectValidByCustomerId(reportDTO.getCustomerId());
Boolean allowOverlap = systemConfigService.getBooleanConfigValue("report.allow.overlap", false);
if (existingReport != null && !allowOverlap) { if (existingReport != null && !allowOverlap) {
throw new BusinessException("该客户已被其他经销商报备,无法重复报备"); throw new BusinessException("该客户已被其他经销商报备,无法重复报备");
} }
@ -134,8 +134,10 @@ public class ReportServiceImpl implements ReportService {
if (auditDTO.getApproved()) { if (auditDTO.getApproved()) {
// 审核通过 // 审核通过
report.setStatus(Constants.REPORT_STATUS_APPROVED); report.setStatus(Constants.REPORT_STATUS_APPROVED);
report.setProtectStartDate(now); LocalDate today = LocalDate.now();
report.setProtectEndDate(now.plusDays(protectDays)); 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()); Customer customer = customerMapper.selectById(report.getCustomerId());
@ -177,7 +179,7 @@ public class ReportServiceImpl implements ReportService {
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void handleExpiredReports() { public void handleExpiredReports() {
// 查询所有保护期已到的报备 // 查询所有保护期已到的报备
String endDate = LocalDateTime.now().toString(); String endDate = LocalDate.now().toString();
List<Report> expiringReports = reportMapper.selectExpiringReports(endDate); List<Report> expiringReports = reportMapper.selectExpiringReports(endDate);
if (expiringReports.isEmpty()) { 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 * 转换为 VO
*/ */
@ -214,7 +226,7 @@ public class ReportServiceImpl implements ReportService {
vo.setStatusDesc("已通过"); vo.setStatusDesc("已通过");
// 计算剩余保护天数 // 计算剩余保护天数
if (report.getProtectEndDate() != null) { 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)); vo.setRemainDays((int) Math.max(0, days));
} }
break; break;

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -1,6 +1,8 @@
package com.bycrm.service.impl; package com.bycrm.service.impl;
import com.bycrm.common.Constants; import com.bycrm.common.Constants;
import com.bycrm.dto.ChangePasswordDTO;
import com.bycrm.dto.ResetPasswordDTO;
import com.bycrm.entity.User; import com.bycrm.entity.User;
import com.bycrm.exception.BusinessException; import com.bycrm.exception.BusinessException;
import com.bycrm.mapper.UserMapper; import com.bycrm.mapper.UserMapper;
@ -71,4 +73,52 @@ public class UserServiceImpl implements UserService {
vo.setRoleDesc(user.getRole() == Constants.USER_ROLE_ADMIN ? "管理员" : "经销商用户"); vo.setRoleDesc(user.getRole() == Constants.USER_ROLE_ADMIN ? "管理员" : "经销商用户");
return vo; 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);
}
} }

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
@ -60,10 +61,10 @@ public class CustomerVO implements Serializable {
private String currentDealerName; private String currentDealerName;
/** /**
* 保护期结束时间 * 保护期结束日期
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd")
private LocalDateTime protectEndDate; private LocalDate protectEndDate;
/** /**
* 创建时间 * 创建时间

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
@ -65,16 +66,16 @@ public class ReportVO implements Serializable {
private String rejectReason; private String rejectReason;
/** /**
* 保护期开始时间 * 保护期开始日期
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd")
private LocalDateTime protectStartDate; private LocalDate protectStartDate;
/** /**
* 保护期结束时间 * 保护期结束日期
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd")
private LocalDateTime protectEndDate; private LocalDate protectEndDate;
/** /**
* 剩余保护天数 * 剩余保护天数

View File

@ -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;
}

View File

@ -9,9 +9,9 @@ spring:
datasource: datasource:
type: com.alibaba.druid.pool.DruidDataSource type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver 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 username: root
password: root password: "Boyun@123"
druid: druid:
initial-size: 5 initial-size: 5
min-idle: 5 min-idle: 5
@ -39,6 +39,10 @@ spring:
date-format: yyyy-MM-dd HH:mm:ss date-format: yyyy-MM-dd HH:mm:ss
default-property-inclusion: non_null default-property-inclusion: non_null
mvc:
pathmatch:
matching-strategy: ant_path_matcher
# MyBatis 配置 # MyBatis 配置
mybatis: mybatis:
mapper-locations: classpath:mapper/*.xml mapper-locations: classpath:mapper/*.xml
@ -50,7 +54,9 @@ mybatis:
# JWT 配置 # JWT 配置
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小时单位毫秒 expiration: 86400000 # 24小时单位毫秒
# CRM 业务配置 # CRM 业务配置

View File

@ -14,6 +14,13 @@
<result column="updated_at" property="updatedAt"/> <result column="updated_at" property="updatedAt"/>
</resultMap> </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 id="selectById" resultMap="BaseResultMap">
SELECT * FROM crm_customer WHERE id = #{id} SELECT * FROM crm_customer WHERE id = #{id}
</select> </select>
@ -77,4 +84,36 @@
DELETE FROM crm_customer WHERE id = #{id} DELETE FROM crm_customer WHERE id = #{id}
</delete> </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> </mapper>

View File

@ -13,6 +13,7 @@
<result column="status" property="status"/> <result column="status" property="status"/>
<result column="created_at" property="createdAt"/> <result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/> <result column="updated_at" property="updatedAt"/>
<result column="user_id" property="userId"/>
</resultMap> </resultMap>
<select id="selectById" resultMap="BaseResultMap"> <select id="selectById" resultMap="BaseResultMap">
@ -24,19 +25,21 @@
</select> </select>
<select id="selectList" resultMap="BaseResultMap"> <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> <where>
<if test="name != null and name != ''"> <if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%') AND d.name LIKE CONCAT('%', #{name}, '%')
</if> </if>
<if test="code != null and code != ''"> <if test="code != null and code != ''">
AND code LIKE CONCAT('%', #{code}, '%') AND d.code LIKE CONCAT('%', #{code}, '%')
</if> </if>
<if test="status != null"> <if test="status != null">
AND status = #{status} AND d.status = #{status}
</if> </if>
</where> </where>
ORDER BY created_at DESC ORDER BY d.created_at DESC
</select> </select>
<insert id="insert" parameterType="com.bycrm.entity.Dealer" useGeneratedKeys="true" keyProperty="id"> <insert id="insert" parameterType="com.bycrm.entity.Dealer" useGeneratedKeys="true" keyProperty="id">
@ -61,4 +64,8 @@
DELETE FROM crm_dealer WHERE id = #{id} DELETE FROM crm_dealer WHERE id = #{id}
</delete> </delete>
<select id="count" resultType="java.lang.Long">
SELECT COUNT(*) FROM crm_dealer
</select>
</mapper> </mapper>

View File

@ -119,4 +119,23 @@
</foreach> </foreach>
</update> </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> </mapper>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,81 @@
server:
port: 8080
servlet:
context-path: /api
spring:
application:
name: by-crm
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.3.80:3306/by_crm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: "Boyun@123"
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
filters: stat,wall
web-stat-filter:
enabled: true
url-pattern: /*
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
login-username: admin
login-password: admin123
jackson:
time-zone: GMT+8
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
type-aliases-package: com.bycrm.entity
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# JWT 配置
jwt:
# 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 业务配置
crm:
report:
ttl-days: 90 # 保护期天数
allow-overlap: false # 是否允许重叠报备(生产环境必须为 false
# Swagger 配置
springfox:
documentation:
swagger-ui:
enabled: true
enabled: true
# 日志配置
logging:
level:
com.bycrm.mapper: debug
org.springframework.web: info
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More