Compare commits

...
12 Commits
Author SHA1 Message Date
Timi 4c162dad34 v0.0.18
CI / build-deploy (pull_request) Successful in 20s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-03 19:59:08 +08:00
Timi c49686c411 v0.0.18
CI / build-deploy (pull_request) Failing after 8s
CI / notify-on-failure (pull_request) Successful in 0s
2026-08-03 19:57:44 +08:00
Timi 65161a7fad fix customer update 2026-08-03 19:21:12 +08:00
Timi 187f270866 add customer lunar birthdate 2026-08-03 18:54:17 +08:00
Timi 50d4bba045 batch customer event 2026-08-03 17:30:33 +08:00
Timi 6eedc06a07 update GAO logger 2026-08-03 15:34:01 +08:00
Timi b55b43eba4 add customer/event record export permission 2026-08-03 13:00:40 +08:00
Timi 20289a3168 add GAO logger 2026-08-03 12:22:48 +08:00
Timi d45bf11477 add GaoRegistrationEvent status and valid date range 2026-08-03 11:41:33 +08:00
Timi 7370b2fc98 rename registration event limit type 2026-08-03 10:48:43 +08:00
Timi 2f366fe219 v1.0.7
CI / build-deploy (pull_request) Successful in 20s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-01 17:34:06 +08:00
Timi f1715a3e32 v1.0.6
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-01 17:03:28 +08:00
33 changed files with 1140 additions and 198 deletions
+6 -2
View File
@@ -11,7 +11,7 @@
<groupId>com.imyeyu.timiserverapi</groupId> <groupId>com.imyeyu.timiserverapi</groupId>
<artifactId>TimiServerAPI</artifactId> <artifactId>TimiServerAPI</artifactId>
<version>1.0.3</version> <version>1.0.6</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>TimiServerAPI</name> <name>TimiServerAPI</name>
<description>imyeyu.com API</description> <description>imyeyu.com API</description>
@@ -173,7 +173,7 @@
<dependency> <dependency>
<groupId>com.imyeyu.spring</groupId> <groupId>com.imyeyu.spring</groupId>
<artifactId>timi-spring</artifactId> <artifactId>timi-spring</artifactId>
<version>0.0.17</version> <version>0.0.18</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.imyeyu.network</groupId> <groupId>com.imyeyu.network</groupId>
@@ -230,6 +230,10 @@
<artifactId>spring-boot-starter-freemarker</artifactId> <artifactId>spring-boot-starter-freemarker</artifactId>
<version>${springboot.version}</version> <version>${springboot.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId> <artifactId>spring-boot-starter-data-mongodb</artifactId>
@@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.imyeyu.utils.Time; import com.imyeyu.utils.Time;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -22,6 +23,7 @@ public class BeanConfig {
@Bean @Bean
public ObjectMapper jackson() { public ObjectMapper jackson() {
return JsonMapper.builder() return JsonMapper.builder()
.addModule(new JavaTimeModule())
.serializationInclusion(JsonInclude.Include.NON_NULL) .serializationInclusion(JsonInclude.Include.NON_NULL)
.defaultDateFormat(Time.dateTime) .defaultDateFormat(Time.dateTime)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.imyeyu.api.annotation.EnableSettingInterceptor; import com.imyeyu.api.annotation.EnableSettingInterceptor;
import com.imyeyu.api.annotation.RequestRateLimitInterceptor; import com.imyeyu.api.annotation.RequestRateLimitInterceptor;
import com.imyeyu.api.annotation.RequireCorePermissionInterceptor; import com.imyeyu.api.annotation.RequireCorePermissionInterceptor;
@@ -78,6 +79,7 @@ public class WebConfig implements WebMvcConfigurer {
@Override @Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
JsonMapper jsonMapper = JsonMapper.builder() JsonMapper jsonMapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
// 设置默认属性包含规则:忽略 null 值 // 设置默认属性包含规则:忽略 null 值
.serializationInclusion(JsonInclude.Include.NON_NULL) .serializationInclusion(JsonInclude.Include.NON_NULL)
// 日期格式化 // 日期格式化
@@ -46,8 +46,8 @@ public class TempFileServiceImplement implements TempFileService {
public List<TempFileResp> create(List<MultipartFile> files, Long ttl) throws TimiException { public List<TempFileResp> create(List<MultipartFile> files, Long ttl) throws TimiException {
Logger logger = new Logger(); Logger logger = new Logger();
logger.setModule("TEMP_FILE"); logger.setModule(Logger.Module.COMMON);
logger.setOperation("STORE"); logger.setOperation("TEMP_FILE_STORE");
logger.setIp(TimiSpring.getRequestIP()); logger.setIp(TimiSpring.getRequestIP());
try { try {
logger.setContent(jackson.writeValueAsString(logRequestArgs(files, ttl))); logger.setContent(jackson.writeValueAsString(logRequestArgs(files, ttl)));
@@ -116,8 +116,8 @@ public class TempFileServiceImplement implements TempFileService {
@Override @Override
public void save(TempFileSaveReq request) { public void save(TempFileSaveReq request) {
Logger logger = new Logger(); Logger logger = new Logger();
logger.setModule("TEMP_FILE"); logger.setModule(Logger.Module.COMMON);
logger.setOperation("SAVE"); logger.setOperation("TEMP_FILE_SAVE");
logger.setIp(TimiSpring.getRequestIP()); logger.setIp(TimiSpring.getRequestIP());
try { try {
logger.setContent(jackson.writeValueAsString(request)); logger.setContent(jackson.writeValueAsString(request));
@@ -0,0 +1,29 @@
package com.imyeyu.api.modules.forevermc.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 服务器接口
*
* @author 夜雨
* @since 2024-08-06 17:20
*/
@Slf4j
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/fmc/server")
public class ServerController {
/**
* 服务器状态报告,由 FMCServer 服务端插件发送
*/
@PostMapping("/report")
public void report() {
}
}
@@ -14,6 +14,8 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
CUSTOMER_READ("CUSTOMER:READ", "客户读取"), CUSTOMER_READ("CUSTOMER:READ", "客户读取"),
CUSTOMER_EXPORT("CUSTOMER:EXPORT", "客户导出", true),
CUSTOMER_UPDATE("CUSTOMER:UPDATE", "客户修改"), CUSTOMER_UPDATE("CUSTOMER:UPDATE", "客户修改"),
CUSTOMER_DELETE("CUSTOMER:DELETE", "客户删除"), CUSTOMER_DELETE("CUSTOMER:DELETE", "客户删除"),
@@ -30,6 +32,8 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
REGISTRATION_EVENT_RECORD_READ("REGISTRATION_EVENT_RECORD:READ", "登记事件记录读取"), REGISTRATION_EVENT_RECORD_READ("REGISTRATION_EVENT_RECORD:READ", "登记事件记录读取"),
REGISTRATION_EVENT_RECORD_EXPORT("REGISTRATION_EVENT_RECORD:EXPORT", "登记事件记录导出", true),
REGISTRATION_EVENT_RECORD_UPDATE("REGISTRATION_EVENT_RECORD:UPDATE", "登记事件记录修改"), REGISTRATION_EVENT_RECORD_UPDATE("REGISTRATION_EVENT_RECORD:UPDATE", "登记事件记录修改"),
REGISTRATION_EVENT_RECORD_DELETE("REGISTRATION_EVENT_RECORD:DELETE", "登记事件记录删除"), REGISTRATION_EVENT_RECORD_DELETE("REGISTRATION_EVENT_RECORD:DELETE", "登记事件记录删除"),
@@ -88,7 +88,7 @@ public class GaoCustomerController {
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@IgnoreGlobalReturn @IgnoreGlobalReturn
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) @RequireGaoPermission(GaoPermissionCode.CUSTOMER_EXPORT)
@PostMapping("/export") @PostMapping("/export")
public void export(@RequestBody GaoCustomerSearchReq req, HttpServletResponse resp) throws IOException { public void export(@RequestBody GaoCustomerSearchReq req, HttpServletResponse resp) throws IOException {
String fileName = URLEncoder.encode("客户列表.xlsx", "UTF-8").replace("+", "%20"); String fileName = URLEncoder.encode("客户列表.xlsx", "UTF-8").replace("+", "%20");
@@ -72,6 +72,14 @@ public class GaoRegistrationEventController {
service.update(registrationEvent); service.update(registrationEvent);
} }
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_UPDATE)
@PostMapping("/sort")
public void sort(@RequestBody List<String> idList) {
service.sort(idList);
}
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_DELETE) @RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_DELETE)
@@ -10,6 +10,7 @@ import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventRecordService; import com.imyeyu.api.modules.gao.service.GaoRegistrationEventRecordService;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegisterStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRegisterStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegistrationRankView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView;
@@ -79,8 +80,8 @@ public class GaoRegistrationEventRecordController {
@RequestRateLimit @RequestRateLimit
@PostMapping("/create") @PostMapping("/create")
@JsonView(ResponseView.Public.class) @JsonView(ResponseView.Public.class)
public GaoRegistrationEventRecord create(@RequestBody GaoRegistrationEventRecordCreateReq req) { public List<GaoRegistrationEventRecord> create(@RequestBody GaoRegistrationEventRecordCreateReq req) {
return service.createRecord(req); return service.createRecords(req);
} }
@AOPLog @AOPLog
@@ -128,7 +129,7 @@ public class GaoRegistrationEventRecordController {
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@IgnoreGlobalReturn @IgnoreGlobalReturn
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ) @RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_EXPORT)
@PostMapping("/range/export") @PostMapping("/range/export")
public void rangeExport(@RequestBody GaoRegistrationEventRecordRangeReq req, HttpServletResponse resp) throws IOException { public void rangeExport(@RequestBody GaoRegistrationEventRecordRangeReq req, HttpServletResponse resp) throws IOException {
String fileName = URLEncoder.encode("登记事件记录.xlsx", "UTF-8").replace("+", "%20"); String fileName = URLEncoder.encode("登记事件记录.xlsx", "UTF-8").replace("+", "%20");
@@ -171,6 +172,15 @@ public class GaoRegistrationEventRecordController {
return service.statAllCustomerByRange(req); return service.statAllCustomerByRange(req);
} }
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/registration-rank")
/// 按时间范围查询客户登记排行榜
public List<GaoCustomerRegistrationRankView> customerRegistrationRank(@RequestBody(required = false) GaoRegistrationEventRecordRangeReq req) {
return service.listCustomerRegistrationRank(req);
}
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ) @RequireGaoPermission(GaoPermissionCode.STAT_READ)
@@ -1,5 +1,7 @@
package com.imyeyu.api.modules.gao.entity; package com.imyeyu.api.modules.gao.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.entity.User;
@@ -9,6 +11,7 @@ import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.time.LocalDate;
import java.util.List; import java.util.List;
/// ///
@@ -30,6 +33,18 @@ public class GaoCustomer extends UUIDEntity {
PHOTO PHOTO
} }
///
/// 出生日期使用的历法
///
public enum BirthdateCalendar {
/// 公历
SOLAR,
/// 农历
LUNAR
}
/// 客户编码 /// 客户编码
private String code; private String code;
@@ -42,6 +57,22 @@ public class GaoCustomer extends UUIDEntity {
/// 年龄 /// 年龄
private Integer age; private Integer age;
/// 出生日期,数据库只保存公历日期
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate birthdate;
/// 出生日期录入时使用的历法
private BirthdateCalendar birthdateCalendar;
@JsonIgnore
@Transient
private boolean birthdateProvided;
public void setBirthdate(LocalDate birthdate) {
this.birthdate = birthdate;
this.birthdateProvided = true;
}
/// 电话 /// 电话
private String telephone; private String telephone;
@@ -16,18 +16,82 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class GaoRegistrationEvent extends UUIDEntity { public class GaoRegistrationEvent extends UUIDEntity {
///
/// 登记事件状态
///
/// @author Codex
/// @since 2026-08-01
public enum Status {
/// 草稿,未发布不可登记
DRAFT,
/// 可登记
ACTIVE,
/// 无效,不可登记但保留统计
INACTIVE,
/// 已删除,不可登记但保留统计
DELETED
}
///
/// 登记限制类型
///
/// @author Codex
/// @since 2026-08-01
public enum LimitType {
/// 不限制
NONE,
/// 每天上午、下午各一次
DAILY_HALF_DAY,
/// 每个自然日一次
NATURAL_DAY,
/// 每隔 N 小时一次
INTERVAL_HOUR,
/// 每个自然月一次
NATURAL_MONTH,
/// 事件总登记次数上限
TOTAL_LIMIT,
/// 单个客户总登记次数上限
CUSTOMER_TOTAL_LIMIT
}
/// 登记事件名称 /// 登记事件名称
private String name; private String name;
/// 说明 /// 说明
private String description; private String description;
/// true 为启用 /// 统计图表颜色
private Boolean enabled; private String color;
/// 状态
private Status status;
/// true 为登记时要求拍照 /// true 为登记时要求拍照
private Boolean requirePhoto; private Boolean requirePhoto;
/// 登记限制类型
private LimitType limitType;
/// 登记限制数值,按限制类型表示小时数或次数
private Integer limitValue;
/// 可登记起始时间戳,毫秒,空为不限制
private Long beginAt;
/// 可登记终止时间戳,毫秒,空为不限制
private Long endAt;
/// 排序值 /// 排序值
private Integer sort; private Integer sort;
@@ -3,7 +3,6 @@ package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent; import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List; import java.util.List;
@@ -16,6 +15,5 @@ public interface GaoRegistrationEventMapper extends BaseMapper<GaoRegistrationEv
List<GaoRegistrationEvent> selectByIdList(@Param("idList") List<String> idList); List<GaoRegistrationEvent> selectByIdList(@Param("idList") List<String> idList);
@Select("SELECT * FROM `gao_registration_event` WHERE `enabled` = TRUE AND `deleted_at` IS NULL ORDER BY `sort` ASC, `created_at` ASC") List<GaoRegistrationEvent> selectEnabledList(@Param("now") Long now);
List<GaoRegistrationEvent> selectEnabledList();
} }
@@ -3,6 +3,7 @@ package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord; import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegisterStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRegisterStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegistrationDayStatView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordListItemView; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordListItemView;
@@ -27,6 +28,28 @@ public interface GaoRegistrationEventRecordMapper extends BaseMapper<GaoRegistra
/// @return 最后登记事件记录列表 /// @return 最后登记事件记录列表
List<GaoRegistrationEventRecord> selectByRegistrationEventAndPeriod(@Param("registrationEventId") String registrationEventId, @Param("periodType") GaoRegistrationEventRecordPeriodReq.PeriodType periodType, @Param("periodValue") Integer periodValue); List<GaoRegistrationEventRecord> selectByRegistrationEventAndPeriod(@Param("registrationEventId") String registrationEventId, @Param("periodType") GaoRegistrationEventRecordPeriodReq.PeriodType periodType, @Param("periodValue") Integer periodValue);
/// 统计客户指定登记事件在时间范围内的登记数
///
/// @param customerId 客户 ID
/// @param registrationEventId 登记事件 ID
/// @param beginRegisteredAt 开始登记时间
/// @param endRegisteredAt 结束登记时间
/// @return 登记数
Long countByCustomerIdAndRegistrationEventIdRange(@Param("customerId") String customerId, @Param("registrationEventId") String registrationEventId, @Param("beginRegisteredAt") Long beginRegisteredAt, @Param("endRegisteredAt") Long endRegisteredAt);
/// 统计客户指定登记事件总登记数
///
/// @param customerId 客户 ID
/// @param registrationEventId 登记事件 ID
/// @return 登记数
Long countByCustomerIdAndRegistrationEventId(@Param("customerId") String customerId, @Param("registrationEventId") String registrationEventId);
/// 统计登记事件总登记数
///
/// @param registrationEventId 登记事件 ID
/// @return 登记数
Long countByRegistrationEventId(@Param("registrationEventId") String registrationEventId);
/// 按周期统计登记事件记录 /// 按周期统计登记事件记录
/// ///
/// @param registrationEventId 登记事件 ID /// @param registrationEventId 登记事件 ID
@@ -55,6 +78,12 @@ public interface GaoRegistrationEventRecordMapper extends BaseMapper<GaoRegistra
/// @return 统计结果 /// @return 统计结果
List<GaoCustomerRegisterStatView> statAllCustomerByRange(@Param("beginRegisteredAt") Long beginRegisteredAt, @Param("endRegisteredAt") Long endRegisteredAt); List<GaoCustomerRegisterStatView> statAllCustomerByRange(@Param("beginRegisteredAt") Long beginRegisteredAt, @Param("endRegisteredAt") Long endRegisteredAt);
/// 按时间范围统计全部客户每日登记
///
/// @param req 范围查询请求
/// @return 每个有登记客户的每日统计
List<GaoCustomerRegistrationDayStatView> statCustomerRegistrationDay(@Param("req") GaoRegistrationEventRecordRangeReq req);
/// 统计单个客户单个登记事件的每日登记事件记录数 /// 统计单个客户单个登记事件的每日登记事件记录数
/// ///
/// @param customerId 客户 ID /// @param customerId 客户 ID
@@ -3,6 +3,7 @@ package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord; import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegisterStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRegisterStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegistrationRankView;
import com.imyeyu.api.modules.gao.vo.GaoQuickRegistrationEventRecordReq; import com.imyeyu.api.modules.gao.vo.GaoQuickRegistrationEventRecordReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq;
@@ -26,11 +27,11 @@ import java.util.List;
/// @since 2026-07-27 /// @since 2026-07-27
public interface GaoRegistrationEventRecordService extends BaseService<GaoRegistrationEventRecord, String> { public interface GaoRegistrationEventRecordService extends BaseService<GaoRegistrationEventRecord, String> {
/// 创建登记事件记录 /// 批量创建登记事件记录
/// ///
/// @param req 创建请求 /// @param req 批量创建请求
/// @return 创建后的登记事件记录 /// @return 创建后的登记事件记录列表
GaoRegistrationEventRecord createRecord(GaoRegistrationEventRecordCreateReq req); List<GaoRegistrationEventRecord> createRecords(GaoRegistrationEventRecordCreateReq req);
/// 快速录入客户并创建登记事件记录 /// 快速录入客户并创建登记事件记录
/// ///
@@ -100,6 +101,12 @@ public interface GaoRegistrationEventRecordService extends BaseService<GaoRegist
/// @return 统计结果 /// @return 统计结果
List<GaoCustomerRegisterStatView> statAllCustomerByRange(GaoRegistrationEventRecordStatReq req); List<GaoCustomerRegisterStatView> statAllCustomerByRange(GaoRegistrationEventRecordStatReq req);
/// 按时间范围查询客户登记排行榜
///
/// @param req 范围查询请求,不传时间为全部时间
/// @return 客户登记排行榜
List<GaoCustomerRegistrationRankView> listCustomerRegistrationRank(GaoRegistrationEventRecordRangeReq req);
/// 按时间段统计单个客户单个登记事件每日趋势 /// 按时间段统计单个客户单个登记事件每日趋势
/// ///
/// @param req 趋势统计请求 /// @param req 趋势统计请求
@@ -30,4 +30,9 @@ public interface GaoRegistrationEventService extends BaseService<GaoRegistration
/// @param registrationEventId 登记事件 ID /// @param registrationEventId 登记事件 ID
/// @return 角色代码列表 /// @return 角色代码列表
List<String> listRoleCode(String registrationEventId); List<String> listRoleCode(String registrationEventId);
/// 按客户端提交的顺序更新登记事件排序
///
/// @param idList 登记事件 ID 列表
void sort(List<String> idList);
} }
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.gao.service.implement; package com.imyeyu.api.modules.gao.service.implement;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
@@ -7,21 +8,25 @@ import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq;
import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.mapper.GaoCustomerMapper; import com.imyeyu.api.modules.gao.mapper.GaoCustomerMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerInviteUpdateReq; import com.imyeyu.api.modules.gao.vo.GaoCustomerInviteUpdateReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerSearchReq; import com.imyeyu.api.modules.gao.vo.GaoCustomerSearchReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStatReq; import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStatReq;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode; import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Row;
@@ -35,6 +40,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.Period;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
@@ -50,6 +56,7 @@ import java.util.stream.Collectors;
/// ///
/// @author Codex /// @author Codex
/// @since 2026-07-27 /// @since 2026-07-27
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustomer, String> implements GaoCustomerService { public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustomer, String> implements GaoCustomerService {
@@ -58,7 +65,9 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()); private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
private final GaoCustomerMapper mapper; private final GaoCustomerMapper mapper;
private final ObjectMapper jackson;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final LoggerService loggerService;
private final UserLoginService userLoginService; private final UserLoginService userLoginService;
private final UserService userService; private final UserService userService;
@@ -67,30 +76,96 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
return mapper; return mapper;
} }
@Override
public PageResult<GaoCustomer> page(Page<GaoCustomer> page) {
PageResult<GaoCustomer> result = super.page(page);
loadListTransient(result.getList());
return result;
}
@Override @Override
public void create(GaoCustomer entity) { public void create(GaoCustomer entity) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_CUSTOMER_CREATE");
try {
logger.setContent(jackson.writeValueAsString(entity));
checkEntity(entity, false); checkEntity(entity, false);
entity.setCreatorUserId(userLoginService.getRequireLoginUserId()); entity.setCreatorUserId(userLoginService.getRequireLoginUserId());
entity.setInvitedCount(0L);
super.create(entity); super.create(entity);
saveAttachment(entity.getId(), resolveAttachmentList(entity)); saveAttachment(entity.getId(), resolveAttachmentList(entity));
changeInvitedCount(entity.getIntroducerCustomerId(), 1L); changeInvitedCount(entity.getIntroducerCustomerId(), 1L);
loadDetailTransient(entity); loadDetailTransient(entity);
logger.setLevel(Logger.Level.INFO);
logger.setResult(entity.getId());
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("新增 GAO 客户失败", e);
throw new TimiException(TimiCode.ERROR, "新增 GAO 客户失败", e);
} finally {
loggerService.create(logger);
}
} }
@Override @Override
public void update(GaoCustomer entity) { public void update(GaoCustomer entity) {
GaoCustomer dbCustomer = checkEntity(entity, true); Logger logger = new Logger(Logger.Module.GAO, "GAO_CUSTOMER_UPDATE");
super.update(entity); try {
logger.setContent(jackson.writeValueAsString(entity));
TimiException.required(entity, "not found customer");
TimiException.required(entity.getId(), "not found customer.id");
GaoCustomer dbCustomer = super.get(entity.getId());
if (!entity.isBirthdateProvided()) {
entity.setBirthdate(dbCustomer.getBirthdate());
entity.setBirthdateCalendar(dbCustomer.getBirthdateCalendar());
}
checkEntity(entity, true);
mapper.update(entity);
saveAttachment(entity.getId(), resolveAttachmentList(entity)); saveAttachment(entity.getId(), resolveAttachmentList(entity));
syncIntroducerInvitedCount(dbCustomer.getIntroducerCustomerId(), entity.getIntroducerCustomerId()); syncIntroducerInvitedCount(dbCustomer.getIntroducerCustomerId(), entity.getIntroducerCustomerId());
logger.setLevel(Logger.Level.INFO);
logger.setResult(entity.getId());
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("修改 GAO 客户失败", e);
throw new TimiException(TimiCode.ERROR, "修改 GAO 客户失败", e);
} finally {
loggerService.create(logger);
}
} }
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void delete(String id) { public void delete(String id) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_CUSTOMER_DELETE");
try {
logger.setContent(id);
GaoCustomer customer = super.get(id); GaoCustomer customer = super.get(id);
super.delete(id); super.delete(id);
changeInvitedCount(customer.getIntroducerCustomerId(), -1L); changeInvitedCount(customer.getIntroducerCustomerId(), -1L);
logger.setLevel(Logger.Level.INFO);
logger.setResult(id);
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("删除 GAO 客户失败", e);
throw new TimiException(TimiCode.ERROR, "删除 GAO 客户失败", e);
} finally {
loggerService.create(logger);
}
} }
@Override @Override
@@ -176,13 +251,15 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
appendRow(sheet, 1, null, "生成时间", EXPORT_TIME_FORMATTER.format(Instant.now())); appendRow(sheet, 1, null, "生成时间", EXPORT_TIME_FORMATTER.format(Instant.now()));
appendRow(sheet, 2, null, "关键词", req == null || req.getKeyword() == null || req.getKeyword().isBlank() ? "全部" : req.getKeyword().trim()); appendRow(sheet, 2, null, "关键词", req == null || req.getKeyword() == null || req.getKeyword().isBlank() ? "全部" : req.getKeyword().trim());
appendRow(sheet, 3, null, "记录总数", String.valueOf(list.size())); appendRow(sheet, 3, null, "记录总数", String.valueOf(list.size()));
appendRow(sheet, 5, headerStyle, "客户编码", "姓名", "性别", "年龄", "电话", "地址", "病症", "备注", "邀请人", "邀请客户数", "创建时间"); appendRow(sheet, 5, headerStyle, "客户编码", "姓名", "性别", "出生日期(公历)", "出生日期历法", "年龄", "电话", "地址", "病症", "备注", "邀请人", "邀请客户数", "创建时间");
int rowIndex = 6; int rowIndex = 6;
for (GaoCustomer customer : list) { for (GaoCustomer customer : list) {
appendRow(sheet, rowIndex, null, appendRow(sheet, rowIndex, null,
text(customer.getCode()), text(customer.getCode()),
text(customer.getName()), text(customer.getName()),
formatGender(customer), formatGender(customer),
text(customer.getBirthdate()),
formatBirthdateCalendar(customer),
customer.getAge() == null ? "" : String.valueOf(customer.getAge()), customer.getAge() == null ? "" : String.valueOf(customer.getAge()),
text(customer.getTelephone()), text(customer.getTelephone()),
text(customer.getAddress()), text(customer.getAddress()),
@@ -194,7 +271,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
); );
rowIndex++; rowIndex++;
} }
for (int index = 0; index < 11; index++) { for (int index = 0; index < 13; index++) {
sheet.autoSizeColumn(index); sheet.autoSizeColumn(index);
sheet.setColumnWidth(index, Math.min(Math.max(sheet.getColumnWidth(index), 10 * 256), 36 * 256)); sheet.setColumnWidth(index, Math.min(Math.max(sheet.getColumnWidth(index), 10 * 256), 36 * 256));
} }
@@ -214,6 +291,9 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void saveInviteRelation(GaoCustomerInviteUpdateReq req) { public void saveInviteRelation(GaoCustomerInviteUpdateReq req) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_CUSTOMER_SAVE_INVITE_RELATION");
try {
logger.setContent(jackson.writeValueAsString(req));
TimiException.required(req, "not found invite req"); TimiException.required(req, "not found invite req");
TimiException.required(req.getCustomerId(), "not found invite req.customerId"); TimiException.required(req.getCustomerId(), "not found invite req.customerId");
GaoCustomer customer = super.get(req.getCustomerId()); GaoCustomer customer = super.get(req.getCustomerId());
@@ -222,17 +302,48 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
checkEntity(customer, true); checkEntity(customer, true);
super.update(customer); super.update(customer);
syncIntroducerInvitedCount(oldIntroducerCustomerId, customer.getIntroducerCustomerId()); syncIntroducerInvitedCount(oldIntroducerCustomerId, customer.getIntroducerCustomerId());
logger.setLevel(Logger.Level.INFO);
logger.setResult(req.getCustomerId());
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("保存 GAO 客户邀请关系失败", e);
throw new TimiException(TimiCode.ERROR, "保存 GAO 客户邀请关系失败", e);
} finally {
loggerService.create(logger);
}
} }
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void deleteInviteRelation(String customerId) { public void deleteInviteRelation(String customerId) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_CUSTOMER_DELETE_INVITE_RELATION");
try {
logger.setContent(customerId);
TimiException.required(customerId, "not found customerId"); TimiException.required(customerId, "not found customerId");
GaoCustomer customer = super.get(customerId); GaoCustomer customer = super.get(customerId);
String oldIntroducerCustomerId = customer.getIntroducerCustomerId(); String oldIntroducerCustomerId = customer.getIntroducerCustomerId();
customer.setIntroducerCustomerId(null); customer.setIntroducerCustomerId(null);
super.update(customer); super.update(customer);
changeInvitedCount(oldIntroducerCustomerId, -1L); changeInvitedCount(oldIntroducerCustomerId, -1L);
logger.setLevel(Logger.Level.INFO);
logger.setResult(customerId);
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("删除 GAO 客户邀请关系失败", e);
throw new TimiException(TimiCode.ERROR, "删除 GAO 客户邀请关系失败", e);
} finally {
loggerService.create(logger);
}
} }
@Override @Override
@@ -285,6 +396,10 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
GaoCustomer dbCustomer = null; GaoCustomer dbCustomer = null;
entity.setCode(normalizeText(entity.getCode())); entity.setCode(normalizeText(entity.getCode()));
entity.setIntroducerCustomerId(normalizeText(entity.getIntroducerCustomerId())); entity.setIntroducerCustomerId(normalizeText(entity.getIntroducerCustomerId()));
if (entity.getBirthdateCalendar() == null) {
entity.setBirthdateCalendar(GaoCustomer.BirthdateCalendar.SOLAR);
}
refreshAge(entity);
if (requireId) { if (requireId) {
TimiException.required(entity.getId(), "not found customer.id"); TimiException.required(entity.getId(), "not found customer.id");
dbCustomer = super.get(entity.getId()); dbCustomer = super.get(entity.getId());
@@ -314,6 +429,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
if (customer == null) { if (customer == null) {
return; return;
} }
refreshAge(customer);
loadIntroducerCustomer(List.of(customer)); loadIntroducerCustomer(List.of(customer));
if (TimiJava.isNotEmpty(customer.getCreatorUserId())) { if (TimiJava.isNotEmpty(customer.getCreatorUserId())) {
customer.setCreatorUser(userService.get(customer.getCreatorUserId())); customer.setCreatorUser(userService.get(customer.getCreatorUserId()));
@@ -322,9 +438,22 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
} }
private void loadListTransient(List<GaoCustomer> customerList) { private void loadListTransient(List<GaoCustomer> customerList) {
if (customerList != null) {
customerList.forEach(this::refreshAge);
}
loadAttachmentList(customerList); loadAttachmentList(customerList);
} }
/// 根据出生日期计算当前周岁。出生日期为空时保留旧数据中的手工年龄。
private void refreshAge(GaoCustomer customer) {
if (customer.getBirthdate() == null) {
return;
}
LocalDate today = LocalDate.now();
TimiException.requiredTrue(!customer.getBirthdate().isAfter(today), "customer.birthdate invalid");
customer.setAge(Period.between(customer.getBirthdate(), today).getYears());
}
private void loadIntroducerCustomer(List<GaoCustomer> customerList) { private void loadIntroducerCustomer(List<GaoCustomer> customerList) {
if (customerList == null || customerList.isEmpty()) { if (customerList == null || customerList.isEmpty()) {
return; return;
@@ -442,6 +571,13 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
}; };
} }
private String formatBirthdateCalendar(GaoCustomer customer) {
if (customer.getBirthdateCalendar() == null) {
return "公历";
}
return customer.getBirthdateCalendar() == GaoCustomer.BirthdateCalendar.LUNAR ? "农历" : "公历";
}
private String formatExportTime(Long value) { private String formatExportTime(Long value) {
if (value == null) { if (value == null) {
return ""; return "";
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.gao.service.implement; package com.imyeyu.api.modules.gao.service.implement;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
@@ -16,6 +17,8 @@ import com.imyeyu.api.modules.gao.service.GaoRegistrationEventService;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventRecordService; import com.imyeyu.api.modules.gao.service.GaoRegistrationEventRecordService;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegisterStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRegisterStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegistrationDayStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegistrationRankView;
import com.imyeyu.api.modules.gao.vo.GaoQuickRegistrationEventRecordReq; import com.imyeyu.api.modules.gao.vo.GaoQuickRegistrationEventRecordReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq;
@@ -25,18 +28,23 @@ import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordPeriodReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordRangeReq; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordRangeReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordStatReq; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordStatReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordTrendStatReq; import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordTrendStatReq;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.service.RoleChecker; import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.UserService; import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.api.modules.user.mapper.UserMapper; import com.imyeyu.api.modules.user.mapper.UserMapper;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time; import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Row;
@@ -47,6 +55,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
@@ -55,6 +64,7 @@ import java.time.temporal.ChronoUnit;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -68,18 +78,21 @@ import java.util.stream.Collectors;
/// ///
/// @author Codex /// @author Codex
/// @since 2026-07-27 /// @since 2026-07-27
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class GaoRegistrationEventRecordServiceImplement extends AbstractEntityService<GaoRegistrationEventRecord, String> implements GaoRegistrationEventRecordService { public class GaoRegistrationEventRecordServiceImplement extends AbstractEntityService<GaoRegistrationEventRecord, String> implements GaoRegistrationEventRecordService {
private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.BASIC_ISO_DATE;
private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()); private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
private final GaoRegistrationEventRecordMapper mapper; private final GaoRegistrationEventRecordMapper mapper;
private final ObjectMapper jackson;
private final GaoCustomerMapper customerMapper; private final GaoCustomerMapper customerMapper;
private final GaoRegistrationEventMapper registrationEventMapper; private final GaoRegistrationEventMapper registrationEventMapper;
private final UserMapper userMapper; private final UserMapper userMapper;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final LoggerService loggerService;
private final RoleChecker roleChecker; private final RoleChecker roleChecker;
private final UserLoginService userLoginService; private final UserLoginService userLoginService;
private final UserService userService; private final UserService userService;
@@ -93,45 +106,123 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public GaoRegistrationEventRecord createRecord(GaoRegistrationEventRecordCreateReq req) { public List<GaoRegistrationEventRecord> createRecords(GaoRegistrationEventRecordCreateReq req) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_RECORD_BATCH_CREATE");
try {
TimiException.required(req, "not found req"); TimiException.required(req, "not found req");
TimiException.required(req.getRegistrationEventId(), "not found req.registrationEventId"); TimiException.requiredTrue(req.getCustomerIdList() != null && !req.getCustomerIdList().isEmpty(), "customerIdList is empty");
String customerId = resolveCustomerId(req); logger.setContent(jackson.writeValueAsString(req));
GaoRegistrationEvent registrationEvent = registrationEventService.get(req.getRegistrationEventId()); List<GaoRegistrationEventRecord> recordList = new ArrayList<>();
checkRegistrationEventPermission(registrationEvent); for (int index = 0; index < req.getCustomerIdList().size(); index++) {
if (Boolean.TRUE.equals(registrationEvent.getRequirePhoto())) { String customerId = req.getCustomerIdList().get(index);
TimiException.requiredTrue(req.getAttachmentIdList() != null && !req.getAttachmentIdList().isEmpty(), "required record photo"); GaoRegistrationEventRecordCreateReq customerReq = copyCreateReq(req);
customerReq.setAttachmentIdList(0 == index && canUseSourceTempFileList(req.getAttachmentIdList())
? req.getAttachmentIdList()
: duplicateTempFileList(req.getAttachmentIdList()));
recordList.add(doCreateRecord(customerReq, customerId));
}
logger.setLevel(Logger.Level.INFO);
logger.setResult(recordList.stream().map(GaoRegistrationEventRecord::getId).collect(Collectors.joining(",")));
return recordList;
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("批量新增 GAO 登记记录失败", e);
throw new TimiException(TimiCode.ERROR, "批量新增 GAO 登记记录失败", e);
} finally {
loggerService.create(logger);
}
} }
GaoRegistrationEventRecord record = new GaoRegistrationEventRecord(); private List<String> duplicateTempFileList(List<String> sourceIdList) {
record.setCustomerId(customerId); if (sourceIdList == null) {
record.setRegistrationEventId(req.getRegistrationEventId()); return null;
record.setOperatorUserId(userLoginService.getRequireLoginUserId()); }
record.setRemark(req.getRemark()); List<String> targetIdList = new ArrayList<>();
record.setRegisteredAt(TimiJava.defaultIfNull(req.getRegisteredAt(), Time.now())); for (String sourceId : sourceIdList) {
super.create(record); if (sourceId == null || sourceId.isBlank()) {
saveAttachment(record.getId(), buildPhotoAttachmentList(req.getAttachmentIdList())); continue;
return get(record.getId()); }
Attachment source = attachmentService.get(sourceId.trim());
TimiException.required(source, "not found attachment");
TimiException.required(source.getMongoId(), "not found attachment.mongoId");
Attachment target = new Attachment();
target.setBizType(Attachment.BizType.TEMP_FILE);
target.setName(source.getName());
target.setInputStream(new ByteArrayInputStream(attachmentService.readAllByMongoId(source.getMongoId())));
attachmentService.create(target);
targetIdList.add(target.getId());
}
return targetIdList;
}
private boolean canUseSourceTempFileList(List<String> sourceIdList) {
if (sourceIdList == null || sourceIdList.isEmpty()) {
return true;
}
for (String sourceId : sourceIdList) {
if (sourceId == null || sourceId.isBlank()) {
continue;
}
Attachment source = attachmentService.get(sourceId.trim());
if (source == null || source.getBizType() != Attachment.BizType.TEMP_FILE) {
return false;
}
}
return true;
}
private GaoRegistrationEventRecordCreateReq copyCreateReq(GaoRegistrationEventRecordCreateReq source) {
GaoRegistrationEventRecordCreateReq target = new GaoRegistrationEventRecordCreateReq();
target.setRegistrationEventId(source.getRegistrationEventId());
target.setRemark(source.getRemark());
target.setRegisteredAt(source.getRegisteredAt());
return target;
} }
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public GaoRegistrationEventRecord quickRegister(GaoQuickRegistrationEventRecordReq req) { public GaoRegistrationEventRecord quickRegister(GaoQuickRegistrationEventRecordReq req) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_RECORD_QUICK_REGISTER");
try {
logger.setContent(jackson.writeValueAsString(req));
TimiException.required(req, "not found req"); TimiException.required(req, "not found req");
TimiException.required(req.getCustomerCode(), "not found req.customerCode"); TimiException.required(req.getCustomerCode(), "not found req.customerCode");
TimiException.required(req.getRegistrationEventId(), "not found req.registrationEventId"); TimiException.required(req.getRegistrationEventId(), "not found req.registrationEventId");
GaoCustomer customer = customerService.getOrQuickCreateByCode(req.getCustomerCode()); GaoCustomer customer = customerService.getOrQuickCreateByCode(req.getCustomerCode());
GaoRegistrationEventRecordCreateReq recordReq = new GaoRegistrationEventRecordCreateReq(); GaoRegistrationEventRecordCreateReq recordReq = new GaoRegistrationEventRecordCreateReq();
recordReq.setCustomerId(customer.getId()); recordReq.setCustomerIdList(List.of(customer.getId()));
recordReq.setRegistrationEventId(req.getRegistrationEventId()); recordReq.setRegistrationEventId(req.getRegistrationEventId());
recordReq.setRemark(req.getRemark()); recordReq.setRemark(req.getRemark());
recordReq.setAttachmentIdList(req.getAttachmentIdList()); recordReq.setAttachmentIdList(req.getAttachmentIdList());
return createRecord(recordReq); GaoRegistrationEventRecord record = createRecords(recordReq).get(0);
logger.setLevel(Logger.Level.INFO);
logger.setResult(record.getId());
return record;
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("GAO 快速登记失败", e);
throw new TimiException(TimiCode.ERROR, "GAO 快速登记失败", e);
} finally {
loggerService.create(logger);
}
} }
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void update(GaoRegistrationEventRecord entity) { public void update(GaoRegistrationEventRecord entity) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_RECORD_UPDATE");
try {
logger.setContent(jackson.writeValueAsString(entity));
TimiException.required(entity, "not found record"); TimiException.required(entity, "not found record");
TimiException.required(entity.getId(), "not found record.id"); TimiException.required(entity.getId(), "not found record.id");
TimiException.required(entity.getCustomerId(), "not found record.customerId"); TimiException.required(entity.getCustomerId(), "not found record.customerId");
@@ -142,6 +233,20 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
entity.setRegisteredAt(TimiJava.defaultIfNull(entity.getRegisteredAt(), Time.now())); entity.setRegisteredAt(TimiJava.defaultIfNull(entity.getRegisteredAt(), Time.now()));
super.update(entity); super.update(entity);
saveAttachment(entity.getId(), resolveAttachmentList(entity)); saveAttachment(entity.getId(), resolveAttachmentList(entity));
logger.setLevel(Logger.Level.INFO);
logger.setResult(entity.getId());
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("修改 GAO 登记记录失败", e);
throw new TimiException(TimiCode.ERROR, "修改 GAO 登记记录失败", e);
} finally {
loggerService.create(logger);
}
} }
@Override @Override
@@ -277,6 +382,29 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
return mapper.statAllCustomerByRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt()); return mapper.statAllCustomerByRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt());
} }
@Override
public List<GaoCustomerRegistrationRankView> listCustomerRegistrationRank(GaoRegistrationEventRecordRangeReq req) {
GaoRegistrationEventRecordRangeReq rangeReq = TimiJava.defaultIfNull(req, new GaoRegistrationEventRecordRangeReq());
if (rangeReq.getBeginRegisteredAt() != null || rangeReq.getEndRegisteredAt() != null) {
checkStatRange(rangeReq.getBeginRegisteredAt(), rangeReq.getEndRegisteredAt());
}
List<GaoCustomerRegistrationDayStatView> dayStatList = mapper.statCustomerRegistrationDay(rangeReq);
Map<String, List<GaoCustomerRegistrationDayStatView>> customerDayStatMap = dayStatList.stream()
.collect(Collectors.groupingBy(GaoCustomerRegistrationDayStatView::getCustomerId));
List<GaoCustomerRegistrationRankView> rankList = new ArrayList<>();
for (Map.Entry<String, List<GaoCustomerRegistrationDayStatView>> entry : customerDayStatMap.entrySet()) {
rankList.add(buildCustomerRegistrationRank(entry.getValue()));
}
rankList.sort(Comparator
.comparing(GaoCustomerRegistrationRankView::getTotalRegistrationDays, Comparator.nullsLast(Comparator.reverseOrder()))
.thenComparing(GaoCustomerRegistrationRankView::getTotalRegistrationCount, Comparator.nullsLast(Comparator.reverseOrder()))
.thenComparing(GaoCustomerRegistrationRankView::getLatestRegisteredAt, Comparator.nullsLast(Comparator.reverseOrder())));
for (int index = 0; index < rankList.size(); index++) {
rankList.get(index).setRank(index + 1);
}
return rankList;
}
@Override @Override
public List<GaoRegistrationEventRecordDailyStatView> statDailyByCustomerAndRegistrationEvent(GaoRegistrationEventRecordTrendStatReq req) { public List<GaoRegistrationEventRecordDailyStatView> statDailyByCustomerAndRegistrationEvent(GaoRegistrationEventRecordTrendStatReq req) {
TimiException.required(req, "not found req"); TimiException.required(req, "not found req");
@@ -299,21 +427,118 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
return list; return list;
} }
private String resolveCustomerId(GaoRegistrationEventRecordCreateReq req) { private String resolveCustomerId(String customerId) {
if (TimiJava.isNotEmpty(req.getCustomerId())) { TimiException.required(customerId, "not found customerId");
customerService.get(req.getCustomerId()); customerService.get(customerId);
return req.getCustomerId(); return customerId;
} }
TimiException.required(req.getCustomerCode(), "not found req.customerCode");
GaoCustomer customer = customerService.getByCode(req.getCustomerCode()); private GaoRegistrationEventRecord doCreateRecord(GaoRegistrationEventRecordCreateReq req, String customerId) {
return customer.getId(); TimiException.required(req, "not found req");
TimiException.required(req.getRegistrationEventId(), "not found req.registrationEventId");
customerId = resolveCustomerId(customerId);
GaoRegistrationEvent registrationEvent = registrationEventService.get(req.getRegistrationEventId());
checkRegistrationEventPermission(registrationEvent);
long registeredAt = TimiJava.defaultIfNull(req.getRegisteredAt(), Time.now());
checkRegistrationEventCreatable(registrationEvent, customerId, registeredAt);
if (Boolean.TRUE.equals(registrationEvent.getRequirePhoto())) {
TimiException.requiredTrue(req.getAttachmentIdList() != null && !req.getAttachmentIdList().isEmpty(), "required record photo");
}
GaoRegistrationEventRecord record = new GaoRegistrationEventRecord();
record.setCustomerId(customerId);
record.setRegistrationEventId(req.getRegistrationEventId());
record.setOperatorUserId(userLoginService.getRequireLoginUserId());
record.setRemark(req.getRemark());
record.setRegisteredAt(registeredAt);
super.create(record);
saveAttachment(record.getId(), buildPhotoAttachmentList(req.getAttachmentIdList()));
return get(record.getId());
} }
private void checkRegistrationEventPermission(GaoRegistrationEvent registrationEvent) { private void checkRegistrationEventPermission(GaoRegistrationEvent registrationEvent) {
List<String> roleCodeList = registrationEventService.listRoleCode(registrationEvent.getId()); List<String> roleCodeList = registrationEventService.listRoleCode(registrationEvent.getId());
if (roleCodeList != null && !roleCodeList.isEmpty()) { TimiException.requiredTrue(roleCodeList != null && !roleCodeList.isEmpty(), "登记事件未配置可登记角色");
roleChecker.checkAny(ModuleCode.GAO, roleCodeList.toArray(String[]::new)); roleChecker.checkAny(ModuleCode.GAO, roleCodeList.toArray(String[]::new));
} }
private void checkRegistrationEventCreatable(GaoRegistrationEvent registrationEvent, String customerId, long registeredAt) {
TimiException.requiredTrue(registrationEvent.getStatus() == null || registrationEvent.getStatus() == GaoRegistrationEvent.Status.ACTIVE, "登记事件不可登记");
TimiException.requiredTrue(registrationEvent.getBeginAt() == null || registrationEvent.getBeginAt() <= registeredAt, "登记事件未到可登记时间");
TimiException.requiredTrue(registrationEvent.getEndAt() == null || registeredAt <= registrationEvent.getEndAt(), "登记事件已过可登记时间");
GaoRegistrationEvent.LimitType limitType = TimiJava.defaultIfNull(registrationEvent.getLimitType(), GaoRegistrationEvent.LimitType.NONE);
switch (limitType) {
case DAILY_HALF_DAY -> checkRegistrationLimitRange(customerId, registrationEvent.getId(), getHalfDayBegin(registeredAt), getHalfDayEnd(registeredAt), "该客户当前半日已登记该事件");
case NATURAL_DAY -> checkRegistrationLimitRange(customerId, registrationEvent.getId(), getDayBegin(registeredAt), getDayEnd(registeredAt), "该客户当日已登记该事件");
case INTERVAL_HOUR -> checkIntervalHourRegistrationLimit(registrationEvent, customerId, registeredAt);
case NATURAL_MONTH -> checkRegistrationLimitRange(customerId, registrationEvent.getId(), getMonthBegin(registeredAt), getMonthEnd(registeredAt), "该客户当月已登记该事件");
case TOTAL_LIMIT -> checkTotalRegistrationLimit(registrationEvent);
case CUSTOMER_TOTAL_LIMIT -> checkCustomerTotalRegistrationLimit(registrationEvent, customerId);
default -> {
}
}
}
private void checkRegistrationLimitRange(String customerId, String registrationEventId, long beginRegisteredAt, long endRegisteredAt, String message) {
Long count = mapper.countByCustomerIdAndRegistrationEventIdRange(customerId, registrationEventId, beginRegisteredAt, endRegisteredAt);
TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < 1, message);
}
private void checkIntervalHourRegistrationLimit(GaoRegistrationEvent registrationEvent, String customerId, long registeredAt) {
Integer limitValue = registrationEvent.getLimitValue();
TimiException.requiredTrue(limitValue != null && 0 < limitValue, "登记事件间隔小时配置错误");
long beginRegisteredAt = registeredAt - limitValue * 60L * 60L * 1000L + 1;
checkRegistrationLimitRange(customerId, registrationEvent.getId(), beginRegisteredAt, registeredAt, "该客户登记该事件间隔不足");
}
private void checkCustomerTotalRegistrationLimit(GaoRegistrationEvent registrationEvent, String customerId) {
Integer limitValue = registrationEvent.getLimitValue();
TimiException.requiredTrue(limitValue != null && 0 < limitValue, "登记事件单客户总次数配置错误");
Long count = mapper.countByCustomerIdAndRegistrationEventId(customerId, registrationEvent.getId());
TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < limitValue, "该客户登记该事件次数已达上限");
}
private void checkTotalRegistrationLimit(GaoRegistrationEvent registrationEvent) {
Integer limitValue = registrationEvent.getLimitValue();
TimiException.requiredTrue(limitValue != null && 0 < limitValue, "登记事件总次数配置错误");
Long count = mapper.countByRegistrationEventId(registrationEvent.getId());
TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < limitValue, "登记事件总次数已达上限");
}
private long getDayBegin(long registeredAt) {
ZoneId zoneId = ZoneId.systemDefault();
return Instant.ofEpochMilli(registeredAt).atZone(zoneId).toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli();
}
private long getDayEnd(long registeredAt) {
ZoneId zoneId = ZoneId.systemDefault();
return Instant.ofEpochMilli(registeredAt).atZone(zoneId).toLocalDate().plusDays(1).atStartOfDay(zoneId).toInstant().toEpochMilli() - 1;
}
private long getHalfDayBegin(long registeredAt) {
ZoneId zoneId = ZoneId.systemDefault();
int hour = Instant.ofEpochMilli(registeredAt).atZone(zoneId).getHour();
long dayBegin = getDayBegin(registeredAt);
return hour < 12 ? dayBegin : dayBegin + 12L * 60L * 60L * 1000L;
}
private long getHalfDayEnd(long registeredAt) {
ZoneId zoneId = ZoneId.systemDefault();
int hour = Instant.ofEpochMilli(registeredAt).atZone(zoneId).getHour();
long dayBegin = getDayBegin(registeredAt);
return hour < 12 ? dayBegin + 12L * 60L * 60L * 1000L - 1 : getDayEnd(registeredAt);
}
private long getMonthBegin(long registeredAt) {
ZoneId zoneId = ZoneId.systemDefault();
LocalDate date = Instant.ofEpochMilli(registeredAt).atZone(zoneId).toLocalDate();
return date.withDayOfMonth(1).atStartOfDay(zoneId).toInstant().toEpochMilli();
}
private long getMonthEnd(long registeredAt) {
ZoneId zoneId = ZoneId.systemDefault();
LocalDate date = Instant.ofEpochMilli(registeredAt).atZone(zoneId).toLocalDate();
return date.withDayOfMonth(1).plusMonths(1).atStartOfDay(zoneId).toInstant().toEpochMilli() - 1;
} }
private void checkStatRange(Long beginRegisteredAt, Long endRegisteredAt) { private void checkStatRange(Long beginRegisteredAt, Long endRegisteredAt) {
@@ -356,6 +581,43 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
return list; return list;
} }
private GaoCustomerRegistrationRankView buildCustomerRegistrationRank(List<GaoCustomerRegistrationDayStatView> dayStatList) {
List<GaoCustomerRegistrationDayStatView> sortedDayStatList = dayStatList.stream()
.sorted(Comparator.comparing(GaoCustomerRegistrationDayStatView::getDateValue, Comparator.nullsLast(Comparator.reverseOrder())))
.toList();
GaoCustomerRegistrationDayStatView first = sortedDayStatList.get(0);
GaoCustomerRegistrationRankView view = new GaoCustomerRegistrationRankView();
view.setCustomerId(first.getCustomerId());
view.setCustomerCode(first.getCustomerCode());
view.setCustomerName(first.getCustomerName());
view.setCustomerPhotoAttachmentId(first.getCustomerPhotoAttachmentId());
view.setTotalRegistrationDays((long) sortedDayStatList.size());
view.setTotalRegistrationCount(sortedDayStatList.stream().mapToLong(item -> TimiJava.defaultIfNull(item.getDailyCount(), 0L)).sum());
view.setLatestRegisteredAt(sortedDayStatList.stream().map(GaoCustomerRegistrationDayStatView::getLatestRegisteredAt).filter(Objects::nonNull).max(Long::compareTo).orElse(null));
view.setRecentContinuousRegistrationDays(countRecentContinuousRegistrationDays(sortedDayStatList));
return view;
}
private long countRecentContinuousRegistrationDays(List<GaoCustomerRegistrationDayStatView> sortedDayStatList) {
if (sortedDayStatList.isEmpty() || sortedDayStatList.get(0).getDateValue() == null) {
return 0;
}
LocalDate expectedDate = LocalDate.parse(String.valueOf(sortedDayStatList.get(0).getDateValue()), DAY_FORMATTER);
long count = 0;
for (GaoCustomerRegistrationDayStatView item : sortedDayStatList) {
if (item.getDateValue() == null) {
continue;
}
LocalDate date = LocalDate.parse(String.valueOf(item.getDateValue()), DAY_FORMATTER);
if (!date.equals(expectedDate)) {
break;
}
count++;
expectedDate = expectedDate.minusDays(1);
}
return count;
}
private void loadTransient(List<GaoRegistrationEventRecord> recordList) { private void loadTransient(List<GaoRegistrationEventRecord> recordList) {
if (recordList == null || recordList.isEmpty()) { if (recordList == null || recordList.isEmpty()) {
return; return;
@@ -1,17 +1,24 @@
package com.imyeyu.api.modules.gao.service.implement; package com.imyeyu.api.modules.gao.service.implement;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRoleRelation;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent; import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent;
import com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventRoleRelationMapper; import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRoleRelation;
import com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventMapper; import com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventMapper;
import com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventRoleRelationMapper;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventService; import com.imyeyu.api.modules.gao.service.GaoRegistrationEventService;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -25,12 +32,17 @@ import java.util.stream.Collectors;
/// ///
/// 登记事件服务实现 /// 登记事件服务实现
/// ///
/// @author Codex /// @author 夜雨
/// @since 2026-07-27 /// @since 2026-07-27 09:00
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class GaoRegistrationEventServiceImplement extends AbstractEntityService<GaoRegistrationEvent, String> implements GaoRegistrationEventService { public class GaoRegistrationEventServiceImplement extends AbstractEntityService<GaoRegistrationEvent, String> implements GaoRegistrationEventService {
private final ObjectMapper jackson;
private final LoggerService loggerService;
private final GaoRegistrationEventMapper mapper; private final GaoRegistrationEventMapper mapper;
private final GaoRegistrationEventRoleRelationMapper roleRelationMapper; private final GaoRegistrationEventRoleRelationMapper roleRelationMapper;
@@ -42,6 +54,7 @@ public class GaoRegistrationEventServiceImplement extends AbstractEntityService<
@Override @Override
public PageResult<GaoRegistrationEvent> pageWithRegistrationEventLimit(Page<GaoRegistrationEvent> page) { public PageResult<GaoRegistrationEvent> pageWithRegistrationEventLimit(Page<GaoRegistrationEvent> page) {
PageResult<GaoRegistrationEvent> result = page(page); PageResult<GaoRegistrationEvent> result = page(page);
result.setList(result.getList().stream().filter(item -> item.getStatus() != GaoRegistrationEvent.Status.DELETED).toList());
loadRegistrationEventLimit(result.getList()); loadRegistrationEventLimit(result.getList());
return result; return result;
} }
@@ -49,24 +62,90 @@ public class GaoRegistrationEventServiceImplement extends AbstractEntityService<
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void create(GaoRegistrationEvent entity) { public void create(GaoRegistrationEvent entity) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_CREATE");
try {
logger.setContent(jackson.writeValueAsString(entity));
checkEntity(entity, false); checkEntity(entity, false);
super.create(entity); super.create(entity);
saveRoleRelation(entity.getId(), entity.getRoleCodeList()); saveRoleRelation(entity.getId(), entity.getRoleCodeList());
logger.setLevel(Logger.Level.INFO);
logger.setResult(entity.getId());
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("新增 GAO 登记事件失败", e);
throw new TimiException(TimiCode.ERROR, "新增 GAO 登记事件失败", e);
} finally {
loggerService.create(logger);
}
} }
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void update(GaoRegistrationEvent entity) { public void update(GaoRegistrationEvent entity) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_UPDATE");
try {
logger.setContent(jackson.writeValueAsString(entity));
Integer sort = entity.getSort();
checkEntity(entity, true); checkEntity(entity, true);
super.update(entity); GaoRegistrationEvent current = get(entity.getId());
current.setName(entity.getName());
current.setDescription(entity.getDescription());
current.setColor(entity.getColor());
current.setStatus(entity.getStatus());
current.setRequirePhoto(entity.getRequirePhoto());
current.setLimitType(entity.getLimitType());
current.setLimitValue(entity.getLimitValue());
current.setBeginAt(entity.getBeginAt());
current.setEndAt(entity.getEndAt());
if (sort != null) {
current.setSort(sort);
}
mapper.update(current);
saveRoleRelation(entity.getId(), entity.getRoleCodeList()); saveRoleRelation(entity.getId(), entity.getRoleCodeList());
logger.setLevel(Logger.Level.INFO);
logger.setResult(entity.getId());
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("修改 GAO 登记事件失败", e);
throw new TimiException(TimiCode.ERROR, "修改 GAO 登记事件失败", e);
} finally {
loggerService.create(logger);
}
} }
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void delete(String id) { public void delete(String id) {
roleRelationMapper.deleteByRegistrationEventId(id); Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_DELETE");
super.delete(id); try {
logger.setContent(id);
GaoRegistrationEvent entity = get(id);
entity.setStatus(GaoRegistrationEvent.Status.DELETED);
super.update(entity);
logger.setLevel(Logger.Level.INFO);
logger.setResult(id);
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("删除 GAO 登记事件失败", e);
throw new TimiException(TimiCode.ERROR, "删除 GAO 登记事件失败", e);
} finally {
loggerService.create(logger);
}
} }
@Override @Override
@@ -78,8 +157,9 @@ public class GaoRegistrationEventServiceImplement extends AbstractEntityService<
@Override @Override
public List<GaoRegistrationEvent> listEnabled() { public List<GaoRegistrationEvent> listEnabled() {
List<GaoRegistrationEvent> list = mapper.selectEnabledList(); List<GaoRegistrationEvent> list = mapper.selectEnabledList(Time.now());
loadRegistrationEventLimit(list); loadRegistrationEventLimit(list);
list = list.stream().filter(item -> !TimiJava.isEmpty(item.getRoleCodeList())).toList();
return list; return list;
} }
@@ -88,23 +168,66 @@ public class GaoRegistrationEventServiceImplement extends AbstractEntityService<
return roleRelationMapper.selectRoleCodeListByRegistrationEventId(registrationEventId); return roleRelationMapper.selectRoleCodeListByRegistrationEventId(registrationEventId);
} }
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void sort(List<String> idList) {
if (idList == null || idList.isEmpty()) {
return;
}
List<String> distinctIdList = new ArrayList<>(new LinkedHashSet<>(idList));
Map<String, GaoRegistrationEvent> eventMap = mapper.selectByIdList(distinctIdList).stream().collect(Collectors.toMap(GaoRegistrationEvent::getId, item -> item));
TimiException.requiredTrue(eventMap.size() == distinctIdList.size(), "not found registrationEvent");
for (int index = 0; index < distinctIdList.size(); index++) {
GaoRegistrationEvent registrationEvent = eventMap.get(distinctIdList.get(index));
registrationEvent.setSort(index);
mapper.update(registrationEvent);
}
}
private void checkEntity(GaoRegistrationEvent entity, boolean requireId) { private void checkEntity(GaoRegistrationEvent entity, boolean requireId) {
TimiException.required(entity, "not found registrationEvent"); TimiException.required(entity, "not found registrationEvent");
if (requireId) { if (requireId) {
TimiException.required(entity.getId(), "not found registrationEvent.id"); TimiException.required(entity.getId(), "not found registrationEvent.id");
} }
TimiException.required(entity.getName(), "not found registrationEvent.name"); TimiException.required(entity.getName(), "not found registrationEvent.name");
if (entity.getEnabled() == null) { if (entity.getStatus() == null) {
entity.setEnabled(true); entity.setStatus(GaoRegistrationEvent.Status.ACTIVE);
} }
if (entity.getRequirePhoto() == null) { if (entity.getRequirePhoto() == null) {
entity.setRequirePhoto(false); entity.setRequirePhoto(false);
} }
if (entity.getLimitType() == null) {
entity.setLimitType(GaoRegistrationEvent.LimitType.NONE);
}
TimiException.requiredTrue(entity.getRoleCodeList() != null && entity.getRoleCodeList()
.stream()
.anyMatch(item -> item != null && !item.isBlank()), "not found registrationEvent.roleCodeList");
checkRegistrationLimit(entity);
checkRegisterablePeriod(entity);
if (entity.getColor() != null) {
entity.setColor(entity.getColor().trim());
}
if (entity.getSort() == null) { if (entity.getSort() == null) {
entity.setSort(0); entity.setSort(0);
} }
} }
private void checkRegistrationLimit(GaoRegistrationEvent entity) {
if (entity.getLimitType() == GaoRegistrationEvent.LimitType.INTERVAL_HOUR || entity.getLimitType() == GaoRegistrationEvent.LimitType.TOTAL_LIMIT || entity.getLimitType() == GaoRegistrationEvent.LimitType.CUSTOMER_TOTAL_LIMIT) {
TimiException.required(entity.getLimitValue(), "not found registrationEvent.limitValue");
TimiException.requiredTrue(0 < entity.getLimitValue(), "registrationEvent.limitValue lte 0");
return;
}
entity.setLimitValue(null);
}
private void checkRegisterablePeriod(GaoRegistrationEvent entity) {
if (entity.getBeginAt() == null && entity.getEndAt() == null) {
return;
}
TimiException.requiredTrue(entity.getBeginAt() == null || entity.getEndAt() == null || entity.getBeginAt() <= entity.getEndAt(), "registrationEvent.beginAt gt registrationEvent.endAt");
}
private void saveRoleRelation(String registrationEventId, List<String> roleCodeList) { private void saveRoleRelation(String registrationEventId, List<String> roleCodeList) {
roleRelationMapper.deleteByRegistrationEventId(registrationEventId); roleRelationMapper.deleteByRegistrationEventId(registrationEventId);
if (roleCodeList == null || roleCodeList.isEmpty()) { if (roleCodeList == null || roleCodeList.isEmpty()) {
@@ -122,23 +245,14 @@ public class GaoRegistrationEventServiceImplement extends AbstractEntityService<
} }
} }
private void loadRegistrationEventLimit(GaoRegistrationEvent registrationEvent) {
registrationEvent.setRoleCodeList(roleRelationMapper.selectRoleCodeListByRegistrationEventId(registrationEvent.getId()));
}
private void loadRegistrationEventLimit(List<GaoRegistrationEvent> registrationEventList) { private void loadRegistrationEventLimit(List<GaoRegistrationEvent> registrationEventList) {
if (registrationEventList == null || registrationEventList.isEmpty()) { if (registrationEventList == null || registrationEventList.isEmpty()) {
return; return;
} }
List<String> registrationEventIdList = registrationEventList.stream() List<String> registrationEventIdList = registrationEventList.stream().map(GaoRegistrationEvent::getId).distinct().toList();
.map(GaoRegistrationEvent::getId) Map<String, List<String>> roleCodeMap = roleRelationMapper.selectByRegistrationEventIdList(new ArrayList<>(registrationEventIdList))
.distinct() .stream()
.toList(); .collect(Collectors.groupingBy(GaoRegistrationEventRoleRelation::getRegistrationEventId, Collectors.mapping(GaoRegistrationEventRoleRelation::getRoleCode, Collectors.toList())));
Map<String, List<String>> roleCodeMap = roleRelationMapper.selectByRegistrationEventIdList(new ArrayList<>(registrationEventIdList)).stream()
.collect(Collectors.groupingBy(
GaoRegistrationEventRoleRelation::getRegistrationEventId,
Collectors.mapping(GaoRegistrationEventRoleRelation::getRoleCode, Collectors.toList())
));
for (GaoRegistrationEvent registrationEvent : registrationEventList) { for (GaoRegistrationEvent registrationEvent : registrationEventList) {
registrationEvent.setRoleCodeList(roleCodeMap.getOrDefault(registrationEvent.getId(), List.of())); registrationEvent.setRoleCodeList(roleCodeMap.getOrDefault(registrationEvent.getId(), List.of()));
} }
@@ -1,10 +1,13 @@
package com.imyeyu.api.modules.gao.service.implement; package com.imyeyu.api.modules.gao.service.implement;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.entity.GaoUser; import com.imyeyu.api.modules.gao.entity.GaoUser;
import com.imyeyu.api.modules.gao.mapper.GaoUserMapper; import com.imyeyu.api.modules.gao.mapper.GaoUserMapper;
import com.imyeyu.api.modules.gao.service.GaoUserService; import com.imyeyu.api.modules.gao.service.GaoUserService;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.entity.Role; import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.mapper.UserMapper; import com.imyeyu.api.modules.user.mapper.UserMapper;
@@ -14,12 +17,14 @@ import com.imyeyu.api.modules.user.vo.role.UserRoleAuthorizeReq;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode; import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time; import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -34,11 +39,14 @@ import java.util.stream.Collectors;
/// ///
/// @author Codex /// @author Codex
/// @since 2026-07-30 /// @since 2026-07-30
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class GaoUserServiceImplement extends AbstractEntityService<GaoUser, String> implements GaoUserService { public class GaoUserServiceImplement extends AbstractEntityService<GaoUser, String> implements GaoUserService {
private final GaoUserMapper mapper; private final GaoUserMapper mapper;
private final ObjectMapper jackson;
private final LoggerService loggerService;
private final UserMapper userMapper; private final UserMapper userMapper;
private final UserService userService; private final UserService userService;
private final RoleService roleService; private final RoleService roleService;
@@ -51,20 +59,54 @@ public class GaoUserServiceImplement extends AbstractEntityService<GaoUser, Stri
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void create(GaoUser entity) { public void create(GaoUser entity) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_USER_CREATE");
try {
logger.setContent(jackson.writeValueAsString(entity));
prepareUser(entity); prepareUser(entity);
checkEntity(entity, false); checkEntity(entity, false);
super.create(entity); super.create(entity);
saveUserRole(entity); saveUserRole(entity);
loadUser(entity); loadUser(entity);
logger.setLevel(Logger.Level.INFO);
logger.setResult(entity.getId());
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("新增 GAO 用户失败", e);
throw new TimiException(TimiCode.ERROR, "新增 GAO 用户失败", e);
} finally {
loggerService.create(logger);
}
} }
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void update(GaoUser entity) { public void update(GaoUser entity) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_USER_UPDATE");
try {
logger.setContent(jackson.writeValueAsString(entity));
checkEntity(entity, true); checkEntity(entity, true);
updateUser(entity); updateUser(entity);
super.update(entity); super.update(entity);
saveUserRole(entity); saveUserRole(entity);
logger.setLevel(Logger.Level.INFO);
logger.setResult(entity.getId());
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("修改 GAO 用户失败", e);
throw new TimiException(TimiCode.ERROR, "修改 GAO 用户失败", e);
} finally {
loggerService.create(logger);
}
} }
@Override @Override
@@ -0,0 +1,33 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 客户登记日期统计视图
///
/// @author Codex
/// @since 2026-08-03
@Data
public class GaoCustomerRegistrationDayStatView {
/// 客户 ID
private String customerId;
/// 客户编码
private String customerCode;
/// 客户姓名
private String customerName;
/// 客户头像附件 ID
private String customerPhotoAttachmentId;
/// 登记日期,格式 yyyyMMdd
private Integer dateValue;
/// 当日登记次数
private Long dailyCount;
/// 当日最后登记时间
private Long latestRegisteredAt;
}
@@ -0,0 +1,39 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 客户登记排行榜视图
///
/// @author Codex
/// @since 2026-08-03
@Data
public class GaoCustomerRegistrationRankView {
/// 排名
private Integer rank;
/// 客户 ID
private String customerId;
/// 客户编码
private String customerCode;
/// 客户姓名
private String customerName;
/// 客户头像附件 ID
private String customerPhotoAttachmentId;
/// 累计登记天数
private Long totalRegistrationDays;
/// 累计登记次数
private Long totalRegistrationCount;
/// 最近连续登记天数
private Long recentContinuousRegistrationDays;
/// 最后登记时间
private Long latestRegisteredAt;
}
@@ -13,11 +13,8 @@ import java.util.List;
@Data @Data
public class GaoRegistrationEventRecordCreateReq { public class GaoRegistrationEventRecordCreateReq {
/// 客户 ID /// 客户 ID 列表,批量登记时使用
private String customerId; private List<String> customerIdList = new ArrayList<>();
/// 客户编码,扫码登记可直接传这个
private String customerCode;
/// 登记事件 ID /// 登记事件 ID
private String registrationEventId; private String registrationEventId;
@@ -1,22 +0,0 @@
package com.imyeyu.api.modules.gao.vo;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
///
/// 登记事件保存请求
///
/// @author Codex
/// @since 2026-07-27
@Data
public class GaoRegistrationEventSaveReq {
/// 登记事件
private GaoRegistrationEvent registrationEvent;
/// 可登记角色代码列表
private List<String> roleCodeList = new ArrayList<>();
}
@@ -17,6 +17,21 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
public class Logger implements IDEntity<String>, Creatable { public class Logger implements IDEntity<String>, Creatable {
///
///
/// @author 夜雨
/// @since 2026-08-03 11:43
public enum Module {
SYSTEM,
COMMON,
USER,
GAO,
}
/** /**
* 日志级别 * 日志级别
* *
@@ -43,7 +58,7 @@ public class Logger implements IDEntity<String>, Creatable {
private Level level; private Level level;
/** 模块名称 */ /** 模块名称 */
private String module; private Module module;
/** 操作描述 */ /** 操作描述 */
private String operation; private String operation;
@@ -69,7 +84,26 @@ public class Logger implements IDEntity<String>, Creatable {
/** 创建时间 */ /** 创建时间 */
private Long createdAt; private Long createdAt;
public Logger(Level level) { public Logger(Module module, String operation) {
this.module = module;
this.operation = operation;
}
public Logger(Module module, Level level, String operation) {
this.module = module;
this.level = level; this.level = level;
this.operation = operation;
}
public static Logger info(Module module, String operation) {
return new Logger(module, Level.INFO, operation);
}
public static Logger warning(Module module, String operation) {
return new Logger(module, Level.WARN, operation);
}
public static Logger error(Module module, String operation) {
return new Logger(module, Level.ERROR, operation);
} }
} }
@@ -16,41 +16,29 @@ public interface LoggerService extends GettableService<Logger, String>, Pageable
void createSync(Logger logger); void createSync(Logger logger);
void create(Logger.Level level, String module, String operation, String userId, String content, String result); void create(Logger.Module module, Logger.Level level, String operation, String content, String result);
default void info(String module, String operation, String userId) { default void info(Logger.Module module, String operation) {
create(Logger.Level.INFO, module, operation, userId, null, null); create(module, Logger.Level.INFO, operation, null, null);
} }
default void info(String module, String operation, String content, String result) { default void info(Logger.Module module, String operation, String content, String result) {
create(Logger.Level.INFO, module, operation, null, content, result); create(module, Logger.Level.INFO, operation, content, result);
} }
default void info(String module, String operation, String userId, String content, String result) { default void warning(Logger.Module module, String operation) {
create(Logger.Level.INFO, module, operation, userId, content, result); create(module, Logger.Level.WARN, operation, null, null);
} }
default void warning(String module, String operation, String userId) { default void warning(Logger.Module module, String operation, String content, String result) {
create(Logger.Level.WARN, module, operation, userId, null, null); create(module, Logger.Level.WARN, operation, content, result);
} }
default void warning(String module, String operation, String content, String result) { default void error(Logger.Module module, String operation) {
create(Logger.Level.WARN, module, operation, null, content, result); create(module, Logger.Level.ERROR, operation, null, null);
} }
default void warning(String module, String operation, String userId, String content, String result) { default void error(Logger.Module module, String operation, String content, String result) {
create(Logger.Level.WARN, module, operation, userId, content, result); create(module, Logger.Level.ERROR, operation, content, result);
}
default void error(String module, String operation, String userId) {
create(Logger.Level.ERROR, module, operation, userId, null, null);
}
default void error(String module, String operation, String content, String result) {
create(Logger.Level.ERROR, module, operation, null, content, result);
}
default void error(String module, String operation, String userId, String content, String result) {
create(Logger.Level.ERROR, module, operation, userId, content, result);
} }
} }
@@ -3,6 +3,10 @@ package com.imyeyu.api.modules.system.service.implement;
import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.mapper.LoggerMapper; import com.imyeyu.api.modules.system.mapper.LoggerMapper;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -18,6 +22,8 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor @RequiredArgsConstructor
public class LoggerServiceImplement extends AbstractEntityService<Logger, String> implements LoggerService { public class LoggerServiceImplement extends AbstractEntityService<Logger, String> implements LoggerService {
private final UserLoginService userLoginService;
private final LoggerMapper mapper; private final LoggerMapper mapper;
@Override @Override
@@ -28,19 +34,23 @@ public class LoggerServiceImplement extends AbstractEntityService<Logger, String
@Override @Override
public void create(Logger logger) { public void create(Logger logger) {
// 默认异步 // 默认异步
Thread.startVirtualThread(() -> super.create(logger)); Thread.startVirtualThread(() -> createSync(logger));
} }
@Override @Override
public void createSync(Logger logger) { public void createSync(Logger logger) {
if (TimiJava.isEmpty(logger.getUserId())) {
User loginUser = userLoginService.getLoginUser();
if (TimiJava.isNotEmpty(loginUser)) {
logger.setUserId(loginUser.getId());
}
}
logger.setIp(TimiJava.defaultIfEmpty(logger.getIp(), TimiSpring.getRequestIP()));
super.create(logger); super.create(logger);
} }
public void create(Logger.Level level, String module, String operation, String userId, String content, String result) { public void create(Logger.Module module, Logger.Level level, String operation, String content, String result) {
Logger log = new Logger(level); Logger log = new Logger(module, level, operation);
log.setModule(module);
log.setOperation(operation);
log.setUserId(userId);
log.setContent(content); log.setContent(content);
log.setResult(result); log.setResult(result);
create(log); create(log);
@@ -25,8 +25,8 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/// 用户登录服务实现 /// 用户登录服务实现
/// ///
@@ -95,7 +95,9 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava {
@Override @Override
public User getLoginUser() { public User getLoginUser() {
String token = TimiSpring.getToken(); String token = TimiSpring.getToken();
TimiException.required(token, "not found token"); if (TimiJava.isEmpty(token)) {
return null;
}
String userId = redisUserToken.get(getUserTokenKey(token)); String userId = redisUserToken.get(getUserTokenKey(token));
if (TimiJava.isEmpty(userId)) { if (TimiJava.isEmpty(userId)) {
return null; return null;
@@ -1,10 +1,11 @@
package com.imyeyu.api.util; package com.imyeyu.api.util;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.bean.ImageType; import com.imyeyu.api.modules.common.bean.ImageType;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode; import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.bean.BootstrapData; import com.imyeyu.api.modules.user.bean.BootstrapData;
import com.imyeyu.api.modules.user.bean.BuiltinAuthRegistry; import com.imyeyu.api.modules.user.bean.BuiltinAuthRegistry;
@@ -92,7 +93,13 @@ public class InitBuiltinUser implements ApplicationRunner {
userService.create(user); userService.create(user);
String logContent = "初始化 %s 自举用户成功,请立即修改密码:%s".formatted(BootstrapData.SYS_USER_NAME, password); String logContent = "初始化 %s 自举用户成功,请立即修改密码:%s".formatted(BootstrapData.SYS_USER_NAME, password);
loggerService.info("SYSTEM", "INITIALIZE_SYSTEM_USER", user.getId(), logContent, null);
Logger logger = new Logger();
logger.setModule(Logger.Module.SYSTEM);
logger.setOperation("INITIALIZE_SYSTEM_USER");
logger.setUserId(user.getId());
logger.setContent(logContent);
loggerService.create(logger);
} }
// 授权系统用户角色 // 授权系统用户角色
{ {
@@ -0,0 +1,6 @@
ALTER TABLE `gao_registration_event`
ADD COLUMN `color` VARCHAR(32) NULL COMMENT '统计图表颜色' AFTER `id`,
ADD COLUMN `status` VARCHAR(32) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态,ACTIVE 可登记,INACTIVE 无效,DELETED 已删除' AFTER `sort`,
ADD COLUMN `limit_type` VARCHAR(32) NOT NULL DEFAULT 'NONE' COMMENT '登记限制类型' AFTER `require_photo`,
ADD COLUMN `limit_value` INT NULL COMMENT '登记限制数值,按限制类型表示小时数或次数' AFTER `limit_type`,
ADD KEY `idx_gao_registration_event_status` (`status`, `enabled`, `deleted_at`, `sort`);
@@ -0,0 +1,7 @@
ALTER TABLE `gao_registration_event`
DROP KEY `idx_gao_registration_event_status`,
DROP COLUMN `enabled`,
MODIFY COLUMN `status` VARCHAR(32) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态,DRAFT 草稿,ACTIVE 可登记,INACTIVE 不可登记,DELETED 已删除',
ADD COLUMN `begin_at` BIGINT NULL COMMENT '可登记起始时间戳,毫秒,空为不限制' AFTER `limit_value`,
ADD COLUMN `end_at` BIGINT NULL COMMENT '可登记终止时间戳,毫秒,空为不限制' AFTER `begin_at`,
ADD KEY `idx_gao_registration_event_status` (`status`, `deleted_at`, `sort`, `created_at`);
@@ -0,0 +1,3 @@
ALTER TABLE `gao_customer`
ADD COLUMN `birthdate` DATE NULL COMMENT '出生日期,统一保存为公历' AFTER `gender`,
ADD COLUMN `birthdate_calendar` VARCHAR(16) NOT NULL DEFAULT 'SOLAR' COMMENT '出生日期录入历法,SOLAR 公历,LUNAR 农历' AFTER `birthdate`;
@@ -18,4 +18,16 @@
</if> </if>
AND `deleted_at` IS NULL AND `deleted_at` IS NULL
</select> </select>
<select id="selectEnabledList" resultType="com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent">
SELECT
*
FROM `gao_registration_event`
WHERE
(`status` IS NULL OR `status` = 'ACTIVE')
AND (`begin_at` IS NULL OR `begin_at` &lt;= #{now})
AND (`end_at` IS NULL OR #{now} &lt;= `end_at`)
AND `deleted_at` IS NULL
ORDER BY `sort` ASC, `created_at` ASC
</select>
</mapper> </mapper>
@@ -20,6 +20,37 @@
ORDER BY `registered_at` DESC ORDER BY `registered_at` DESC
</select> </select>
<select id="countByCustomerIdAndRegistrationEventIdRange" resultType="java.lang.Long">
SELECT
COUNT(1)
FROM `gao_registration_event_record`
WHERE
`customer_id` = #{customerId}
AND `registration_event_id` = #{registrationEventId}
AND `registered_at` &gt;= #{beginRegisteredAt}
AND `registered_at` &lt;= #{endRegisteredAt}
AND `deleted_at` IS NULL
</select>
<select id="countByCustomerIdAndRegistrationEventId" resultType="java.lang.Long">
SELECT
COUNT(1)
FROM `gao_registration_event_record`
WHERE
`customer_id` = #{customerId}
AND `registration_event_id` = #{registrationEventId}
AND `deleted_at` IS NULL
</select>
<select id="countByRegistrationEventId" resultType="java.lang.Long">
SELECT
COUNT(1)
FROM `gao_registration_event_record`
WHERE
`registration_event_id` = #{registrationEventId}
AND `deleted_at` IS NULL
</select>
<select id="statByRegistrationEventAndPeriodType" resultType="com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView"> <select id="statByRegistrationEventAndPeriodType" resultType="com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView">
SELECT SELECT
`registration_event_id` AS `registrationEventId`, `registration_event_id` AS `registrationEventId`,
@@ -112,6 +143,54 @@
ORDER BY `latestRegisteredAt` DESC ORDER BY `latestRegisteredAt` DESC
</select> </select>
<select id="statCustomerRegistrationDay" resultType="com.imyeyu.api.modules.gao.vo.GaoCustomerRegistrationDayStatView">
SELECT
r.`customer_id` AS `customerId`,
c.`code` AS `customerCode`,
c.`name` AS `customerName`,
(
SELECT a.`id`
FROM `attachment` a
WHERE
a.`biz_type` = 'GAO_CUSTOMER'
AND a.`biz_id` = c.`id`
AND a.`attach_type` = 'PHOTO'
AND a.`deleted_at` IS NULL
AND (a.`destroy_at` IS NULL OR UNIX_TIMESTAMP() &lt; a.`destroy_at`)
ORDER BY a.`created_at` ASC
LIMIT 1
) AS `customerPhotoAttachmentId`,
CAST(DATE_FORMAT(FROM_UNIXTIME(r.`registered_at` / 1000), '%Y%m%d') AS UNSIGNED) AS `dateValue`,
COUNT(1) AS `dailyCount`,
MAX(r.`registered_at`) AS `latestRegisteredAt`
FROM `gao_registration_event_record` r
LEFT JOIN `gao_customer` c ON r.`customer_id` = c.`id`
WHERE
r.`deleted_at` IS NULL
AND c.`deleted_at` IS NULL
<if test="req.beginRegisteredAt != null">
AND r.`registered_at` &gt;= #{req.beginRegisteredAt}
</if>
<if test="req.endRegisteredAt != null">
AND r.`registered_at` &lt;= #{req.endRegisteredAt}
</if>
<if test="req.registrationEventId != null and req.registrationEventId != ''">
AND r.`registration_event_id` = #{req.registrationEventId}
</if>
<if test="req.registrationEventIdList != null and req.registrationEventIdList.size() > 0">
AND r.`registration_event_id` IN
<foreach collection="req.registrationEventIdList" item="registrationEventId" open="(" separator="," close=")">
#{registrationEventId}
</foreach>
</if>
GROUP BY
r.`customer_id`,
c.`code`,
c.`name`,
DATE_FORMAT(FROM_UNIXTIME(r.`registered_at` / 1000), '%Y%m%d')
ORDER BY r.`customer_id` ASC, `dateValue` DESC
</select>
<select id="statDailyCountByCustomerIdAndRegistrationEventIdRange" resultType="com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView"> <select id="statDailyCountByCustomerIdAndRegistrationEventIdRange" resultType="com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView">
SELECT SELECT
CAST(DATE_FORMAT(FROM_UNIXTIME(`registered_at` / 1000), '%Y%m%d') AS UNSIGNED) AS `dateValue`, CAST(DATE_FORMAT(FROM_UNIXTIME(`registered_at` / 1000), '%Y%m%d') AS UNSIGNED) AS `dateValue`,