Compare commits

...
35 Commits
Author SHA1 Message Date
timi 2170c48763 Merge pull request 'v1.0.8' (#15) from dev into master
Reviewed-on: #15
2026-08-03 16:36:19 +00:00
Timi 568563475e v1.0.8
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-04 00:35:57 +08:00
Timi 8a92e46eb1 unlimit attach read rate 2026-08-03 23:46:12 +08:00
Timi 1875cec8a5 add registration event rank 2026-08-03 23:45:56 +08:00
timi c2d1c48aae Merge pull request 'v0.0.18' (#14) from dev into master
Reviewed-on: #14
2026-08-03 11:59:20 +00:00
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 e14958c643 Merge pull request 'v0.0.18' (#13) from dev into master
Reviewed-on: #13
2026-08-03 11:58:16 +00: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 da288338cd Merge pull request 'v1.0.7' (#12) from dev into master
Reviewed-on: #12
2026-08-01 09:34:18 +00: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 ea430f2468 Merge pull request 'v1.0.6' (#11) from dev into master
Reviewed-on: #11
2026-08-01 09:03:48 +00: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
timi f76f79744a Merge pull request 'v1.0.5' (#10) from dev into master
Reviewed-on: #10
2026-08-01 08:52:52 +00:00
Timi 833a303bb6 v1.0.5
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-01 16:51:53 +08:00
timi 19daaff907 Merge pull request 'v1.0.4' (#9) from dev into master
Reviewed-on: #9
2026-08-01 03:13:00 +00:00
Timi 1658be7d8d v1.0.4
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-01 11:11:53 +08:00
timi db94d8f932 Merge pull request 'v1.0.3' (#8) from dev into master
Reviewed-on: #8
2026-07-31 17:19:48 +00:00
Timi 6c537852a0 v1.0.3
CI / build-deploy (pull_request) Successful in 1m8s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-01 01:19:21 +08:00
timi 53618fe362 Merge pull request 'v1.0.3' (#7) from dev into master
Reviewed-on: #7
2026-07-31 17:13:37 +00:00
Timi 15bf9bd688 v1.0.3
CI / build-deploy (pull_request) Failing after 2m52s
CI / notify-on-failure (pull_request) Successful in 0s
2026-08-01 01:13:08 +08:00
Timi ce22661a9b v1.0.3 2026-07-31 18:36:37 +08:00
timi 523edffc4e Merge pull request 'v1.0.2' (#6) from dev into master
Reviewed-on: #6
2026-04-13 10:28:11 +00:00
timi c7ddb1a8b0 Merge pull request 'v1.0.1' (#5) from dev into master
Reviewed-on: #5
2026-04-12 16:17:02 +00:00
timi 45c9fc814a Merge pull request 'v1.0.0' (#4) from dev into master
Reviewed-on: #4
2026-04-09 05:16:04 +00:00
timi 407dc13ac4 Merge pull request 'v1.0.0' (#3) from dev into master
Reviewed-on: #3
2026-04-09 04:09:16 +00:00
timi 0c06bf16c2 Merge pull request 'v1.0.0' (#2) from dev into master
Reviewed-on: #2
2026-04-09 04:03:43 +00:00
timi 1db39e77d3 Merge pull request 'v1.0.0' (#1) from dev into master
Reviewed-on: #1
2026-04-08 08:48:12 +00:00
80 changed files with 1842 additions and 366 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
outputs:
deployment_status: ${{ steps.set_status.outputs.status }}
env:
JAVA_HOME: /usr/lib/jvm/java-21-openjdk
JAVA_HOME: /usr/lib/jvm/java-25-openjdk
steps:
- name: Checkout code
uses: actions/checkout@v4
+12 -3
View File
@@ -11,7 +11,7 @@
<groupId>com.imyeyu.timiserverapi</groupId>
<artifactId>TimiServerAPI</artifactId>
<version>1.0.3</version>
<version>1.0.8</version>
<packaging>jar</packaging>
<name>TimiServerAPI</name>
<description>imyeyu.com API</description>
@@ -173,7 +173,7 @@
<dependency>
<groupId>com.imyeyu.spring</groupId>
<artifactId>timi-spring</artifactId>
<version>0.0.16</version>
<version>0.0.18</version>
</dependency>
<dependency>
<groupId>com.imyeyu.network</groupId>
@@ -183,7 +183,12 @@
<dependency>
<groupId>com.imyeyu.lang</groupId>
<artifactId>timi-lang</artifactId>
<version>0.0.4</version>
<version>0.0.5</version>
</dependency>
<dependency>
<groupId>com.imyeyu.java</groupId>
<artifactId>timi-java</artifactId>
<version>0.0.6</version>
</dependency>
<dependency>
<groupId>com.imyeyu.utils</groupId>
@@ -225,6 +230,10 @@
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
@@ -40,7 +40,7 @@ public class RequestRateLimitInterceptor extends RequestRateLimitAbstractInterce
redis.setAndKeepTTL(key, ++count);
} else {
log.warn("请求频率过高:[" + key + "].C" + count + "L" + limit);
throw new TimiException(TimiCode.REQUEST_BAD).msgKey("request_rate_limit");
throw new TimiException(TimiCode.REQUEST_BAD, "request_rate_limit");
}
}
return true;
@@ -13,7 +13,9 @@ public enum CoreRoleCode implements BuiltinRoleCode {
SYSTEM("系统", true, null),
ADMIN("管理员", false, SYSTEM.name());
ADMIN("管理员", false, SYSTEM.name()),
USER_ROLE_ADMIN("授权管理", false, ADMIN.name());
final String defaultName;
final boolean protectedRole;
@@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.imyeyu.utils.Time;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -22,6 +23,7 @@ public class BeanConfig {
@Bean
public ObjectMapper jackson() {
return JsonMapper.builder()
.addModule(new JavaTimeModule())
.serializationInclusion(JsonInclude.Include.NON_NULL)
.defaultDateFormat(Time.dateTime)
.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.ObjectMapper;
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.RequestRateLimitInterceptor;
import com.imyeyu.api.annotation.RequireCorePermissionInterceptor;
@@ -78,6 +79,7 @@ public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
JsonMapper jsonMapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
// 设置默认属性包含规则:忽略 null 值
.serializationInclusion(JsonInclude.Include.NON_NULL)
// 日期格式化
@@ -208,7 +208,6 @@ public class AttachmentController {
}
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@GetMapping({"/read/{id}", "/download/{id}"})
public void readById(@PathVariable String id, @RequestParam(required = false) Integer thumbWidth, @RequestParam(required = false) Integer thumbHeight) throws UnsupportedEncodingException {
@@ -4,6 +4,7 @@ import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.mapper.RawMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@@ -31,6 +32,8 @@ public interface AttachmentMapper extends BaseMapper<Attachment, String>, RawMap
List<Attachment> selectByBizId(Attachment.BizType bizType, String bizId, List<String> attachTypes, Page<Attachment> page);
List<Attachment> selectByBizIdList(@Param("bizType") Attachment.BizType bizType, @Param("bizIdList") List<String> bizIdList, @Param("attachTypes") List<String> attachTypes);
long countByBizId(Attachment.BizType bizType, String bizId, List<String> attachTypes);
/**
@@ -9,6 +9,7 @@ import com.imyeyu.spring.service.BaseService;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
/**
* 附件服务
@@ -50,6 +51,16 @@ public interface AttachmentService extends BaseService<Attachment, String> {
*/
List<Attachment> listByBizId(Attachment.BizType bizType, String bizId, String... attachTypes);
/**
* 根据多个业务 ID 批量获取附件。
*
* @param bizType 业务类型
* @param bizIdList 业务 ID 列表
* @param attachTypes 附件类型,可为 null
* @return 按业务 ID 分组的附件列表
*/
Map<String, List<Attachment>> mapByBizIdList(Attachment.BizType bizType, List<String> bizIdList, String... attachTypes);
/**
* 根据业务获取所有附件
*
@@ -43,7 +43,9 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@@ -97,7 +99,7 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
}
} catch (Exception e) {
log.error("delete mongo file error, id={}", attach.getId(), e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO delete mongo file error");
throw new TimiException(TimiCode.ERROR, "TODO delete mongo file error");
}
};
mapper.selectThumbsBySourceId(id).forEach(doDestroy::handler);
@@ -145,7 +147,7 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(mongoId)));
}
log.error("create error", e);
throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO read attachment input stream error");
throw new TimiException(TimiCode.ARG_BAD, "TODO read attachment input stream error");
}
}
@@ -200,7 +202,7 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(newMongoId)));
}
log.error("update error", e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO update file error");
throw new TimiException(TimiCode.ERROR, "TODO update file error");
}
}
@@ -271,7 +273,7 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
return thumb;
} catch (Exception e) {
log.error("create thumbnail error", e);
throw new TimiException(TimiCode.ERROR).msgKey("exception.attachment.create.error");
throw new TimiException(TimiCode.ERROR, "exception.attachment.create.error");
}
}
@@ -310,6 +312,22 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
return mapper.selectByBizId(bizType, bizId, List.of(attachTypes), null);
}
@Override
public Map<String, List<Attachment>> mapByBizIdList(Attachment.BizType bizType, List<String> bizIdList, String... attachTypes) {
if (bizIdList == null || bizIdList.isEmpty()) {
return Map.of();
}
List<String> normalizedBizIdList = bizIdList.stream()
.filter(TimiJava::isNotEmpty)
.distinct()
.toList();
if (normalizedBizIdList.isEmpty()) {
return Map.of();
}
return mapper.selectByBizIdList(bizType, new ArrayList<>(normalizedBizIdList), List.of(attachTypes)).stream()
.collect(Collectors.groupingBy(Attachment::getBizId));
}
@Override
public long countByBizId(Attachment.BizType bizType, String bizId, String... attachTypes) {
return mapper.countByBizId(bizType, bizId, List.of(attachTypes));
@@ -354,7 +372,7 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
if (isNew) {
// 请求有、数据库无:将临时文件记录直接转存为永久附件
if (TimiJava.isEmpty(tempFileId)) {
throw new TimiException(TimiCode.ERROR).msgKey("exception.attachment.file.required");
throw new TimiException(TimiCode.ERROR, "exception.attachment.file.required");
}
Attachment tempFile = mapper.selectRaw(tempFileId);
tempFile.setBizType(bizType);
@@ -51,11 +51,11 @@ public class ClipboardServiceImplement implements ClipboardService {
public void setContent(String id, String content) {
validateId(id);
if (content == null) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("剪切板内容不能为空");
throw new TimiException(TimiCode.ARG_MISS, "剪切板内容不能为空");
}
byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
if (MAX_CONTENT_SIZE < contentBytes.length) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("剪切板内容不能超过 10MB");
throw new TimiException(TimiCode.ARG_BAD, "剪切板内容不能超过 10MB");
}
redisClipboard.set(getKey(id), content, Time.D * 7);
notifySubscribers(id, content);
@@ -78,7 +78,7 @@ public class ClipboardServiceImplement implements ClipboardService {
private void validateId(String id) {
if (TimiJava.isEmpty(id)) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("id 不能为空");
throw new TimiException(TimiCode.ARG_MISS, "id 不能为空");
}
}
@@ -39,8 +39,10 @@ public class MultilingualServiceImplement extends AbstractEntityService<Multilin
Multilingual result = redisLanguage.get(getKey(id));
if (result == null) {
result = super.get(id);
if (result != null) {
redisLanguage.set(getKey(id), result, Time.D * 7);
}
}
return result;
}
@@ -46,8 +46,8 @@ public class TempFileServiceImplement implements TempFileService {
public List<TempFileResp> create(List<MultipartFile> files, Long ttl) throws TimiException {
Logger logger = new Logger();
logger.setModule("TEMP_FILE");
logger.setOperation("STORE");
logger.setModule(Logger.Module.COMMON);
logger.setOperation("TEMP_FILE_STORE");
logger.setIp(TimiSpring.getRequestIP());
try {
logger.setContent(jackson.writeValueAsString(logRequestArgs(files, ttl)));
@@ -116,8 +116,8 @@ public class TempFileServiceImplement implements TempFileService {
@Override
public void save(TempFileSaveReq request) {
Logger logger = new Logger();
logger.setModule("TEMP_FILE");
logger.setOperation("SAVE");
logger.setModule(Logger.Module.COMMON);
logger.setOperation("TEMP_FILE_SAVE");
logger.setIp(TimiSpring.getRequestIP());
try {
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_EXPORT("CUSTOMER:EXPORT", "客户导出", true),
CUSTOMER_UPDATE("CUSTOMER:UPDATE", "客户修改"),
CUSTOMER_DELETE("CUSTOMER:DELETE", "客户删除"),
@@ -30,6 +32,8 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
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_DELETE("REGISTRATION_EVENT_RECORD:DELETE", "登记事件记录删除"),
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.gao.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.bean.RoleMatchMode;
@@ -12,6 +13,7 @@ import com.imyeyu.api.modules.gao.service.GaoRegistrationEventService;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventRecordService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerInviteUpdateReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerSearchReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStatReq;
@@ -21,6 +23,7 @@ import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
@@ -57,6 +60,7 @@ public class GaoCustomerController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/list")
@JsonView(ResponseView.Public.class)
public PageResult<GaoCustomer> list(@RequestBody Page<GaoCustomer> page) {
PageResult<GaoCustomer> result = service.page(page);
fillLastSignInAt(result.getList());
@@ -67,6 +71,7 @@ public class GaoCustomerController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/search")
@JsonView(ResponseView.Public.class)
public List<GaoCustomer> search(@RequestBody GaoCustomerSearchReq req) {
return service.search(req);
}
@@ -75,6 +80,7 @@ public class GaoCustomerController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/search/page")
@JsonView(ResponseView.Public.class)
public PageResult<GaoCustomer> searchPage(@RequestBody Page<GaoCustomerSearchReq> page) {
return service.searchPage(page);
}
@@ -82,7 +88,7 @@ public class GaoCustomerController {
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_EXPORT)
@PostMapping("/export")
public void export(@RequestBody GaoCustomerSearchReq req, HttpServletResponse resp) throws IOException {
String fileName = URLEncoder.encode("客户列表.xlsx", "UTF-8").replace("+", "%20");
@@ -95,6 +101,7 @@ public class GaoCustomerController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/detail")
@JsonView(ResponseView.Public.class)
public GaoCustomer detail(@RequestParam String id) {
return service.get(id);
}
@@ -103,14 +110,25 @@ public class GaoCustomerController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/detail/code")
@JsonView(ResponseView.Public.class)
public GaoCustomer detailByCode(@RequestParam String code) {
return service.getByCode(code);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/detail/code/nullable")
@JsonView(ResponseView.Public.class)
public GaoCustomer nullableDetailByCode(@RequestParam String code) {
return service.getNullableByCode(code);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/invited/list")
@JsonView(ResponseView.Public.class)
public List<GaoCustomer> invitedList(@RequestParam String introducerCustomerId) {
return service.listInvited(introducerCustomerId);
}
@@ -119,6 +137,7 @@ public class GaoCustomerController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_CREATE)
@PostMapping("/create")
@JsonView(ResponseView.Public.class)
public GaoCustomer create(@RequestBody GaoCustomer customer) {
service.create(customer);
return customer;
@@ -127,6 +146,7 @@ public class GaoCustomerController {
@AOPLog
@RequestRateLimit
@PostMapping("/quick/register")
@JsonView(ResponseView.Public.class)
public GaoRegistrationEventRecord quickRegister(@RequestBody GaoQuickRegistrationEventRecordReq req) {
return registrationEventRecordService.quickRegister(req);
}
@@ -159,6 +179,7 @@ public class GaoCustomerController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/invite/detail")
@JsonView(ResponseView.Public.class)
public GaoCustomer inviteDetail(@RequestParam String customerId) {
return service.get(customerId);
}
@@ -188,6 +209,15 @@ public class GaoCustomerController {
return service.statDailyTrend(req);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/stat/gender")
/// 统计客户性别分布
public List<GaoCustomerGenderStatView> genderStat() {
return service.statGender();
}
private void fillLastSignInAt(List<GaoCustomer> customerList) {
if (customerList == null || customerList.isEmpty()) {
return;
@@ -72,6 +72,14 @@ public class GaoRegistrationEventController {
service.update(registrationEvent);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_UPDATE)
@PostMapping("/sort")
public void sort(@RequestBody List<String> idList) {
service.sort(idList);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_DELETE)
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.gao.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.bean.RoleMatchMode;
@@ -9,8 +10,11 @@ import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventRecordService;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView;
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.GaoRegistrationEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordListItemView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordPeriodReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordRangeReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordStatReq;
@@ -20,6 +24,7 @@ import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
@@ -49,6 +54,7 @@ public class GaoRegistrationEventRecordController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ)
@PostMapping("/list")
@JsonView(ResponseView.Public.class)
public PageResult<GaoRegistrationEventRecord> list(@RequestBody Page<GaoRegistrationEventRecord> page) {
return service.page(page);
}
@@ -57,7 +63,7 @@ public class GaoRegistrationEventRecordController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ)
@PostMapping("/range/page")
public PageResult<GaoRegistrationEventRecord> rangePage(@RequestBody Page<GaoRegistrationEventRecordRangeReq> page) {
public PageResult<GaoRegistrationEventRecordListItemView> rangePage(@RequestBody Page<GaoRegistrationEventRecordRangeReq> page) {
return service.pageByRange(page);
}
@@ -65,6 +71,7 @@ public class GaoRegistrationEventRecordController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ)
@PostMapping("/detail")
@JsonView(ResponseView.Public.class)
public GaoRegistrationEventRecord detail(@RequestParam String id) {
return service.get(id);
}
@@ -72,8 +79,9 @@ public class GaoRegistrationEventRecordController {
@AOPLog
@RequestRateLimit
@PostMapping("/create")
public GaoRegistrationEventRecord create(@RequestBody GaoRegistrationEventRecordCreateReq req) {
return service.createRecord(req);
@JsonView(ResponseView.Public.class)
public List<GaoRegistrationEventRecord> create(@RequestBody GaoRegistrationEventRecordCreateReq req) {
return service.createRecords(req);
}
@AOPLog
@@ -96,6 +104,7 @@ public class GaoRegistrationEventRecordController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ)
@PostMapping("/period/list")
@JsonView(ResponseView.Public.class)
public List<GaoRegistrationEventRecord> periodList(@RequestBody GaoRegistrationEventRecordPeriodReq req) {
return service.listByRegistrationEventAndPeriod(req);
}
@@ -104,14 +113,23 @@ public class GaoRegistrationEventRecordController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ)
@PostMapping("/range/list")
@JsonView(ResponseView.Public.class)
public List<GaoRegistrationEventRecord> rangeList(@RequestBody GaoRegistrationEventRecordRangeReq req) {
return service.listByRange(req);
}
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ)
@PostMapping("/range/calendar/list")
public List<GaoRegistrationEventRecordCalendarView> calendarRangeList(@RequestBody GaoRegistrationEventRecordRangeReq req) {
return service.listCalendarByRange(req);
}
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_EXPORT)
@PostMapping("/range/export")
public void rangeExport(@RequestBody GaoRegistrationEventRecordRangeReq req, HttpServletResponse resp) throws IOException {
String fileName = URLEncoder.encode("登记事件记录.xlsx", "UTF-8").replace("+", "%20");
@@ -154,6 +172,15 @@ public class GaoRegistrationEventRecordController {
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
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.gao.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.annotation.RequireGaoRole;
@@ -13,6 +14,7 @@ import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -40,6 +42,7 @@ public class GaoUserController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_READ)
@PostMapping("/list")
@JsonView(ResponseView.Public.class)
public PageResult<GaoUser> list(@RequestBody Page<GaoUser> page) {
return service.pageWithUser(page);
}
@@ -48,6 +51,7 @@ public class GaoUserController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_READ)
@PostMapping("/detail")
@JsonView(ResponseView.Public.class)
public GaoUser detail(@RequestParam String id) {
return service.get(id);
}
@@ -56,6 +60,7 @@ public class GaoUserController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_READ)
@PostMapping("/detail/user")
@JsonView(ResponseView.Public.class)
public GaoUser detailByUserId(@RequestParam String userId) {
return service.getByUserId(userId);
}
@@ -64,6 +69,7 @@ public class GaoUserController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_CREATE)
@PostMapping("/create")
@JsonView(ResponseView.Public.class)
public GaoUser create(@RequestBody GaoUser gaoUser) {
service.create(gaoUser);
return service.get(gaoUser.getId());
@@ -73,6 +79,7 @@ public class GaoUserController {
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_UPDATE)
@PostMapping("/update")
@JsonView(ResponseView.Public.class)
public GaoUser update(@RequestBody GaoUser gaoUser) {
service.update(gaoUser);
return service.get(gaoUser.getId());
@@ -1,13 +1,17 @@
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.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.bean.Gender;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
import java.util.List;
///
@@ -29,6 +33,18 @@ public class GaoCustomer extends UUIDEntity {
PHOTO
}
///
/// 出生日期使用的历法
///
public enum BirthdateCalendar {
/// 公历
SOLAR,
/// 农历
LUNAR
}
/// 客户编码
private String code;
@@ -41,6 +57,22 @@ public class GaoCustomer extends UUIDEntity {
/// 年龄
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;
@@ -59,6 +91,9 @@ public class GaoCustomer extends UUIDEntity {
/// 介绍人客户 ID
private String introducerCustomerId;
/// 录入用户 ID
private String creatorUserId;
@Transient
private Long lastSignInAt;
@@ -66,6 +101,8 @@ public class GaoCustomer extends UUIDEntity {
private GaoCustomer introducerCustomer;
@Transient
private User creatorUser;
private Long invitedCount;
@Transient
@@ -16,18 +16,82 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true)
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 description;
/// true 为启用
private Boolean enabled;
/// 统计图表颜色
private String color;
/// 状态
private Status status;
/// true 为登记时要求拍照
private Boolean requirePhoto;
/// 登记限制类型
private LimitType limitType;
/// 登记限制数值,按限制类型表示小时数或次数
private Integer limitValue;
/// 可登记起始时间戳,毫秒,空为不限制
private Long beginAt;
/// 可登记终止时间戳,毫秒,空为不限制
private Long endAt;
/// 排序值
private Integer sort;
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@@ -18,6 +19,11 @@ public interface GaoCustomerMapper extends BaseMapper<GaoCustomer, String> {
@Select("SELECT * FROM `gao_customer` WHERE `code` = #{code} AND `deleted_at` IS NULL LIMIT 1")
GaoCustomer selectByCode(@Param("code") String code);
@Select("SELECT * FROM `gao_customer` WHERE `name` = #{name} AND `deleted_at` IS NULL LIMIT 1")
GaoCustomer selectByName(@Param("name") String name);
List<GaoCustomer> selectByIdList(@Param("idList") List<String> idList);
List<GaoCustomer> search(@Param("keyword") String keyword, @Param("introducerCustomerId") String introducerCustomerId);
Long countSearch(@Param("keyword") String keyword, @Param("introducerCustomerId") String introducerCustomerId);
@@ -28,6 +34,8 @@ public interface GaoCustomerMapper extends BaseMapper<GaoCustomer, String> {
Long countInvited(@Param("introducerCustomerId") String introducerCustomerId);
void updateInvitedCountDelta(@Param("customerId") String customerId, @Param("delta") Long delta);
/// 统计每日新增客户数
///
/// @param beginCreatedAt 开始创建时间
@@ -40,4 +48,9 @@ public interface GaoCustomerMapper extends BaseMapper<GaoCustomer, String> {
/// @param beginCreatedAt 开始创建时间
/// @return 客户总数
Long countCreatedBefore(@Param("beginCreatedAt") Long beginCreatedAt);
/// 统计客户性别分布
///
/// @return 性别统计列表
List<GaoCustomerGenderStatView> statGender();
}
@@ -2,7 +2,7 @@ package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -13,6 +13,7 @@ import java.util.List;
/// @since 2026-07-27
public interface GaoRegistrationEventMapper extends BaseMapper<GaoRegistrationEvent, String> {
@Select("SELECT * FROM `gao_registration_event` WHERE `enabled` = TRUE AND `deleted_at` IS NULL ORDER BY `sort` ASC, `created_at` ASC")
List<GaoRegistrationEvent> selectEnabledList();
List<GaoRegistrationEvent> selectByIdList(@Param("idList") List<String> idList);
List<GaoRegistrationEvent> selectEnabledList(@Param("now") Long now);
}
@@ -3,7 +3,10 @@ package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView;
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.GaoRegistrationEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordListItemView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordPeriodReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordRangeReq;
import com.imyeyu.spring.mapper.BaseMapper;
@@ -25,6 +28,28 @@ public interface GaoRegistrationEventRecordMapper extends BaseMapper<GaoRegistra
/// @return 最后登记事件记录列表
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
@@ -53,6 +78,12 @@ public interface GaoRegistrationEventRecordMapper extends BaseMapper<GaoRegistra
/// @return 统计结果
List<GaoCustomerRegisterStatView> statAllCustomerByRange(@Param("beginRegisteredAt") Long beginRegisteredAt, @Param("endRegisteredAt") Long endRegisteredAt);
/// 按时间范围统计全部客户每日登记
///
/// @param req 范围查询请求
/// @return 每个有登记客户的每日统计
List<GaoCustomerRegistrationDayStatView> statCustomerRegistrationDay(@Param("req") GaoRegistrationEventRecordRangeReq req);
/// 统计单个客户单个登记事件的每日登记事件记录数
///
/// @param customerId 客户 ID
@@ -104,6 +135,12 @@ public interface GaoRegistrationEventRecordMapper extends BaseMapper<GaoRegistra
/// @return 登记事件记录列表
List<GaoRegistrationEventRecord> selectByRange(@Param("req") GaoRegistrationEventRecordRangeReq req);
/// 按任意时间范围查询日历轻量记录
///
/// @param req 范围查询请求
/// @return 日历轻量记录列表
List<GaoRegistrationEventRecordCalendarView> selectCalendarByRange(@Param("req") GaoRegistrationEventRecordRangeReq req);
/// 按任意时间范围统计登记事件记录
///
/// @param req 范围查询请求
@@ -116,5 +153,5 @@ public interface GaoRegistrationEventRecordMapper extends BaseMapper<GaoRegistra
/// @param offset 偏移量
/// @param size 每页条数
/// @return 登记事件记录列表
List<GaoRegistrationEventRecord> selectPageByRange(@Param("req") GaoRegistrationEventRecordRangeReq req, @Param("offset") Long offset, @Param("size") Long size);
List<GaoRegistrationEventRecordListItemView> selectPageByRange(@Param("req") GaoRegistrationEventRecordRangeReq req, @Param("offset") Long offset, @Param("size") Long size);
}
@@ -18,6 +18,19 @@ public interface GaoRegistrationEventRoleRelationMapper extends BaseMapper<GaoRe
@Select("SELECT `role_code` FROM `gao_registration_event_role_relation` WHERE `registration_event_id` = #{registrationEventId} AND `deleted_at` IS NULL ORDER BY `created_at` ASC")
List<String> selectRoleCodeListByRegistrationEventId(@Param("registrationEventId") String registrationEventId);
@Select({
"<script>",
"SELECT * FROM `gao_registration_event_role_relation`",
"WHERE `registration_event_id` IN",
"<foreach collection='registrationEventIdList' item='registrationEventId' open='(' separator=',' close=')'>",
"#{registrationEventId}",
"</foreach>",
"AND `deleted_at` IS NULL",
"ORDER BY `created_at` ASC",
"</script>"
})
List<GaoRegistrationEventRoleRelation> selectByRegistrationEventIdList(@Param("registrationEventIdList") List<String> registrationEventIdList);
@Update("UPDATE `gao_registration_event_role_relation` SET `deleted_at` = UNIX_TIMESTAMP() WHERE `registration_event_id` = #{registrationEventId} AND `deleted_at` IS NULL")
void deleteByRegistrationEventId(@Param("registrationEventId") String registrationEventId);
}
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerInviteUpdateReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerSearchReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStatReq;
@@ -76,4 +77,9 @@ public interface GaoCustomerService extends BaseService<GaoCustomer, String> {
/// @param req 趋势统计请求
/// @return 每日趋势列表
List<GaoCustomerDailyStatView> statDailyTrend(GaoCustomerTrendStatReq req);
/// 统计客户性别分布
///
/// @return 性别统计列表
List<GaoCustomerGenderStatView> statGender();
}
@@ -3,9 +3,12 @@ package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView;
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.GaoRegistrationEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordListItemView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordPeriodReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordRangeReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordStatReq;
@@ -24,11 +27,11 @@ import java.util.List;
/// @since 2026-07-27
public interface GaoRegistrationEventRecordService extends BaseService<GaoRegistrationEventRecord, String> {
/// 创建登记事件记录
/// 批量创建登记事件记录
///
/// @param req 创建请求
/// @return 创建后的登记事件记录
GaoRegistrationEventRecord createRecord(GaoRegistrationEventRecordCreateReq req);
/// @param req 批量创建请求
/// @return 创建后的登记事件记录列表
List<GaoRegistrationEventRecord> createRecords(GaoRegistrationEventRecordCreateReq req);
/// 快速录入客户并创建登记事件记录
///
@@ -42,11 +45,17 @@ public interface GaoRegistrationEventRecordService extends BaseService<GaoRegist
/// @return 登记事件记录列表
List<GaoRegistrationEventRecord> listByRange(GaoRegistrationEventRecordRangeReq req);
/// 按任意时间范围查询日历轻量记录
///
/// @param req 范围查询请求
/// @return 日历轻量记录列表
List<GaoRegistrationEventRecordCalendarView> listCalendarByRange(GaoRegistrationEventRecordRangeReq req);
/// 按任意时间范围分页查询登记事件记录
///
/// @param page 分页参数
/// @return 登记事件记录分页结果
PageResult<GaoRegistrationEventRecord> pageByRange(Page<GaoRegistrationEventRecordRangeReq> page);
PageResult<GaoRegistrationEventRecordListItemView> pageByRange(Page<GaoRegistrationEventRecordRangeReq> page);
/// 按任意时间范围导出登记事件记录 Excel
///
@@ -92,6 +101,12 @@ public interface GaoRegistrationEventRecordService extends BaseService<GaoRegist
/// @return 统计结果
List<GaoCustomerRegisterStatView> statAllCustomerByRange(GaoRegistrationEventRecordStatReq req);
/// 按时间范围查询客户登记排行榜
///
/// @param req 范围查询请求,不传时间为全部时间
/// @return 客户登记排行榜
List<GaoCustomerRegistrationRankView> listCustomerRegistrationRank(GaoRegistrationEventRecordRangeReq req);
/// 按时间段统计单个客户单个登记事件每日趋势
///
/// @param req 趋势统计请求
@@ -30,4 +30,9 @@ public interface GaoRegistrationEventService extends BaseService<GaoRegistration
/// @param registrationEventId 登记事件 ID
/// @return 角色代码列表
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;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
@@ -8,17 +9,24 @@ import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.mapper.GaoCustomerMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerInviteUpdateReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerSearchReq;
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.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
@@ -32,6 +40,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.Period;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
@@ -39,12 +48,15 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
///
/// 客户服务实现
///
/// @author Codex
/// @since 2026-07-27
@Slf4j
@Service
@RequiredArgsConstructor
public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustomer, String> implements GaoCustomerService {
@@ -53,32 +65,113 @@ 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 final GaoCustomerMapper mapper;
private final ObjectMapper jackson;
private final AttachmentService attachmentService;
private final LoggerService loggerService;
private final UserLoginService userLoginService;
private final UserService userService;
@Override
protected BaseMapper<GaoCustomer, String> mapper() {
return mapper;
}
@Override
public PageResult<GaoCustomer> page(Page<GaoCustomer> page) {
PageResult<GaoCustomer> result = super.page(page);
loadListTransient(result.getList());
return result;
}
@Override
public void create(GaoCustomer entity) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_CUSTOMER_CREATE");
try {
logger.setContent(jackson.writeValueAsString(entity));
checkEntity(entity, false);
entity.setCreatorUserId(userLoginService.getRequireLoginUserId());
entity.setInvitedCount(0L);
super.create(entity);
saveAttachment(entity.getId(), resolveAttachmentList(entity));
loadTransient(entity);
changeInvitedCount(entity.getIntroducerCustomerId(), 1L);
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
public void update(GaoCustomer entity) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_CUSTOMER_UPDATE");
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);
super.update(entity);
mapper.update(entity);
saveAttachment(entity.getId(), resolveAttachmentList(entity));
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)
@Override
public void delete(String id) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_CUSTOMER_DELETE");
try {
logger.setContent(id);
GaoCustomer customer = super.get(id);
super.delete(id);
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
public GaoCustomer get(String id) {
GaoCustomer customer = super.get(id);
loadTransient(customer);
loadDetailTransient(customer);
return customer;
}
@@ -87,7 +180,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
TimiException.required(code, "not found customer.code");
GaoCustomer customer = mapper.selectByCode(code);
TimiException.required(customer, "not found customer");
loadTransient(customer);
loadDetailTransient(customer);
return customer;
}
@@ -95,7 +188,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
public GaoCustomer getNullableByCode(String code) {
TimiException.required(code, "not found customer.code");
GaoCustomer customer = mapper.selectByCode(code);
loadTransient(customer);
loadDetailTransient(customer);
return customer;
}
@@ -117,7 +210,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
public List<GaoCustomer> search(GaoCustomerSearchReq req) {
req = resolveSearchReq(req);
List<GaoCustomer> list = mapper.search(req.getKeyword(), req.getIntroducerCustomerId());
list.forEach(this::loadTransient);
loadListTransient(list);
return list;
}
@@ -131,7 +224,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
GaoCustomerSearchReq req = resolveSearchReq(page.getEqualsExample());
Long total = TimiJava.defaultIfNull(mapper.countSearch(req.getKeyword(), req.getIntroducerCustomerId()), 0L);
List<GaoCustomer> list = mapper.searchPage(req.getKeyword(), req.getIntroducerCustomerId(), page.getIndex() * page.getSize(), page.getSize());
list.forEach(this::loadTransient);
loadListTransient(list);
PageResult<GaoCustomer> result = new PageResult<>();
result.setTotal(total);
result.setList(list);
@@ -141,6 +234,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
@Override
public byte[] exportSearchExcel(GaoCustomerSearchReq req) throws IOException {
List<GaoCustomer> list = search(req);
loadIntroducerCustomer(list);
try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) {
Sheet sheet = workbook.createSheet("客户列表");
CellStyle titleStyle = workbook.createCellStyle();
@@ -157,13 +251,15 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
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, 3, null, "记录总数", String.valueOf(list.size()));
appendRow(sheet, 5, headerStyle, "客户编码", "姓名", "性别", "年龄", "电话", "地址", "病症", "备注", "邀请人", "邀请客户数", "创建时间");
appendRow(sheet, 5, headerStyle, "客户编码", "姓名", "性别", "出生日期(公历)", "出生日期历法", "年龄", "电话", "地址", "病症", "备注", "邀请人", "邀请客户数", "创建时间");
int rowIndex = 6;
for (GaoCustomer customer : list) {
appendRow(sheet, rowIndex, null,
text(customer.getCode()),
text(customer.getName()),
formatGender(customer),
text(customer.getBirthdate()),
formatBirthdateCalendar(customer),
customer.getAge() == null ? "" : String.valueOf(customer.getAge()),
text(customer.getTelephone()),
text(customer.getAddress()),
@@ -175,7 +271,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
);
rowIndex++;
}
for (int index = 0; index < 11; index++) {
for (int index = 0; index < 13; index++) {
sheet.autoSizeColumn(index);
sheet.setColumnWidth(index, Math.min(Math.max(sheet.getColumnWidth(index), 10 * 256), 36 * 256));
}
@@ -188,28 +284,66 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
public List<GaoCustomer> listInvited(String introducerCustomerId) {
TimiException.required(introducerCustomerId, "not found introducerCustomerId");
List<GaoCustomer> list = mapper.selectInvitedList(introducerCustomerId);
list.forEach(this::loadTransient);
loadListTransient(list);
return list;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
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.getCustomerId(), "not found invite req.customerId");
GaoCustomer customer = super.get(req.getCustomerId());
String oldIntroducerCustomerId = customer.getIntroducerCustomerId();
customer.setIntroducerCustomerId(req.getIntroducerCustomerId());
checkEntity(customer, true);
super.update(customer);
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)
@Override
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");
GaoCustomer customer = super.get(customerId);
String oldIntroducerCustomerId = customer.getIntroducerCustomerId();
customer.setIntroducerCustomerId(null);
super.update(customer);
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
@@ -243,6 +377,11 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
return list;
}
@Override
public List<GaoCustomerGenderStatView> statGender() {
return mapper.statGender();
}
private void checkDailyStatRange(LocalDate beginDate, LocalDate endDate) {
long days = ChronoUnit.DAYS.between(beginDate, endDate) + 1;
TimiException.requiredTrue(days <= 366, "daily stat range too large");
@@ -252,33 +391,132 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
return req == null ? new GaoCustomerSearchReq() : req;
}
private void checkEntity(GaoCustomer entity, boolean requireId) {
private GaoCustomer checkEntity(GaoCustomer entity, boolean requireId) {
TimiException.required(entity, "not found customer");
GaoCustomer dbCustomer = null;
entity.setCode(normalizeText(entity.getCode()));
entity.setIntroducerCustomerId(normalizeText(entity.getIntroducerCustomerId()));
if (entity.getBirthdateCalendar() == null) {
entity.setBirthdateCalendar(GaoCustomer.BirthdateCalendar.SOLAR);
}
refreshAge(entity);
if (requireId) {
TimiException.required(entity.getId(), "not found customer.id");
dbCustomer = super.get(entity.getId());
entity.setCreatorUserId(dbCustomer.getCreatorUserId());
}
TimiException.required(entity.getName(), "not found customer.name");
entity.setName(entity.getName().trim());
TimiException.requiredTrue(!entity.getName().isEmpty(), "not found customer.name");
GaoCustomer duplicateCustomer = mapper.selectByName(entity.getName());
if (duplicateCustomer != null && !duplicateCustomer.getId().equals(entity.getId())) {
throw new TimiException(TimiCode.ARG_BAD, "客户姓名已存在");
}
if (TimiJava.isNotEmpty(entity.getCode())) {
GaoCustomer dbCustomer = mapper.selectByCode(entity.getCode());
if (dbCustomer != null && !dbCustomer.getId().equals(entity.getId())) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("客户编码已存在");
duplicateCustomer = mapper.selectByCode(entity.getCode());
if (duplicateCustomer != null && !duplicateCustomer.getId().equals(entity.getId())) {
throw new TimiException(TimiCode.ARG_BAD, "客户编码已存在");
}
}
if (TimiJava.isNotEmpty(entity.getIntroducerCustomerId())) {
TimiException.requiredTrue(!entity.getIntroducerCustomerId().equals(entity.getId()), "customer.introducerCustomerId invalid");
TimiException.required(super.get(entity.getIntroducerCustomerId()), "not found introducer customer");
}
return dbCustomer;
}
private void loadTransient(GaoCustomer customer) {
private void loadDetailTransient(GaoCustomer customer) {
if (customer == null) {
return;
}
if (TimiJava.isNotEmpty(customer.getIntroducerCustomerId())) {
customer.setIntroducerCustomer(super.get(customer.getIntroducerCustomerId()));
refreshAge(customer);
loadIntroducerCustomer(List.of(customer));
if (TimiJava.isNotEmpty(customer.getCreatorUserId())) {
customer.setCreatorUser(userService.get(customer.getCreatorUserId()));
}
customer.setInvitedCount(mapper.countInvited(customer.getId()));
customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId(), GaoCustomer.AttachType.PHOTO.name()));
List<GaoCustomer> detailCustomerList = new ArrayList<>();
detailCustomerList.add(customer);
if (customer.getIntroducerCustomer() != null) {
detailCustomerList.add(customer.getIntroducerCustomer());
}
loadAttachmentList(detailCustomerList);
}
private void loadListTransient(List<GaoCustomer> customerList) {
if (customerList != null) {
customerList.forEach(this::refreshAge);
}
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) {
if (customerList == null || customerList.isEmpty()) {
return;
}
List<String> introducerCustomerIdList = customerList.stream()
.map(GaoCustomer::getIntroducerCustomerId)
.filter(TimiJava::isNotEmpty)
.distinct()
.toList();
if (introducerCustomerIdList.isEmpty()) {
return;
}
Map<String, GaoCustomer> introducerCustomerMap = mapper.selectByIdList(new ArrayList<>(introducerCustomerIdList)).stream()
.collect(Collectors.toMap(GaoCustomer::getId, item -> item, (left, right) -> left));
for (GaoCustomer customer : customerList) {
customer.setIntroducerCustomer(introducerCustomerMap.get(customer.getIntroducerCustomerId()));
}
}
private void loadAttachmentList(List<GaoCustomer> customerList) {
if (customerList == null || customerList.isEmpty()) {
return;
}
List<String> customerIdList = customerList.stream()
.map(GaoCustomer::getId)
.filter(TimiJava::isNotEmpty)
.distinct()
.toList();
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList, GaoCustomer.AttachType.PHOTO.name());
for (GaoCustomer customer : customerList) {
customer.setAttachmentList(attachmentMap.getOrDefault(customer.getId(), List.of()));
}
}
private void syncIntroducerInvitedCount(String oldIntroducerCustomerId, String newIntroducerCustomerId) {
String normalizedOldIntroducerCustomerId = normalizeText(oldIntroducerCustomerId);
String normalizedNewIntroducerCustomerId = normalizeText(newIntroducerCustomerId);
if (Objects.equals(normalizedOldIntroducerCustomerId, normalizedNewIntroducerCustomerId)) {
return;
}
changeInvitedCount(normalizedOldIntroducerCustomerId, -1L);
changeInvitedCount(normalizedNewIntroducerCustomerId, 1L);
}
private void changeInvitedCount(String customerId, long delta) {
String normalizedCustomerId = normalizeText(customerId);
if (normalizedCustomerId == null || delta == 0) {
return;
}
mapper.updateInvitedCountDelta(normalizedCustomerId, delta);
}
private String normalizeText(String value) {
if (value == null) {
return null;
}
String trimmedValue = value.trim();
return trimmedValue.isEmpty() ? null : trimmedValue;
}
private void saveAttachment(String customerId, List<Attachment> attachmentList) {
@@ -338,6 +576,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) {
if (value == null) {
return "";
@@ -1,5 +1,6 @@
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.bean.ModuleCode;
import com.imyeyu.api.modules.common.entity.Attachment;
@@ -8,30 +9,42 @@ import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord;
import com.imyeyu.api.modules.gao.mapper.GaoCustomerMapper;
import com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventMapper;
import com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventService;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventRecordService;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView;
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.GaoRegistrationEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordListItemView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordPeriodReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordRangeReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordStatReq;
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.UserLoginService;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.api.modules.user.mapper.UserMapper;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
@@ -42,6 +55,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
@@ -50,26 +64,35 @@ import java.time.temporal.ChronoUnit;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.stream.Collectors;
///
/// 登记事件记录服务实现
///
/// @author Codex
/// @since 2026-07-27
@Slf4j
@Service
@RequiredArgsConstructor
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 final GaoRegistrationEventRecordMapper mapper;
private final ObjectMapper jackson;
private final GaoCustomerMapper customerMapper;
private final GaoRegistrationEventMapper registrationEventMapper;
private final UserMapper userMapper;
private final AttachmentService attachmentService;
private final LoggerService loggerService;
private final RoleChecker roleChecker;
private final UserLoginService userLoginService;
private final UserService userService;
@@ -83,45 +106,123 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
@Transactional(TimiServerDBConfig.ROLLBACKER)
@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.getRegistrationEventId(), "not found req.registrationEventId");
String customerId = resolveCustomerId(req);
GaoRegistrationEvent registrationEvent = registrationEventService.get(req.getRegistrationEventId());
checkRegistrationEventPermission(registrationEvent);
if (Boolean.TRUE.equals(registrationEvent.getRequirePhoto())) {
TimiException.requiredTrue(req.getAttachmentIdList() != null && !req.getAttachmentIdList().isEmpty(), "required record photo");
TimiException.requiredTrue(req.getCustomerIdList() != null && !req.getCustomerIdList().isEmpty(), "customerIdList is empty");
logger.setContent(jackson.writeValueAsString(req));
List<GaoRegistrationEventRecord> recordList = new ArrayList<>();
for (int index = 0; index < req.getCustomerIdList().size(); index++) {
String customerId = req.getCustomerIdList().get(index);
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();
record.setCustomerId(customerId);
record.setRegistrationEventId(req.getRegistrationEventId());
record.setOperatorUserId(userLoginService.getRequireLoginUserId());
record.setRemark(req.getRemark());
record.setRegisteredAt(TimiJava.defaultIfNull(req.getRegisteredAt(), Time.now()));
super.create(record);
saveAttachment(record.getId(), buildPhotoAttachmentList(req.getAttachmentIdList()));
return get(record.getId());
private List<String> duplicateTempFileList(List<String> sourceIdList) {
if (sourceIdList == null) {
return null;
}
List<String> targetIdList = new ArrayList<>();
for (String sourceId : sourceIdList) {
if (sourceId == null || sourceId.isBlank()) {
continue;
}
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)
@Override
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.getCustomerCode(), "not found req.customerCode");
TimiException.required(req.getRegistrationEventId(), "not found req.registrationEventId");
GaoCustomer customer = customerService.getOrQuickCreateByCode(req.getCustomerCode());
GaoRegistrationEventRecordCreateReq recordReq = new GaoRegistrationEventRecordCreateReq();
recordReq.setCustomerId(customer.getId());
recordReq.setCustomerIdList(List.of(customer.getId()));
recordReq.setRegistrationEventId(req.getRegistrationEventId());
recordReq.setRemark(req.getRemark());
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)
@Override
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.getId(), "not found record.id");
TimiException.required(entity.getCustomerId(), "not found record.customerId");
@@ -132,12 +233,26 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
entity.setRegisteredAt(TimiJava.defaultIfNull(entity.getRegisteredAt(), Time.now()));
super.update(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
public GaoRegistrationEventRecord get(String id) {
GaoRegistrationEventRecord record = super.get(id);
loadTransient(record);
loadTransient(List.of(record));
return record;
}
@@ -148,7 +263,7 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
TimiException.required(req.getPeriodType(), "not found req.periodType");
TimiException.required(req.getPeriodValue(), "not found req.periodValue");
List<GaoRegistrationEventRecord> list = mapper.selectByRegistrationEventAndPeriod(req.getRegistrationEventId(), req.getPeriodType(), req.getPeriodValue());
list.forEach(this::loadTransient);
loadTransient(list);
return list;
}
@@ -157,12 +272,19 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
TimiException.required(req, "not found req");
checkStatRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt());
List<GaoRegistrationEventRecord> list = mapper.selectByRange(req);
list.forEach(this::loadTransient);
loadTransient(list);
return list;
}
@Override
public PageResult<GaoRegistrationEventRecord> pageByRange(Page<GaoRegistrationEventRecordRangeReq> page) {
public List<GaoRegistrationEventRecordCalendarView> listCalendarByRange(GaoRegistrationEventRecordRangeReq req) {
TimiException.required(req, "not found req");
checkStatRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt());
return mapper.selectCalendarByRange(req);
}
@Override
public PageResult<GaoRegistrationEventRecordListItemView> pageByRange(Page<GaoRegistrationEventRecordRangeReq> page) {
TimiException.required(page, "not found page");
TimiException.required(page.getIndex(), "not found page.index");
TimiException.required(page.getSize(), "not found page.size");
@@ -171,9 +293,8 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
GaoRegistrationEventRecordRangeReq req = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoRegistrationEventRecordRangeReq());
checkStatRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt());
Long total = TimiJava.defaultIfNull(mapper.countByRange(req), 0L);
List<GaoRegistrationEventRecord> list = mapper.selectPageByRange(req, page.getIndex() * page.getSize(), page.getSize());
list.forEach(this::loadTransient);
PageResult<GaoRegistrationEventRecord> result = new PageResult<>();
List<GaoRegistrationEventRecordListItemView> list = mapper.selectPageByRange(req, page.getIndex() * page.getSize(), page.getSize());
PageResult<GaoRegistrationEventRecordListItemView> result = new PageResult<>();
result.setTotal(total);
result.setList(list);
return result;
@@ -236,7 +357,7 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
return Collections.emptyList();
}
List<GaoRegistrationEventRecord> list = mapper.selectLatestByCustomerIdListAndRegistrationEventId(customerIdList, registrationEventId);
list.forEach(this::loadTransient);
loadTransient(list);
return list;
}
@@ -261,6 +382,29 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
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
public List<GaoRegistrationEventRecordDailyStatView> statDailyByCustomerAndRegistrationEvent(GaoRegistrationEventRecordTrendStatReq req) {
TimiException.required(req, "not found req");
@@ -279,25 +423,122 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
public List<GaoRegistrationEventRecord> listLatestByCustomerId(String customerId) {
TimiException.required(customerId, "not found customerId");
List<GaoRegistrationEventRecord> list = mapper.selectLatestByCustomerId(customerId);
list.forEach(this::loadTransient);
loadTransient(list);
return list;
}
private String resolveCustomerId(GaoRegistrationEventRecordCreateReq req) {
if (TimiJava.isNotEmpty(req.getCustomerId())) {
customerService.get(req.getCustomerId());
return req.getCustomerId();
private String resolveCustomerId(String customerId) {
TimiException.required(customerId, "not found customerId");
customerService.get(customerId);
return customerId;
}
TimiException.required(req.getCustomerCode(), "not found req.customerCode");
GaoCustomer customer = customerService.getByCode(req.getCustomerCode());
return customer.getId();
private GaoRegistrationEventRecord doCreateRecord(GaoRegistrationEventRecordCreateReq req, String customerId) {
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) {
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));
}
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) {
@@ -340,16 +581,88 @@ public class GaoRegistrationEventRecordServiceImplement extends AbstractEntitySe
return list;
}
private void loadTransient(GaoRegistrationEventRecord record) {
if (record == null) {
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) {
if (recordList == null || recordList.isEmpty()) {
return;
}
record.setCustomer(customerService.get(record.getCustomerId()));
record.setRegistrationEvent(registrationEventService.get(record.getRegistrationEventId()));
if (TimiJava.isNotEmpty(record.getOperatorUserId())) {
record.setOperator(userService.get(record.getOperatorUserId()));
List<String> customerIdList = recordList.stream()
.map(GaoRegistrationEventRecord::getCustomerId)
.filter(TimiJava::isNotEmpty)
.distinct()
.toList();
Map<String, GaoCustomer> customerMap = customerMapper.selectByIdList(new ArrayList<>(customerIdList)).stream()
.collect(Collectors.toMap(GaoCustomer::getId, Function.identity(), (left, right) -> left));
List<String> registrationEventIdList = recordList.stream()
.map(GaoRegistrationEventRecord::getRegistrationEventId)
.filter(TimiJava::isNotEmpty)
.distinct()
.toList();
Map<String, GaoRegistrationEvent> registrationEventMap = registrationEventMapper.selectByIdList(new ArrayList<>(registrationEventIdList)).stream()
.collect(Collectors.toMap(GaoRegistrationEvent::getId, Function.identity(), (left, right) -> left));
List<String> operatorUserIdList = recordList.stream()
.map(GaoRegistrationEventRecord::getOperatorUserId)
.filter(TimiJava::isNotEmpty)
.distinct()
.toList();
Map<String, com.imyeyu.api.modules.user.entity.User> userMap = userMapper.listByIdList(new ArrayList<>(operatorUserIdList)).stream()
.collect(Collectors.toMap(com.imyeyu.api.modules.user.entity.User::getId, Function.identity(), (left, right) -> left));
List<String> recordIdList = recordList.stream()
.map(GaoRegistrationEventRecord::getId)
.filter(TimiJava::isNotEmpty)
.distinct()
.toList();
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(
Attachment.BizType.GAO_REGISTRATION_EVENT_RECORD,
recordIdList,
GaoRegistrationEventRecord.AttachType.PHOTO.name()
);
for (GaoRegistrationEventRecord record : recordList) {
record.setCustomer(customerMap.get(record.getCustomerId()));
record.setRegistrationEvent(registrationEventMap.get(record.getRegistrationEventId()));
record.setOperator(userMap.get(record.getOperatorUserId()));
record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of()));
}
record.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_REGISTRATION_EVENT_RECORD, record.getId(), GaoRegistrationEventRecord.AttachType.PHOTO.name()));
}
private void checkDailyStatRange(LocalDate beginDate, LocalDate endDate) {
@@ -1,33 +1,48 @@
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.modules.gao.entity.GaoRegistrationEventRoleRelation;
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.GaoRegistrationEventRoleRelationMapper;
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.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
///
/// 登记事件服务实现
///
/// @author Codex
/// @since 2026-07-27
/// @author 夜雨
/// @since 2026-07-27 09:00
@Slf4j
@Service
@RequiredArgsConstructor
public class GaoRegistrationEventServiceImplement extends AbstractEntityService<GaoRegistrationEvent, String> implements GaoRegistrationEventService {
private final ObjectMapper jackson;
private final LoggerService loggerService;
private final GaoRegistrationEventMapper mapper;
private final GaoRegistrationEventRoleRelationMapper roleRelationMapper;
@@ -39,33 +54,98 @@ public class GaoRegistrationEventServiceImplement extends AbstractEntityService<
@Override
public PageResult<GaoRegistrationEvent> pageWithRegistrationEventLimit(Page<GaoRegistrationEvent> page) {
PageResult<GaoRegistrationEvent> result = page(page);
if (result.getList() != null) {
result.getList().forEach(this::loadRegistrationEventLimit);
}
result.setList(result.getList().stream().filter(item -> item.getStatus() != GaoRegistrationEvent.Status.DELETED).toList());
loadRegistrationEventLimit(result.getList());
return result;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
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);
super.create(entity);
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)
@Override
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);
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());
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)
@Override
public void delete(String id) {
roleRelationMapper.deleteByRegistrationEventId(id);
super.delete(id);
Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_DELETE");
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
@@ -77,8 +157,9 @@ public class GaoRegistrationEventServiceImplement extends AbstractEntityService<
@Override
public List<GaoRegistrationEvent> listEnabled() {
List<GaoRegistrationEvent> list = mapper.selectEnabledList();
list.forEach(this::loadRegistrationEventLimit);
List<GaoRegistrationEvent> list = mapper.selectEnabledList(Time.now());
loadRegistrationEventLimit(list);
list = list.stream().filter(item -> !TimiJava.isEmpty(item.getRoleCodeList())).toList();
return list;
}
@@ -87,23 +168,66 @@ public class GaoRegistrationEventServiceImplement extends AbstractEntityService<
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) {
TimiException.required(entity, "not found registrationEvent");
if (requireId) {
TimiException.required(entity.getId(), "not found registrationEvent.id");
}
TimiException.required(entity.getName(), "not found registrationEvent.name");
if (entity.getEnabled() == null) {
entity.setEnabled(true);
if (entity.getStatus() == null) {
entity.setStatus(GaoRegistrationEvent.Status.ACTIVE);
}
if (entity.getRequirePhoto() == null) {
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) {
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) {
roleRelationMapper.deleteByRegistrationEventId(registrationEventId);
if (roleCodeList == null || roleCodeList.isEmpty()) {
@@ -121,7 +245,16 @@ public class GaoRegistrationEventServiceImplement extends AbstractEntityService<
}
}
private void loadRegistrationEventLimit(GaoRegistrationEvent registrationEvent) {
registrationEvent.setRoleCodeList(roleRelationMapper.selectRoleCodeListByRegistrationEventId(registrationEvent.getId()));
private void loadRegistrationEventLimit(List<GaoRegistrationEvent> registrationEventList) {
if (registrationEventList == null || registrationEventList.isEmpty()) {
return;
}
List<String> registrationEventIdList = registrationEventList.stream().map(GaoRegistrationEvent::getId).distinct().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) {
registrationEvent.setRoleCodeList(roleCodeMap.getOrDefault(registrationEvent.getId(), List.of()));
}
}
}
@@ -1,10 +1,13 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.entity.GaoUser;
import com.imyeyu.api.modules.gao.mapper.GaoUserMapper;
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.User;
import com.imyeyu.api.modules.user.mapper.UserMapper;
@@ -14,11 +17,14 @@ import com.imyeyu.api.modules.user.vo.role.UserRoleAuthorizeReq;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -33,11 +39,14 @@ import java.util.stream.Collectors;
///
/// @author Codex
/// @since 2026-07-30
@Slf4j
@Service
@RequiredArgsConstructor
public class GaoUserServiceImplement extends AbstractEntityService<GaoUser, String> implements GaoUserService {
private final GaoUserMapper mapper;
private final ObjectMapper jackson;
private final LoggerService loggerService;
private final UserMapper userMapper;
private final UserService userService;
private final RoleService roleService;
@@ -50,20 +59,54 @@ public class GaoUserServiceImplement extends AbstractEntityService<GaoUser, Stri
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(GaoUser entity) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_USER_CREATE");
try {
logger.setContent(jackson.writeValueAsString(entity));
prepareUser(entity);
checkEntity(entity, false);
super.create(entity);
saveUserRole(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)
@Override
public void update(GaoUser entity) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_USER_UPDATE");
try {
logger.setContent(jackson.writeValueAsString(entity));
checkEntity(entity, true);
updateUser(entity);
super.update(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
@@ -98,7 +141,7 @@ public class GaoUserServiceImplement extends AbstractEntityService<GaoUser, Stri
TimiException.required(userMapper.select(entity.getUserId()), "not found user");
GaoUser dbGaoUser = mapper.selectByUserId(entity.getUserId());
if (dbGaoUser != null && !dbGaoUser.getId().equals(entity.getId())) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("GAO 用户已存在");
throw new TimiException(TimiCode.ARG_BAD, "GAO 用户已存在");
}
if (entity.getEnabled() == null) {
entity.setEnabled(true);
@@ -112,6 +155,7 @@ public class GaoUserServiceImplement extends AbstractEntityService<GaoUser, Stri
}
User user = entity.getUser();
TimiException.required(user, "not found gao user.user");
user.setPhoneNoVerifyAt(TimiJava.defaultIfNull(user.getPhoneNoVerifyAt(), Time.now()));
userService.create(user);
entity.setUserId(user.getId());
}
@@ -0,0 +1,18 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 客户性别统计视图
///
/// @author Codex
/// @since 2026-08-01
@Data
public class GaoCustomerGenderStatView {
/// 性别编码,未知时为 UNKNOWN
private String genderCode;
/// 客户数量
private Long count;
}
@@ -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;
}
@@ -0,0 +1,30 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 登记事件记录日历轻量视图
///
/// @author Codex
/// @since 2026-08-01
@Data
public class GaoRegistrationEventRecordCalendarView {
/// 记录 ID
private String id;
/// 客户 ID
private String customerId;
/// 客户编码
private String customerCode;
/// 客户姓名
private String customerName;
/// 登记事件 ID
private String registrationEventId;
/// 登记时间
private Long registeredAt;
}
@@ -13,11 +13,8 @@ import java.util.List;
@Data
public class GaoRegistrationEventRecordCreateReq {
/// 客户 ID
private String customerId;
/// 客户编码,扫码登记可直接传这个
private String customerCode;
/// 客户 ID 列表,批量登记时使用
private List<String> customerIdList = new ArrayList<>();
/// 登记事件 ID
private String registrationEventId;
@@ -0,0 +1,33 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 登记事件记录列表项视图
///
/// @author Codex
/// @since 2026-08-01
@Data
public class GaoRegistrationEventRecordListItemView {
/// 记录 ID
private String id;
/// 客户 ID
private String customerId;
/// 客户姓名
private String customerName;
/// 客户头像附件 ID
private String customerPhotoAttachmentId;
/// 登记事件 ID
private String registrationEventId;
/// 登记事件名称
private String registrationEventName;
/// 登记时间
private Long registeredAt;
}
@@ -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<>();
}
@@ -45,7 +45,7 @@ public class JournalAPIInterceptor implements HandlerInterceptor {
requiredUploadPermission = handlerMethod.getMethodAnnotation(RequiredUploadPermission.class) != null;
}
if (!canAccess()) {
throw new TimiException(TimiCode.PERMISSION_MISS).msgKey("invalid.key");
throw new TimiException(TimiCode.PERMISSION_MISS, "invalid.key");
}
return !requiredUploadPermission || canUploadKey();
}
@@ -37,7 +37,7 @@ public class LyricController {
// @PostMapping("/correct")
// public long correctRequest(CaptchaData<LyricCorrectRequest> request) {
// if (IOSize.MB * 120 < request.getData().getFile().getSize()) {
// throw new TimiException(TimiCode.ARG_BAD).msgKey("lyric.correct_request.too_big");
// throw new TimiException(TimiCode.ARG_BAD, "lyric.correct_request.too_big");
// }
// return correctService.correctRequest(request.getData());
// }
@@ -135,7 +135,7 @@ public class LyricServiceImplement extends AbstractEntityService<Lyric, String>
return lyric;
} catch (Exception e) {
log.error("fetch lyric fail", e);
throw new TimiException(TimiCode.ERROR).msgKey("无法获取歌词");
throw new TimiException(TimiCode.ERROR, "无法获取歌词");
}
}
}
@@ -94,7 +94,7 @@ abstract class AbstractMirror {
onException(mirror, e);
if (e instanceof TimiException te) {
log.warn("[%s] Fail: %s".formatted(mirror.getBean(), te.getMsg()));
log.warn("[%s] Fail: %s".formatted(mirror.getBean(), te.getMessage()));
} else {
log.error("[%s] Error".formatted(mirror.getBean()), e);
}
@@ -132,7 +132,7 @@ public class TerminalPipe implements TimiJava {
*/
public void exec(CallbackArg<String> callback, String command) {
if (status == Status.DIED) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("该会话已终止");
throw new TimiException(TimiCode.RESULT_BAD, "该会话已终止");
}
this.status = Status.RUNNING;
this.callback = callback;
@@ -87,7 +87,7 @@ public class FileController implements TimiJava, OS.FileSystem {
public ServerFile object(HttpServletRequest req, HttpServletResponse resp) {
String path = req.getServletPath().substring("/system/file/object".length());
if (TimiJava.isEmpty(path)) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("TODO 缺少参数:object");
throw new TimiException(TimiCode.ARG_MISS, "TODO 缺少参数:object");
}
ServerFile file = new ServerFile(new File(path));
service.checkAccessPermission(file.getAbsolutePath());
@@ -105,10 +105,10 @@ public class FileController implements TimiJava, OS.FileSystem {
@PostMapping("/search")
public List<ServerFile> search(@RequestBody Map<String, String> params) {
if (TimiJava.isEmpty(params.get("path"))) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:path");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:path");
}
if (TimiJava.isEmpty(params.get("keyword"))) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:keyword");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:keyword");
}
return service.doFilter(service.search(params.get("path"), params.get("keyword")));
}
@@ -126,7 +126,7 @@ public class FileController implements TimiJava, OS.FileSystem {
public boolean exist(HttpServletRequest req, HttpServletResponse resp) {
String path = req.getServletPath().substring("/system/file/exist".length());
if (TimiJava.isEmpty(path)) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:path");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:path");
}
return service.canAccess(path) && service.isExist(path);
}
@@ -237,10 +237,10 @@ public class FileController implements TimiJava, OS.FileSystem {
@PostMapping("/mkdir")
public String mkdir(@RequestBody Map<String, String> params) {
if (TimiJava.isEmpty(params.get("path"))) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:path");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:path");
}
if (TimiJava.isEmpty(params.get("name"))) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:name");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:name");
}
return service.mkdir(params.get("path"), params.get("name")).getAbsolutePath();
}
@@ -256,10 +256,10 @@ public class FileController implements TimiJava, OS.FileSystem {
@PostMapping("/rename")
public boolean rename(@RequestBody Map<String, String> params) {
if (TimiJava.isEmpty(params.get("from"))) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:from");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:from");
}
if (TimiJava.isEmpty(params.get("to"))) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:to");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:to");
}
return service.rename(params.get("from"), params.get("to"));
}
@@ -275,10 +275,10 @@ public class FileController implements TimiJava, OS.FileSystem {
@PostMapping("/zip")
public String zip(@RequestBody ListFileToRequest listFileToRequest) {
if (TimiJava.isEmpty(listFileToRequest.getList())) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:list[]");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:list[]");
}
if (TimiJava.isEmpty(listFileToRequest.getTo())) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:to");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:to");
}
// 异步任务
FileZipAsyncTask task = new FileZipAsyncTask(listFileToRequest.getList(), listFileToRequest.getTo());
@@ -298,14 +298,14 @@ public class FileController implements TimiJava, OS.FileSystem {
public String unzip(@RequestBody Map<String, String> params) {
String zipFilePath = params.get("zipFile");
if (TimiJava.isEmpty(zipFilePath)) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:zipFile");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:zipFile");
}
File zipFile = new File(zipFilePath);
if (!zipFile.exists()) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("文件不存在:" + zipFilePath);
throw new TimiException(TimiCode.ARG_BAD, "文件不存在:" + zipFilePath);
}
if (TimiJava.isEmpty(params.get("to"))) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:to");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:to");
}
// 异步任务
FileUnZipAsyncTask task = new FileUnZipAsyncTask(zipFile, params.get("to"));
@@ -341,14 +341,14 @@ public class FileController implements TimiJava, OS.FileSystem {
// TODO 具名入参
String tarFilePath = params.get("tarFile");
if (TimiJava.isEmpty(tarFilePath)) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:tarFile");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:tarFile");
}
File tarFile = new File(tarFilePath);
if (!tarFile.exists()) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("文件不存在:" + tarFilePath);
throw new TimiException(TimiCode.ARG_BAD, "文件不存在:" + tarFilePath);
}
if (TimiJava.isEmpty(params.get("to"))) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:to");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:to");
}
FileUnTarAsyncTask task = new FileUnTarAsyncTask(tarFile, params.get("to"));
asyncTaskService.addAsyncTask(task);
@@ -17,6 +17,21 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
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 String module;
private Module module;
/** 操作描述 */
private String operation;
@@ -69,7 +84,26 @@ public class Logger implements IDEntity<String>, Creatable {
/** 创建时间 */
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.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 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) {
create(Logger.Level.INFO, module, operation, userId, null, null);
default void info(Logger.Module module, String operation) {
create(module, Logger.Level.INFO, operation, null, null);
}
default void info(String module, String operation, String content, String result) {
create(Logger.Level.INFO, module, operation, null, content, result);
default void info(Logger.Module module, String operation, String content, String result) {
create(module, Logger.Level.INFO, operation, content, result);
}
default void info(String module, String operation, String userId, String content, String result) {
create(Logger.Level.INFO, module, operation, userId, content, result);
default void warning(Logger.Module module, String operation) {
create(module, Logger.Level.WARN, operation, null, null);
}
default void warning(String module, String operation, String userId) {
create(Logger.Level.WARN, module, operation, userId, null, null);
default void warning(Logger.Module module, String operation, String content, String result) {
create(module, Logger.Level.WARN, operation, content, result);
}
default void warning(String module, String operation, String content, String result) {
create(Logger.Level.WARN, module, operation, null, content, result);
default void error(Logger.Module module, String operation) {
create(module, Logger.Level.ERROR, operation, null, null);
}
default void warning(String module, String operation, String userId, String content, String result) {
create(Logger.Level.WARN, module, operation, userId, 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);
default void error(Logger.Module module, String operation, String content, String result) {
create(module, Logger.Level.ERROR, operation, content, result);
}
}
@@ -116,7 +116,7 @@ public abstract class AbstractAsyncTask extends AsyncTask {
/** 开始 */
void start() {
if (status != AsyncTask.Status.IDLE && status != AsyncTask.Status.PAUSE) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("can not start this task");
throw new TimiException(TimiCode.RESULT_BAD, "can not start this task");
}
synchronized (pauseLocker) {
pauseLocker.notifyAll();
@@ -127,7 +127,7 @@ public abstract class AbstractAsyncTask extends AsyncTask {
/** 暂停 */
void pause() {
if (!canPause || (status != AsyncTask.Status.WAITING && status != AsyncTask.Status.RUNNING)) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("can not pause this task");
throw new TimiException(TimiCode.RESULT_BAD, "can not pause this task");
}
status = Status.PAUSE;
onPause();
@@ -139,7 +139,7 @@ public abstract class AbstractAsyncTask extends AsyncTask {
return;
}
if (!canInterrupt || (status != AsyncTask.Status.WAITING && status != AsyncTask.Status.RUNNING && status != AsyncTask.Status.PAUSE)) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("can not interrupt this task");
throw new TimiException(TimiCode.RESULT_BAD, "can not interrupt this task");
}
if (status == AsyncTask.Status.PAUSE) {
status = AsyncTask.Status.INTERRUPT;
@@ -109,7 +109,7 @@ public class AsyncTaskServiceImplement implements AsyncTaskService {
task.setMessage(task.logBuffer.toString());
return task;
}
throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not found task");
throw new TimiException(TimiCode.RESULT_NULL, "TODO not found task");
}
@Override
@@ -114,7 +114,7 @@ public class FileServiceImplement implements TimiJava, FileService {
if (canAccess(path)) {
return;
}
throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("TODO invalid permission");
throw new TimiException(TimiCode.PERMISSION_ERROR, "TODO invalid permission");
}
@Override
@@ -139,7 +139,7 @@ public class FileServiceImplement implements TimiJava, FileService {
}
return result;
} else {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("文件或路径不存在");
throw new TimiException(TimiCode.RESULT_NULL, "文件或路径不存在");
}
}
@@ -176,7 +176,7 @@ public class FileServiceImplement implements TimiJava, FileService {
}
return result;
} else {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("文件或路径不存在");
throw new TimiException(TimiCode.RESULT_NULL, "文件或路径不存在");
}
}
@@ -186,7 +186,7 @@ public class FileServiceImplement implements TimiJava, FileService {
try {
return IO.dir(uri);
} catch (NoPermissionException e) {
throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("权限错误");
throw new TimiException(TimiCode.PERMISSION_ERROR, "权限错误");
}
}
@@ -205,7 +205,7 @@ public class FileServiceImplement implements TimiJava, FileService {
try {
return IO.getInputStream(getPath(path));
} catch (FileNotFoundException e) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("找不到文件");
throw new TimiException(TimiCode.RESULT_NULL, "找不到文件");
}
}
@@ -213,25 +213,25 @@ public class FileServiceImplement implements TimiJava, FileService {
public void upload(TransferFile file) {
String path = getPath(Decoder.url(file.getPath()));
if (file.getName() == null) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少参数:name");
throw new TimiException(TimiCode.ARG_MISS, "缺少参数:name");
}
String fileName = Decoder.url(file.getName());
if (!OS.isValidFileName(path)) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("路径名称不合法:" + path);
throw new TimiException(TimiCode.ARG_BAD, "路径名称不合法:" + path);
}
if (!OS.isValidFileName(fileName)) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("文件名称不合法:" + fileName);
throw new TimiException(TimiCode.ARG_BAD, "文件名称不合法:" + fileName);
}
if (file.getLength() == file.getFile().getSize()) {
try {
toFile(new File(path + fileName), file.getFile().getInputStream());
} catch (IOException e) {
log.error("get request input stream error", e);
throw new TimiException(TimiCode.ARG_BAD).msgKey("获取文件输入流失败");
throw new TimiException(TimiCode.ARG_BAD, "获取文件输入流失败");
}
} else {
log.error("request submit request size: {}, receive request size: {}", file.getLength(), file.getFile().getSize());
throw new TimiException(TimiCode.ERROR).msgKey("不完整上传文件,已忽略");
throw new TimiException(TimiCode.ERROR, "不完整上传文件,已忽略");
}
}
@@ -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.mapper.LoggerMapper;
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.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
@@ -18,6 +22,8 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor
public class LoggerServiceImplement extends AbstractEntityService<Logger, String> implements LoggerService {
private final UserLoginService userLoginService;
private final LoggerMapper mapper;
@Override
@@ -28,19 +34,23 @@ public class LoggerServiceImplement extends AbstractEntityService<Logger, String
@Override
public void create(Logger logger) {
// 默认异步
Thread.startVirtualThread(() -> super.create(logger));
Thread.startVirtualThread(() -> createSync(logger));
}
@Override
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);
}
public void create(Logger.Level level, String module, String operation, String userId, String content, String result) {
Logger log = new Logger(level);
log.setModule(module);
log.setOperation(operation);
log.setUserId(userId);
public void create(Logger.Module module, Logger.Level level, String operation, String content, String result) {
Logger log = new Logger(module, level, operation);
log.setContent(content);
log.setResult(result);
create(log);
@@ -63,7 +63,7 @@ public class FileMoveAsyncTask extends AbstractAsyncTask {
File toFile = new File(IO.fitPath(to) + item.getValue());
IO.dir(toFile.getParent());
if (!item.getKey().renameTo(toFile)) {
throw new TimiException(TimiCode.ERROR).msgKey("移动失败:" + item.getKey().getAbsolutePath() + " -> " + toFile.getAbsolutePath());
throw new TimiException(TimiCode.ERROR, "移动失败:" + item.getKey().getAbsolutePath() + " -> " + toFile.getAbsolutePath());
}
}
@@ -67,10 +67,10 @@ public class DockerEngineClient {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("docker engine request interrupted: {}", requestPath, e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO docker engine request interrupted");
throw new TimiException(TimiCode.ERROR, "TODO docker engine request interrupted");
} catch (IOException e) {
log.error("docker engine request error: {}", requestPath, e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO docker engine request error");
throw new TimiException(TimiCode.ERROR, "TODO docker engine request error");
}
}
@@ -53,7 +53,7 @@ public class SystemAPIInterceptor implements HandlerInterceptor {
// TimiSpring.setRequestAttr(HIGH_AUTHORITY, keyMap.get(key));
// return true;
// }
// throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("TODO invalid key");
// throw new TimiException(TimiCode.PERMISSION_ERROR, "TODO invalid key");
return true;
}
@@ -43,14 +43,14 @@ public class UpsStatusClient {
public JsonNode getStatusJson() {
if (statusUrl == null || statusUrl.isBlank()) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少配置:ups.status-url");
throw new TimiException(TimiCode.ARG_MISS, "缺少配置:ups.status-url");
}
try {
String response = CommonRequest.post(statusUrl).bodyEntity(ArgMap.of("portName", "USBusbdev3").toEntity()).asString();
return jackson.readTree(response);
} catch (IOException e) {
log.error("request ups status error: {}", statusUrl, e);
throw new TimiException(TimiCode.ERROR).msgKey("UPS 状态请求失败");
throw new TimiException(TimiCode.ERROR, "UPS 状态请求失败");
}
}
@@ -4,6 +4,7 @@ import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.annotation.RequireCoreRole;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.bean.CoreRoleCode;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.user.entity.Permission;
import com.imyeyu.api.modules.user.service.PermissionService;
import com.imyeyu.api.modules.user.vo.permission.BatchCreateReq;
@@ -32,7 +33,7 @@ public class PermissionController {
private final PermissionService service;
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY)
@RequireCorePermission(CorePermissionCode.PERMISSION_READ)
@PostMapping("/list")
public PageResult<Permission> list(@RequestBody Page<Permission> page) {
@@ -5,6 +5,7 @@ import com.imyeyu.api.annotation.RequireCoreRole;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.bean.CoreRoleCode;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.user.entity.Permission;
import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.api.modules.user.service.PermissionService;
@@ -38,7 +39,7 @@ public class RoleController {
private final RoleService service;
private final PermissionService permissionService;
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY)
@RequireCorePermission(CorePermissionCode.ROLE_READ)
@PostMapping("/list")
public PageResult<Role> list(@RequestBody Page<Role> page) {
@@ -49,7 +50,7 @@ public class RoleController {
return result;
}
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY)
@RequireCorePermission(CorePermissionCode.ROLE_READ)
@PostMapping("/detail")
public Role detail(@RequestParam String id) {
@@ -82,7 +83,7 @@ public class RoleController {
service.delete(id);
}
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY)
@RequireCorePermission(CorePermissionCode.USER_ROLE_READ)
@PostMapping("/authorized/list")
public List<Role> listUserRole(@RequestParam String userId, @RequestParam(required = false) ModuleCode moduleCode) {
@@ -93,14 +94,14 @@ public class RoleController {
}
@AOPLog
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY)
@RequireCorePermission(value = {CorePermissionCode.USER_ROLE_CREATE, CorePermissionCode.USER_ROLE_DELETE})
@PostMapping("/authorized/create")
public void authorizeUserRole(@RequestBody UserRoleAuthorizeReq req) {
service.authorizeUserRole(req);
}
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY)
@RequireCorePermission(CorePermissionCode.USER_ROLE_READ)
@PostMapping("/authorized/permission")
public List<Permission> listAuthorizedPermission(@RequestParam String userId) {
@@ -53,7 +53,7 @@ public class PermissionCheckerImplement implements PermissionChecker {
public void checkAll(ModuleCode moduleCode, String... permissionCodeList) {
if (!hasAll(moduleCode, permissionCodeList)) {
String codeList = buildPermissionKeys(moduleCode, permissionCodeList).toString();
throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("权限不足:%s".formatted(codeList));
throw new TimiException(TimiCode.PERMISSION_ERROR, "权限不足:%s".formatted(codeList));
}
}
@@ -61,7 +61,7 @@ public class PermissionCheckerImplement implements PermissionChecker {
public void checkAny(ModuleCode moduleCode, String... permissionCodeList) {
if (!hasAny(moduleCode, permissionCodeList)) {
String codeList = buildPermissionKeys(moduleCode, permissionCodeList).toString();
throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("权限不足:%s".formatted(codeList));
throw new TimiException(TimiCode.PERMISSION_ERROR, "权限不足:%s".formatted(codeList));
}
}
@@ -8,8 +8,8 @@ import com.imyeyu.api.modules.user.mapper.PermissionMapper;
import com.imyeyu.api.modules.user.mapper.RolePermissionMapper;
import com.imyeyu.api.modules.user.service.PermissionService;
import com.imyeyu.api.modules.user.vo.permission.BatchCreateReq;
import com.imyeyu.api.util.RedisMultilingual;
import com.imyeyu.java.TimiJava;
import com.imyeyu.lang.message.MessageResolver;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
@@ -35,7 +35,7 @@ public class PermissionServiceImplement extends AbstractEntityService<Permission
private final MultilingualMapper multilingualMapper;
private final MultilingualService multilingualService;
private final RedisMultilingual multilingual;
private final MessageResolver messageResolver;
@Override
protected BaseMapper<Permission, String> mapper() {
@@ -61,7 +61,7 @@ public class PermissionServiceImplement extends AbstractEntityService<Permission
permission.setId(UUID.randomUUID().toString());
permission.setModuleCode(req.getModuleCode());
permission.setCode(req.getPrefixCode() + "_" + type.name());
permission.setName(req.getPrefixName() + multilingual.map(TimiSpring.getLanguage()).text(type.name().toLowerCase()));
permission.setName(req.getPrefixName() + messageResolver.resolve(type.name().toLowerCase(), null, TimiSpring.getLanguage()));
permission.setCreatedAt(Time.now());
permissionList.add(permission);
}
@@ -47,14 +47,14 @@ public class RoleCheckerImplement implements RoleChecker {
@Override
public void checkAll(ModuleCode moduleCode, String... roleCodeList) {
if (!hasAll(moduleCode, roleCodeList)) {
throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("require role: %s".formatted(buildRoleKeys(moduleCode, roleCodeList)));
throw new TimiException(TimiCode.PERMISSION_ERROR, "require role: %s".formatted(buildRoleKeys(moduleCode, roleCodeList)));
}
}
@Override
public void checkAny(ModuleCode moduleCode, String... roleCodeList) {
if (!hasAny(moduleCode, roleCodeList)) {
throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("require role: %s".formatted(buildRoleKeys(moduleCode, roleCodeList)));
throw new TimiException(TimiCode.PERMISSION_ERROR, "require role: %s".formatted(buildRoleKeys(moduleCode, roleCodeList)));
}
}
@@ -159,7 +159,7 @@ public class RoleServiceImplement extends AbstractEntityService<Role, String> im
TimiException.required(req, "not found req");
TimiException.required(req.getUserId(), "not found req.userId");
if (req.getModuleCode() == null) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("moduleCode");
throw new TimiException(TimiCode.ARG_MISS, "moduleCode");
}
List<UserRole> bindingList = userRoleService.listByUserId(req.getUserId());
List<String> bindingRoleIdList = bindingList.stream().map(UserRole::getRoleId).toList();
@@ -25,8 +25,8 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/// 用户登录服务实现
///
@@ -95,7 +95,9 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava {
@Override
public User getLoginUser() {
String token = TimiSpring.getToken();
TimiException.required(token, "not found token");
if (TimiJava.isEmpty(token)) {
return null;
}
String userId = redisUserToken.get(getUserTokenKey(token));
if (TimiJava.isEmpty(userId)) {
return null;
@@ -65,6 +65,7 @@ public class UserServiceImplement extends AbstractEntityService<User, String> im
dbUser.setPassword(Digest.argon2id(user.getPassword()));
dbUser.setEmail(user.getEmail());
dbUser.setPhoneNo(user.getPhoneNo());
dbUser.setPhoneNoVerifyAt(user.getPhoneNoVerifyAt());
dbUser.setExp(TimiJava.defaultIfNull(user.getExp(), 0));
super.create(dbUser);
user.setId(dbUser.getId());
@@ -200,10 +200,10 @@ public class CaptchaManager {
String cacheKey = CACHE_KEY.formatted(uniqueId);
String cacheValue = redisCaptcha.get(cacheKey);
if (TimiJava.isEmpty(cacheValue)) {
throw new TimiException(TimiCode.ARG_EXPIRED).msgKey("captcha.expire");
throw new TimiException(TimiCode.ARG_EXPIRED, "captcha.expire");
}
if (!value.trim().equalsIgnoreCase(cacheValue)) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("captcha.error");
throw new TimiException(TimiCode.RESULT_BAD, "captcha.error");
}
// 清除缓存
redisCaptcha.destroy(cacheKey);
@@ -1,9 +1,6 @@
package com.imyeyu.api.util;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.lang.mapper.AbstractLanguageMapper;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.util.GlobalReturnHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.ArchiveCommand;
@@ -42,20 +39,11 @@ public class InitApplication implements ApplicationRunner {
private String devLang;
private final SettingService settingService;
private final RedisMultilingual redisMultilingual;
private final GlobalReturnHandler globalReturnHandler;
private void initGitCommand() {
ArchiveCommand.registerFormat("tar.gz", new TarFormat());
}
private void initMultilingual() {
globalReturnHandler.setMultilingualHeader(mapping -> {
AbstractLanguageMapper map = redisMultilingual.map(TimiSpring.getLanguage());
return map.textArgs(mapping.getMsgKey(), mapping.getMsgArgs());
});
}
@Override
public void run(ApplicationArguments args) throws Exception {
Method[] methods = getClass().getDeclaredMethods();
@@ -1,10 +1,11 @@
package com.imyeyu.api.util;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
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.gao.bean.GaoPermissionCode;
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.user.bean.BootstrapData;
import com.imyeyu.api.modules.user.bean.BuiltinAuthRegistry;
@@ -92,7 +93,13 @@ public class InitBuiltinUser implements ApplicationRunner {
userService.create(user);
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);
}
// 授权系统用户角色
{
@@ -1,43 +0,0 @@
package com.imyeyu.api.util;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.service.MultilingualService;
import com.imyeyu.java.bean.Language;
import com.imyeyu.lang.mapper.AbstractLanguageMapper;
import com.imyeyu.utils.StringInterpolator;
import org.jcodec.api.NotSupportedException;
import java.util.Map;
/**
* @author 夜雨
* @version 2024-04-03 11:01
*/
public class RedisLanguage extends AbstractLanguageMapper {
private static final StringInterpolator INTERPOLATOR = new StringInterpolator(StringInterpolator.SIMPLE_OBJ);
public RedisLanguage(Language.Enum language) {
super(language);
}
@Override
public void add(String id, String value) {
throw new NotSupportedException("not supported to add value, MultilingualService will auto cache when not found value");
}
@Override
public boolean has(String id) {
return text(id).equals(id);
}
@Override
public String text(String id) {
return TimiServerAPI.applicationContext.getBean(MultilingualService.class).get(id).getValue(language);
}
@Override
public String textArgs(String id, Map<String, Object> args) {
return INTERPOLATOR.inject(text(id), args);
}
}
@@ -0,0 +1,44 @@
package com.imyeyu.api.util;
import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.api.modules.common.service.MultilingualService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.Language;
import com.imyeyu.lang.message.MessageResolver;
import com.imyeyu.utils.StringInterpolator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* Redis 消息解析器
*
* @author 夜雨
* @since 2026-08-01 00:00
*/
@Component
@RequiredArgsConstructor
public class RedisMessageResolver implements MessageResolver {
private static final StringInterpolator INTERPOLATOR = new StringInterpolator(StringInterpolator.SIMPLE_OBJ);
private final MultilingualService multilingualService;
@Override
public String resolve(String messageCode, Map<String, Object> messageArgs, Language.Enum language) {
if (TimiJava.isEmpty(messageCode)) {
return "";
}
Language.Enum actualLanguage = language == null ? Language.Enum.zh_CN : language;
Multilingual multilingual = multilingualService.get(messageCode);
if (multilingual == null) {
return INTERPOLATOR.inject(messageCode, messageArgs);
}
String template = multilingual.getValue(actualLanguage);
if (TimiJava.isEmpty(template)) {
return INTERPOLATOR.inject(messageCode, messageArgs);
}
return INTERPOLATOR.inject(template, messageArgs);
}
}
@@ -1,24 +0,0 @@
package com.imyeyu.api.util;
import com.imyeyu.java.bean.Language;
import com.imyeyu.lang.multi.Multilingual;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* @author 夜雨
* @version 2024-04-03 11:15
*/
@Component
@RequiredArgsConstructor
public class RedisMultilingual extends Multilingual {
@PostConstruct
private void postConstruct() {
Language.Enum[] languages = Language.Enum.values();
for (Language.Enum language : languages) {
add(language, new RedisLanguage(language));
}
}
}
@@ -0,0 +1,20 @@
ALTER TABLE `gao_customer`
DROP INDEX `uk_gao_customer_code`,
ADD COLUMN `active_code` VARCHAR(64) GENERATED ALWAYS AS (
CASE WHEN `deleted_at` IS NULL THEN `code` ELSE NULL END
) STORED COMMENT '未删除客户编码' INVISIBLE,
ADD UNIQUE KEY `uk_gao_customer_active_code` (`active_code`);
ALTER TABLE `permission`
DROP INDEX `uk_module_code_code`,
ADD COLUMN `active_code` VARCHAR(50) GENERATED ALWAYS AS (
CASE WHEN `deleted_at` IS NULL THEN `code` ELSE NULL END
) STORED COMMENT '未删除权限代码' INVISIBLE,
ADD UNIQUE KEY `uk_permission_module_active_code` (`module_code`, `active_code`);
ALTER TABLE `role`
DROP INDEX `uk_module_code_code`,
ADD COLUMN `active_code` VARCHAR(50) GENERATED ALWAYS AS (
CASE WHEN `deleted_at` IS NULL THEN `code` ELSE NULL END
) STORED COMMENT '未删除角色代码' INVISIBLE,
ADD UNIQUE KEY `uk_role_module_active_code` (`module_code`, `active_code`);
@@ -0,0 +1,5 @@
ALTER TABLE `gao_customer`
ADD COLUMN `active_name` VARCHAR(64) GENERATED ALWAYS AS (
CASE WHEN `deleted_at` IS NULL THEN NULLIF(TRIM(`name`), '') ELSE NULL END
) STORED COMMENT '未删除客户姓名' INVISIBLE,
ADD UNIQUE KEY `uk_gao_customer_active_name` (`active_name`);
@@ -0,0 +1,3 @@
ALTER TABLE `gao_customer`
ADD COLUMN `creator_user_id` VARCHAR(36) NULL COMMENT '录入用户 ID' AFTER `introducer_customer_id`,
ADD KEY `idx_gao_customer_creator_user_id` (`creator_user_id`);
@@ -0,0 +1,12 @@
ALTER TABLE `gao_customer`
ADD COLUMN `invited_count` BIGINT NOT NULL DEFAULT 0 COMMENT '邀请客户数' AFTER `introducer_customer_id`;
UPDATE `gao_customer` AS `customer`
SET `invited_count` = (
SELECT COUNT(1)
FROM `gao_customer` AS `invitee`
WHERE
`invitee`.`introducer_customer_id` = `customer`.`id`
AND `invitee`.`deleted_at` IS NULL
)
WHERE `customer`.`deleted_at` IS NULL;
@@ -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`;
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.common.mapper.AttachmentMapper">
<select id="selectByBizIdList" resultType="com.imyeyu.api.modules.common.entity.Attachment">
SELECT
*
FROM `attachment`
WHERE
`biz_type` = #{bizType}
<if test="bizIdList != null and !bizIdList.isEmpty()">
AND `biz_id` IN
<foreach collection="bizIdList" item="bizId" separator="," open="(" close=")">
#{bizId}
</foreach>
</if>
<if test="bizIdList == null or bizIdList.isEmpty()">
AND 1 = 0
</if>
AND `deleted_at` IS NULL
AND (`destroy_at` IS NULL OR UNIX_TIMESTAMP() &lt; `destroy_at`)
<if test="attachTypes != null and !attachTypes.isEmpty()">
AND `attach_type` IN
<foreach collection="attachTypes" item="attachType" separator="," open="(" close=")">
#{attachType}
</foreach>
</if>
ORDER BY `created_at` ASC
</select>
</mapper>
@@ -28,6 +28,18 @@
ORDER BY `created_at` DESC
</select>
<select id="selectByIdList" resultType="com.imyeyu.api.modules.gao.entity.GaoCustomer">
SELECT
*
FROM `gao_customer`
WHERE
`id` IN
<foreach collection="idList" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND `deleted_at` IS NULL
</select>
<select id="countSearch" resultType="java.lang.Long">
SELECT
COUNT(1)
@@ -65,6 +77,16 @@
AND `deleted_at` IS NULL
</select>
<update id="updateInvitedCountDelta">
UPDATE `gao_customer`
SET
`invited_count` = GREATEST(IFNULL(`invited_count`, 0) + #{delta}, 0),
`updated_at` = UNIX_TIMESTAMP() * 1000
WHERE
`id` = #{customerId}
AND `deleted_at` IS NULL
</update>
<select id="statDailyNewCustomer" resultType="com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStatView">
SELECT
CAST(DATE_FORMAT(FROM_UNIXTIME(`created_at` / 1000), '%Y%m%d') AS UNSIGNED) AS `dateValue`,
@@ -86,4 +108,14 @@
`created_at` &lt; #{beginCreatedAt}
AND `deleted_at` IS NULL
</select>
<select id="statGender" resultType="com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView">
SELECT
IFNULL(CAST(`gender` AS CHAR), 'UNKNOWN') AS `genderCode`,
COUNT(1) AS `count`
FROM `gao_customer`
WHERE
`deleted_at` IS NULL
GROUP BY IFNULL(CAST(`gender` AS CHAR), 'UNKNOWN')
</select>
</mapper>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventMapper">
<select id="selectByIdList" resultType="com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent">
SELECT
*
FROM `gao_registration_event`
WHERE
1 = 1
<if test="idList != null and !idList.isEmpty()">
AND `id` IN
<foreach collection="idList" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</if>
<if test="idList == null or idList.isEmpty()">
AND 1 = 0
</if>
AND `deleted_at` IS NULL
</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>
@@ -20,6 +20,37 @@
ORDER BY `registered_at` DESC
</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
`registration_event_id` AS `registrationEventId`,
@@ -112,6 +143,54 @@
ORDER BY `latestRegisteredAt` DESC
</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
CAST(DATE_FORMAT(FROM_UNIXTIME(`registered_at` / 1000), '%Y%m%d') AS UNSIGNED) AS `dateValue`,
@@ -259,6 +338,20 @@
ORDER BY r.`registered_at` DESC, r.`created_at` DESC
</select>
<select id="selectCalendarByRange" resultType="com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView">
SELECT
r.`id` AS `id`,
r.`customer_id` AS `customerId`,
c.`code` AS `customerCode`,
c.`name` AS `customerName`,
r.`registration_event_id` AS `registrationEventId`,
r.`registered_at` AS `registeredAt`
<include refid="rangeFrom"/>
WHERE
<include refid="rangeWhere"/>
ORDER BY r.`registered_at` DESC, r.`created_at` DESC
</select>
<select id="countByRange" resultType="java.lang.Long">
SELECT
COUNT(1)
@@ -267,9 +360,26 @@
<include refid="rangeWhere"/>
</select>
<select id="selectPageByRange" resultType="com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord">
<select id="selectPageByRange" resultType="com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordListItemView">
SELECT
r.*
r.`id` AS `id`,
r.`customer_id` AS `customerId`,
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`,
r.`registration_event_id` AS `registrationEventId`,
e.`name` AS `registrationEventName`,
r.`registered_at` AS `registeredAt`
<include refid="rangeFrom"/>
WHERE
<include refid="rangeWhere"/>