Compare commits

...
53 Commits
Author SHA1 Message Date
timi 944de228f7 Merge pull request 'v1.0.30' (#42) from dev into master
Reviewed-on: #42
2026-09-01 17:44:48 +00:00
Timi a81c8476e7 v1.0.30
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-02 01:44:32 +08:00
timi 25c9cd4dfc Merge pull request 'v1.0.30' (#41) from dev into master
Reviewed-on: #41
2026-09-01 17:42:44 +00:00
Timi e7cf1a72e3 v1.0.30
CI / build-deploy (pull_request) Failing after 6s
CI / notify-on-failure (pull_request) Successful in 0s
2026-09-02 01:42:24 +08:00
timi 882cd226ec Merge pull request 'v1.0.30' (#40) from dev into master
Reviewed-on: #40
2026-09-01 17:38:50 +00:00
Timi eec665109e v1.0.30
CI / build-deploy (pull_request) Failing after 3s
CI / notify-on-failure (pull_request) Successful in 0s
2026-09-02 01:37:47 +08:00
Timi f6a2f5a6f5 support half paid 2026-09-01 14:07:04 +08:00
Timi 9ba610de86 update store diary attachment 2026-09-01 14:06:13 +08:00
Timi e59dc363f4 v1.0.29
CI / build-deploy (pull_request) Skipped
CI / notify-on-failure (pull_request) Skipped
2026-09-01 01:17:10 +08:00
Timi eeb7b5b32e add GaoPackage 2026-09-01 01:16:52 +08:00
Timi b4b6a13f8d add GaoPackage 2026-08-31 20:31:28 +08:00
Timi 4a7de1909c fix store diary page 2026-08-31 17:26:33 +08:00
Timi b7fcecfa7b add store diary 2026-08-31 15:10:25 +08:00
Timi 3b68e6dd85 update event status 2026-08-31 12:20:51 +08:00
timi feb4d7af66 Merge pull request 'v1.0.28' (#38) from dev into master
Reviewed-on: #38
2026-08-30 15:51:37 +00:00
Timi 5cfcd24657 v1.0.28
CI / build-deploy (pull_request) Successful in 23s
CI / notify-on-failure (pull_request) Skipped
2026-08-30 23:51:06 +08:00
Timi 712118556b add event option 2026-08-30 23:50:47 +08:00
timi d75ab1ed5f Merge pull request 'v1.0.27' (#37) from dev into master
Reviewed-on: #37
2026-08-29 17:49:23 +00:00
Timi 4496619eeb v1.0.27
CI / build-deploy (pull_request) Successful in 23s
CI / notify-on-failure (pull_request) Skipped
2026-08-30 01:46:02 +08:00
Timi c85b400a94 fix require photo event 2026-08-30 01:25:22 +08:00
Timi 94c99e3444 fix update customer code 2026-08-29 16:05:24 +08:00
Timi 44c2095d77 add event verify 2026-08-29 15:35:11 +08:00
timi a501eb7443 Merge pull request 'v1.0.26' (#36) from dev into master
Reviewed-on: #36
2026-08-28 16:52:15 +00:00
Timi 35ec38ed9c v1.0.26
CI / build-deploy (pull_request) Successful in 23s
CI / notify-on-failure (pull_request) Skipped
2026-08-29 00:50:26 +08:00
Timi 2b5be16ad7 fix notify delete 2026-08-29 00:49:50 +08:00
Timi d747a0fdae add gao user notify 2026-08-25 22:55:18 +08:00
Timi 9f884e2b74 keep alive attachment variant 2026-08-25 22:54:55 +08:00
Timi 0239697a2b fix customer invited count 2026-08-25 17:57:54 +08:00
Timi 2b11bdf65e tiptap content for notify detail 2026-08-24 16:50:23 +08:00
timi 19941d9480 Merge pull request 'v1.0.25' (#35) from dev into master
Reviewed-on: #35
2026-08-23 17:50:25 +00:00
Timi 81933d4e61 v1.0.25
CI / build-deploy (pull_request) Successful in 22s
CI / notify-on-failure (pull_request) Skipped
2026-08-24 01:49:58 +08:00
Timi 43d76040d8 fix store permission 2026-08-24 01:49:44 +08:00
timi cd3d522d4f Merge pull request 'v1.0.24' (#34) from dev into master
Reviewed-on: #34
2026-08-22 09:56:13 +00:00
Timi 8de739cbb7 v1.0.24
CI / build-deploy (pull_request) Successful in 24s
CI / notify-on-failure (pull_request) Skipped
2026-08-22 17:55:54 +08:00
timi 181c5078e9 Merge pull request 'v1.0.23' (#33) from dev into master
Reviewed-on: #33
2026-08-22 09:51:37 +00:00
Timi e2e0023d13 v1.0.23
CI / build-deploy (pull_request) Successful in 23s
CI / notify-on-failure (pull_request) Skipped
2026-08-22 17:50:56 +08:00
Timi 8c6eed2379 update store delete 2026-08-22 17:21:30 +08:00
Timi c32ecd1f8c add point account list api 2026-08-22 16:42:09 +08:00
Timi 3c56e21efa add point totalConsumed 2026-08-22 11:45:00 +08:00
Timi 376f0ecd05 fix multi role invite error 2026-08-22 11:20:09 +08:00
timi e97f46e6b4 Merge pull request 'v1.0.22' (#32) from dev into master
Reviewed-on: #32
2026-08-21 17:31:33 +00:00
Timi a208947a1b v1.0.22
CI / build-deploy (pull_request) Successful in 24s
CI / notify-on-failure (pull_request) Skipped
2026-08-22 01:31:14 +08:00
timi c0f89deb83 Merge pull request 'v1.0.22' (#31) from dev into master
Reviewed-on: #31
2026-08-21 17:28:32 +00:00
Timi fc62663f2c v1.0.22
CI / build-deploy (pull_request) Failing after 3s
CI / notify-on-failure (pull_request) Successful in 0s
2026-08-22 01:27:28 +08:00
Timi fcae2eac09 fix gao user invite validate 2026-08-21 23:51:38 +08:00
Timi 2e9225852d add AttachmentVariant 2026-08-21 23:43:40 +08:00
Timi 3a3809b661 fix gao user invite 2026-08-21 21:54:09 +08:00
Timi 1b8936f197 support sms login 2026-08-21 18:13:58 +08:00
timi ce695463d3 Merge pull request 'v1.0.21' (#30) from dev into master
Reviewed-on: #30
2026-08-21 07:29:34 +00:00
Timi aff056712e v1.0.21
CI / build-deploy (pull_request) Successful in 22s
CI / notify-on-failure (pull_request) Skipped
2026-08-21 15:29:15 +08:00
Timi af18b0999d v1.0.20 2026-08-21 15:28:49 +08:00
timi 83c683d473 Merge pull request 'v1.0.20' (#29) from dev into master
Reviewed-on: #29
2026-08-21 07:25:32 +00:00
Timi 4963988ec2 v1.0.20
CI / build-deploy (pull_request) Successful in 30s
CI / notify-on-failure (pull_request) Skipped
2026-08-21 15:25:04 +08:00
179 changed files with 8372 additions and 560 deletions
+3 -8
View File
@@ -11,7 +11,7 @@
<groupId>com.imyeyu.timiserverapi</groupId>
<artifactId>TimiServerAPI</artifactId>
<version>1.0.19</version>
<version>1.0.30</version>
<packaging>jar</packaging>
<name>TimiServerAPI</name>
<description>imyeyu.com API</description>
@@ -198,7 +198,7 @@
<dependency>
<groupId>com.imyeyu.java</groupId>
<artifactId>timi-java</artifactId>
<version>0.0.7</version>
<version>0.0.11</version>
</dependency>
<dependency>
<groupId>com.imyeyu.utils</groupId>
@@ -213,7 +213,7 @@
<dependency>
<groupId>com.imyeyu.font.icon</groupId>
<artifactId>timi-icon-font</artifactId>
<version>0.0.1</version>
<version>0.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -240,11 +240,6 @@
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<dependency>
<groupId>com.e2ins.ms.notify</groupId>
<artifactId>e2ins-msg-notify-service-sdk</artifactId>
<version>0.0.9</version>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
@@ -1,12 +1,9 @@
package com.imyeyu.api.config;
import com.e2ins.ms.notify.SmsNotifyService;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
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;
@@ -32,12 +29,6 @@ public class BeanConfig {
.build();
}
@Bean
public SmsNotifyService smsNotifyService() {
// TODO
return new SmsNotifyService(null, null);
}
@Bean
public Yaml yaml() {
return new Yaml();
@@ -1,48 +0,0 @@
package com.imyeyu.api.modules.common.bean.attachment;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author 夜雨
* @since 2025-12-11 18:14
*/
public class Metadata {
/**
* 图片
*
* @author 夜雨
* @since 2025-12-11 18:15
*/
@Data
public static class Image {
private int width;
private int height;
}
/**
* 缩略图
*
* @author 夜雨
* @since 2026-01-04 18:10
*/
@Data
@EqualsAndHashCode(callSuper = true)
public static class ThumbImage extends Image {
private String sourceId;
private String sourceMongoId;
private String sourceMimeType;
/** 请求宽度(缓存键之一,null 表示未指定) */
private Integer requestWidth;
/** 请求高度(缓存键之一,null 表示未指定) */
private Integer requestHeight;
}
}
@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.common.bean.attachment;
/// 缩略图缩放模式
///
/// @author 夜雨
/// @since 2026-08-21
public enum ThumbnailFitMode {
/// 保持比例完整放入目标尺寸
FIT,
/// 保持比例裁剪填满目标尺寸
COVER,
/// 强制缩放到目标尺寸,可能导致图片变形
STRETCH
}
@@ -42,6 +42,19 @@ public class NotifyQueueConfig {
private String secretId;
private String secretKey;
private Captcha captcha;
/// 短信验证码
@Data
public static class Captcha {
private String templateId;
private int ttlMinute;
private int argIndex;
}
}
/// 微信队列配置
@@ -1,7 +1,9 @@
package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.bean.attachment.ThumbnailFitMode;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.AttachmentVariantService;
import com.imyeyu.api.modules.common.service.TempFileService;
import com.imyeyu.api.modules.common.vo.attach.TempFileResp;
import com.imyeyu.java.TimiJava;
@@ -41,6 +43,7 @@ public class AttachmentController {
private final TempFileService tempFileService;
private final AttachmentService service;
private final AttachmentVariantService variantService;
/**
* 查询附件详情
@@ -184,14 +187,19 @@ public class AttachmentController {
@AOPLog
@IgnoreGlobalReturn
@GetMapping({"/read/{id}", "/download/{id}"})
public void readById(@PathVariable String id, @RequestParam(required = false) Integer thumbWidth, @RequestParam(required = false) Integer thumbHeight) throws UnsupportedEncodingException {
public void readById(
@PathVariable String id,
@RequestParam(required = false) Integer thumbWidth,
@RequestParam(required = false) Integer thumbHeight,
@RequestParam(defaultValue = "FIT") ThumbnailFitMode fitMode
) throws UnsupportedEncodingException {
HttpServletResponse resp = TimiSpring.getResponse();
Attachment attach = service.get(id);
if (attach == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
responseAttachment(attach, thumbWidth, thumbHeight);
responseAttachment(attach, thumbWidth, thumbHeight, fitMode);
}
@AOPLog
@@ -207,21 +215,31 @@ public class AttachmentController {
public void read(@RequestParam String id) throws UnsupportedEncodingException {
HttpServletResponse resp = TimiSpring.getResponse();
Attachment attach = service.get(id);
if (attach == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (TimiSpring.getRequest().getRequestURI().endsWith("/download")) {
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
}
response(attach);
response(attach, false);
}
private void responseAttachment(Attachment attach, Integer thumbWidth, Integer thumbHeight) throws UnsupportedEncodingException {
HttpServletResponse resp = TimiSpring.getResponse();
if (attach != null && TimiSpring.getRequest().getRequestURI().endsWith("/download") && attach.getName() != null) {
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
}
responseThumb(attach, thumbWidth, thumbHeight);
private void responseAttachment(
Attachment attach,
Integer thumbWidth,
Integer thumbHeight,
ThumbnailFitMode fitMode
) throws UnsupportedEncodingException {
responseThumb(attach, thumbWidth, thumbHeight, fitMode);
}
protected void responseThumb(Attachment attachment, Integer thumbWidth, Integer thumbHeight) {
protected void responseThumb(
Attachment attachment,
Integer thumbWidth,
Integer thumbHeight,
ThumbnailFitMode fitMode
) throws UnsupportedEncodingException {
HttpServletResponse resp = TimiSpring.getResponse();
if (TimiJava.isEmpty(attachment)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
@@ -229,21 +247,65 @@ public class AttachmentController {
}
if (thumbWidth == null && thumbHeight == null) {
// 原图
setDownloadHeader(attachment);
response(attachment);
return;
}
// 缩略图
Attachment thumb = service.fetchThumb(attachment, thumbWidth, thumbHeight);
response(TimiJava.defaultIfNull(thumb, attachment));
Attachment thumb = variantService.fetchThumb(attachment, thumbWidth, thumbHeight, fitMode);
setDownloadHeader(thumb);
response(thumb);
}
private void setDownloadHeader(Attachment attachment) throws UnsupportedEncodingException {
if (attachment != null && TimiSpring.getRequest().getRequestURI().endsWith("/download") && attachment.getName() != null) {
TimiSpring.getResponse().setHeader("Content-Disposition", Network.getFileDownloadHeader(attachment.getName()));
}
}
/// 设置附件缓存头并处理条件请求
///
/// @param attachment 实际响应附件
/// @return true 表示客户端缓存仍有效,已返回 304
private boolean responseCache(Attachment attachment) {
HttpServletResponse resp = TimiSpring.getResponse();
resp.setHeader("Cache-Control", "private, no-cache");
String md5 = attachment.getMd5();
if (md5 == null || md5.isBlank()) {
return false;
}
String etag = "\"%s\"".formatted(md5);
resp.setHeader("ETag", etag);
String ifNoneMatch = TimiSpring.getHeader("If-None-Match");
if (ifNoneMatch == null) {
return false;
}
for (String value : ifNoneMatch.split(",")) {
String candidate = value.trim();
if ("*".equals(candidate) || etag.equals(candidate) || ("W/" + etag).equals(candidate)) {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return true;
}
}
return false;
}
protected void response(Attachment attachment) {
response(attachment, true);
}
private void response(Attachment attachment, boolean cacheable) {
HttpServletResponse resp = TimiSpring.getResponse();
try {
if (TimiJava.isEmpty(attachment)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (!cacheable) {
resp.setHeader("Cache-Control", "private, no-store");
} else if (responseCache(attachment)) {
return;
}
resp.setContentType(attachment.getMimeType());
attachment.doResponse();
} catch (Exception e) {
@@ -7,11 +7,17 @@ import com.imyeyu.api.modules.common.entity.Notify;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
import com.imyeyu.api.modules.common.service.NotifyDetailService;
import com.imyeyu.api.modules.common.service.NotifyService;
import com.imyeyu.api.modules.common.service.SmsService;
import com.imyeyu.api.modules.common.vo.NotifyManagementItem;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.CaptchaValid;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
@@ -37,6 +43,8 @@ import java.util.stream.Stream;
@RequestMapping("/notify")
public class NotifyController {
private final SmsService smsService;
private final UserService userService;
private final NotifyService service;
private final UserLoginService userLoginService;
@@ -63,25 +71,31 @@ public class NotifyController {
@RequiredToken
@RequestMapping("/detail")
public Notify detail(@RequestParam String id) {
Notify notify = service.get(id);
{
List<NotifyDetail> detailList = detailService.listByNotifyId(id);
Set<String> userIdSet = detailList.stream()
.filter(item -> item.getMsgType() == NotifyDetail.MsgType.INTERNAL)
.flatMap(item -> Stream.of(item.getSendFrom(), item.getSendTo()))
.collect(Collectors.toSet());
Map<String, User> userMap = userService.mapByIdList(userIdSet);
for (NotifyDetail detail : detailList) {
if (TimiJava.isNotEmpty(detail.getSendFrom())) {
detail.setFromUser(userMap.get(detail.getSendFrom()));
return fillDetailList(service.get(id));
}
if (TimiJava.isNotEmpty(detail.getSendTo())) {
detail.setToUser(userMap.get(detail.getSendTo()));
/// 查询管理端通知列表
///
/// @param page 分页参数
/// @return 管理端通知列表
@JsonView(ResponseView.Admin.class)
@RequireCorePermission(CorePermissionCode.NOTIFY_READ)
@RequiredToken
@PostMapping("/management/list")
public PageResult<NotifyManagementItem> managementList(@RequestBody(required = false) Page<Notify> page) {
return service.pageManagement(page, userLoginService.getRequireLoginUserId());
}
}
notify.setDetailList(detailList);
}
return notify;
/// 查询管理端通知详情及收件人
///
/// @param id 通知 ID
/// @return 通知详情及收件人
@JsonView(ResponseView.Admin.class)
@RequireCorePermission(CorePermissionCode.NOTIFY_READ)
@RequiredToken
@PostMapping("/management/detail")
public Notify managementDetail(@RequestParam String id) {
return fillDetailList(service.getManagement(id, userLoginService.getRequireLoginUserId()));
}
/// 删除通知及其通知详情
@@ -104,6 +118,34 @@ public class NotifyController {
service.cancel(id);
}
/// 删除管理端通知及其通知详情
///
/// @param id 通知 ID
@RequireCorePermission(CorePermissionCode.NOTIFY_DELETE)
@RequiredToken
@PostMapping("/management/delete")
public void managementDelete(@RequestParam String id) {
service.deleteManagement(id, userLoginService.getRequireLoginUserId());
}
/// 取消管理端尚未成功发送的通知
///
/// @param id 通知 ID
@RequireCorePermission(CorePermissionCode.NOTIFY_CANCEL)
@RequiredToken
@PostMapping("/management/cancel")
public void managementCancel(@RequestParam String id) {
service.cancelManagement(id, userLoginService.getRequireLoginUserId());
}
@AOPLog
@CaptchaValid
@RequestRateLimit(value = 1, inSeconds = 59)
@PostMapping("/sms/captcha/create")
public String smsCaptchaCreate(@RequestBody CaptchaData<Notify> req) {
return smsService.captchaCreate(req.getData());
}
/// 查询当前用户的站内通知
///
/// @param page 分页参数
@@ -134,14 +176,14 @@ public class NotifyController {
return detail;
}
/// 删除当前用户的一条站内通知
/// 隐藏当前用户的一条站内通知
///
/// @param id 通知详情 ID
@RequireCorePermission(CorePermissionCode.NOTIFY_READ)
@RequiredToken
@PostMapping("/internal/delete")
public void deleteInternal(@RequestParam String id) {
detailService.deleteInternalByUserId(id, userLoginService.getRequireLoginUserId());
detailService.hideInternalByUserId(id, userLoginService.getRequireLoginUserId());
}
/// 查询当前用户未读通知数量
@@ -171,4 +213,30 @@ public class NotifyController {
public void markAllSiteRead() {
detailService.markAllSiteRead(userLoginService.getRequireLoginUserId());
}
/// 回填通知详情中的用户信息
///
/// @param notify 通知
/// @return 已回填用户信息的通知
private Notify fillDetailList(Notify notify) {
if (notify == null) {
return null;
}
List<NotifyDetail> detailList = detailService.listByNotifyId(notify.getId());
Set<String> userIdSet = detailList.stream()
.filter(item -> item.getMsgType() == NotifyDetail.MsgType.INTERNAL)
.flatMap(item -> Stream.of(item.getSendFrom(), item.getSendTo()))
.collect(Collectors.toSet());
Map<String, User> userMap = userService.mapByIdList(userIdSet);
for (NotifyDetail detail : detailList) {
if (TimiJava.isNotEmpty(detail.getSendFrom())) {
detail.setFromUser(userMap.get(detail.getSendFrom()));
}
if (TimiJava.isNotEmpty(detail.getSendTo())) {
detail.setToUser(userMap.get(detail.getSendTo()));
}
}
notify.setDetailList(detailList);
return notify;
}
}
@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.modules.common.bean.attachment.SettingSupport;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.spring.annotation.table.PageIgnore;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
@@ -30,6 +31,9 @@ public class Article extends UUIDEntity implements SettingSupport {
/** 博客 */
BLOG,
/// GAO 门店日记
GAO_STORE_DIARY,
/// 反馈
FEEDBACK
}
@@ -93,6 +97,12 @@ public class Article extends UUIDEntity implements SettingSupport {
/** 文章状态 */
protected Status status;
/// 最后更新用户 ID
protected String updatedBy;
@Transient
protected User updater;
@Transient
protected List<Setting> settingList;
@@ -32,12 +32,18 @@ public class Attachment extends UUIDEntity {
ARTICLE,
NOTIFY_DETAIL,
USER,
GAO_CUSTOMER,
GAO_EVENT_RECORD,
GAO_PACKAGE_DELIVERY,
GAO_FUND_RECORD,
TEMP_FILE,
MIRROR
@@ -0,0 +1,68 @@
package com.imyeyu.api.modules.common.entity;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.modules.common.bean.attachment.ThumbnailFitMode;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.Id;
import lombok.Data;
/// 附件缩略图变体记录
///
/// @author 夜雨
/// @since 2026-08-21
@Data
public class AttachmentVariant {
/// 变体 ID
@Id
@AutoUUID
private String id;
/// 源附件 ID
private String sourceAttachmentId;
/// 源文件版本,使用源 GridFS ID
private String sourceMongoId;
/// 源文件摘要
private String sourceMd5;
/// 请求宽度,0 表示未指定
private int requestWidth;
/// 请求高度,0 表示未指定
private int requestHeight;
/// 缩放模式
private ThumbnailFitMode fitMode;
/// 输出格式
private String outputFormat;
/// 缩略图算法版本
private String algorithmVersion;
/// 缩略图 GridFS ID
private String mongoId;
/// 缩略图名称
private String name;
/// 缩略图 MIME 类型
private String mimeType;
/// 缩略图元数据
private ObjectNode metadata;
/// 缩略图大小
private long size;
/// 缩略图摘要
private String md5;
/// 创建时间
private long createdAt;
/// 过期时间
private long expireAt;
}
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.common.entity;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Creatable;
@@ -25,10 +26,10 @@ public class BaseQueueEntity implements IDEntity<String>, Creatable, Destroyable
protected String notifyDetailId;
/** 内容 */
protected String data;
protected String content;
/** 参数 */
protected String args;
protected JsonNode args;
/** 发送去向 */
protected String sendTo;
@@ -50,7 +51,7 @@ public class BaseQueueEntity implements IDEntity<String>, Creatable, Destroyable
/// @param detail 通知详情
public BaseQueueEntity(NotifyDetail detail) {
notifyDetailId = detail.getId();
data = detail.getData();
content = detail.getContent();
args = detail.getArgs();
sendTo = detail.getSendTo();
sendAt = detail.getSendAt();
@@ -1,6 +1,8 @@
package com.imyeyu.api.modules.common.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.service.BaseQueueService;
import com.imyeyu.api.modules.common.service.HTTPQueueService;
@@ -15,6 +17,8 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 通知详情
*
@@ -81,6 +85,19 @@ public class NotifyDetail extends UUIDEntity {
FAIL
}
/// 内容类型
///
/// @author 夜雨
/// @since 2026-08-24
public enum ContentType {
/** 纯文本 */
TEXT,
/** TipTap 富文本 */
TIP_TAP
}
/** 所属通知 ID */
@JsonView(ResponseView.Public.class)
private String notifyId;
@@ -99,11 +116,15 @@ public class NotifyDetail extends UUIDEntity {
/** 内容 */
@JsonView(ResponseView.Public.class)
private String data;
private String content;
/// 内容类型
@JsonView(ResponseView.Public.class)
private ContentType contentType;
/** 参数 */
@JsonView(ResponseView.Public.class)
private String args;
private JsonNode args;
/** 发送来源 */
@JsonView(ResponseView.Public.class)
@@ -137,6 +158,10 @@ public class NotifyDetail extends UUIDEntity {
@JsonView(ResponseView.Public.class)
private Long readAt;
/// 收件人隐藏时间,仅站内通知生效
@JsonIgnore
private Long hiddenAt;
@Transient
protected Notify notify;
@@ -145,4 +170,9 @@ public class NotifyDetail extends UUIDEntity {
@Transient
protected User toUser;
/// 媒体附件列表
@JsonView(ResponseView.Public.class)
@Transient
protected List<Attachment> attachmentList;
}
@@ -34,28 +34,10 @@ public interface AttachmentMapper extends BaseMapper<Attachment, String>, RawMap
List<Attachment> selectAllByBizIdList(Attachment.BizType bizType, Collection<String> bizIdList, List<String> attachTypes);
List<Attachment> selectImageByBizIdList(Attachment.BizType bizType, Collection<String> bizIdList, String attachType);
long countByBizId(Attachment.BizType bizType, String bizId, List<String> attachTypes);
/**
* 按源附件 ID 与请求尺寸查询已缓存缩略图
*
* @param bizType 源附件业务类型
* @param sourceId 源附件 ID
* @param requestWidth 请求宽度,{@code null} 表示未指定
* @param requestHeight 请求高度,{@code null} 表示未指定
* @return 缩略图附件,不存在返回 {@code null}
*/
Attachment selectThumb(Attachment.BizType bizType, String sourceId, Integer requestWidth, Integer requestHeight);
/**
* 查询源附件的所有未销毁缩略图(含已软删除)
*
* @param sourceId 源附件 ID
* @return 缩略图列表
*/
@Select("SELECT * FROM attachment WHERE biz_id = #{sourceId} AND attach_type = 'THUMB' AND is_destroyed = FALSE")
List<Attachment> selectThumbsBySourceId(String sourceId);
@Select("SELECT * FROM attachment WHERE `is_destroyed` = FALSE AND `destroy_at` < " + UNIX_TIME)
List<Attachment> selectNeedDestroy();
}
@@ -0,0 +1,86 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.AttachmentVariant;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// 附件缩略图变体 Mapper
///
/// @author 夜雨
/// @since 2026-08-21
public interface AttachmentVariantMapper {
/// 查询有效缩略图附件
///
/// @param sourceAttachmentId 源附件 ID
/// @param sourceMongoId 源 GridFS ID
/// @param requestWidth 请求宽度,0 表示未指定
/// @param requestHeight 请求高度,0 表示未指定
/// @param fitMode 缩放模式
/// @param outputFormat 输出格式
/// @param algorithmVersion 算法版本
/// @param now 当前时间
/// @return 缩略图附件
Attachment selectValidAttachment(
@Param("sourceAttachmentId") String sourceAttachmentId,
@Param("sourceMongoId") String sourceMongoId,
@Param("requestWidth") int requestWidth,
@Param("requestHeight") int requestHeight,
@Param("fitMode") String fitMode,
@Param("outputFormat") String outputFormat,
@Param("algorithmVersion") String algorithmVersion,
@Param("now") long now
);
/// 查询指定变体记录,不判断是否过期
///
/// @param sourceAttachmentId 源附件 ID
/// @param sourceMongoId 源 GridFS ID
/// @param requestWidth 请求宽度,0 表示未指定
/// @param requestHeight 请求高度,0 表示未指定
/// @param fitMode 缩放模式
/// @param outputFormat 输出格式
/// @param algorithmVersion 算法版本
/// @return 变体记录
AttachmentVariant selectByKey(
@Param("sourceAttachmentId") String sourceAttachmentId,
@Param("sourceMongoId") String sourceMongoId,
@Param("requestWidth") int requestWidth,
@Param("requestHeight") int requestHeight,
@Param("fitMode") String fitMode,
@Param("outputFormat") String outputFormat,
@Param("algorithmVersion") String algorithmVersion
);
/// 查询源附件的所有变体
///
/// @param sourceAttachmentId 源附件 ID
/// @return 变体列表
List<AttachmentVariant> selectBySourceAttachmentId(@Param("sourceAttachmentId") String sourceAttachmentId);
/// 查询已过期变体
///
/// @param now 当前时间
/// @return 已过期变体列表
List<AttachmentVariant> selectExpired(@Param("now") long now);
/// 新增变体
///
/// @param variant 变体
void insert(AttachmentVariant variant);
/// 延长缩略图变体的过期时间
///
/// @param id 变体 ID
/// @param now 当前时间
/// @param expireAt 新的过期时间
/// @return 更新记录数
int updateExpireAt(@Param("id") String id, @Param("now") long now, @Param("expireAt") long expireAt);
/// 删除变体
///
/// @param id 变体 ID
void delete(@Param("id") String id);
}
@@ -16,35 +16,37 @@ import java.util.List;
*/
public interface NotifyDetailMapper extends BaseMapper<NotifyDetail, String> {
String NOT_HIDDEN = " AND `hidden_at` IS NULL";
/** 查询用户的站内通知 */
@Select("SELECT * FROM `notify_detail` WHERE `msg_type` = 'INTERNAL' AND `send_to` = #{userId} " + NOT_DELETE + " ORDER BY `created_at` DESC LIMIT #{offset}, #{limit}")
@Select("SELECT * FROM `notify_detail` WHERE `msg_type` = 'INTERNAL' AND `send_to` = #{userId} AND `send_at` < " + UNIX_TIME + NOT_HIDDEN + NOT_DELETE + " ORDER BY `created_at` DESC LIMIT #{offset}, #{limit}")
List<NotifyDetail> selectSiteByUserId(@Param("userId") String userId, @Param("offset") long offset, @Param("limit") long limit);
/** 查询用户的未读站内通知 */
@Select("SELECT * FROM `notify_detail` WHERE `msg_type` = 'INTERNAL' AND `send_to` = #{userId} AND `read_at` IS NULL " + NOT_DELETE + " ORDER BY `created_at` DESC LIMIT #{offset}, #{limit}")
@Select("SELECT * FROM `notify_detail` WHERE `msg_type` = 'INTERNAL' AND `send_to` = #{userId} AND `send_at` < " + UNIX_TIME + " AND `read_at` IS NULL " + NOT_HIDDEN + NOT_DELETE + " ORDER BY `created_at` DESC LIMIT #{offset}, #{limit}")
List<NotifyDetail> selectUnreadSiteByUserId(@Param("userId") String userId, @Param("offset") long offset, @Param("limit") long limit);
/** 统计用户的站内通知数量 */
@Select("SELECT COUNT(*) FROM `notify_detail` WHERE `msg_type` = 'INTERNAL' AND `send_to` = #{userId} " + NOT_DELETE)
@Select("SELECT COUNT(*) FROM `notify_detail` WHERE `msg_type` = 'INTERNAL' AND `send_to` = #{userId} AND `send_at` < " + UNIX_TIME + NOT_HIDDEN + NOT_DELETE)
long countSiteByUserId(@Param("userId") String userId);
/** 统计用户的未读站内通知数量 */
@Select("SELECT COUNT(*) FROM `notify_detail` WHERE `msg_type` = 'INTERNAL' AND `send_to` = #{userId} AND `read_at` IS NULL " + NOT_DELETE)
@Select("SELECT COUNT(*) FROM `notify_detail` WHERE `msg_type` = 'INTERNAL' AND `send_to` = #{userId} AND `send_at` < " + UNIX_TIME + " AND `read_at` IS NULL " + NOT_HIDDEN + NOT_DELETE)
long countUnreadSiteByUserId(@Param("userId") String userId);
/** 查询用户拥有的站内通知 */
@Select("SELECT * FROM `notify_detail` WHERE `id` = #{detailId} AND `msg_type` = 'INTERNAL' AND `send_to` = #{userId} " + NOT_DELETE + " LIMIT 1")
@Select("SELECT * FROM `notify_detail` WHERE `id` = #{detailId} AND `msg_type` = 'INTERNAL' AND `send_to` = #{userId} AND `send_at` < " + UNIX_TIME + NOT_HIDDEN + NOT_DELETE + " LIMIT 1")
NotifyDetail selectSiteByIdAndUserId(@Param("detailId") String detailId, @Param("userId") String userId);
/** 删除用户的站内通知 */
@Update("UPDATE `notify_detail` SET `deleted_at` = " + UNIX_TIME + ", `updated_at` = " + UNIX_TIME + " WHERE `id` = #{detailId} AND `msg_type` = 'INTERNAL' AND `send_to` = #{userId} " + NOT_DELETE)
int deleteSiteByIdAndUserId(@Param("detailId") String detailId, @Param("userId") String userId);
/** 隐藏用户的站内通知 */
@Update("UPDATE `notify_detail` SET `hidden_at` = " + UNIX_TIME + ", `updated_at` = " + UNIX_TIME + " WHERE `id` = #{detailId} AND `msg_type` = 'INTERNAL' AND `send_to` = #{userId} AND `send_at` < " + UNIX_TIME + NOT_HIDDEN + NOT_DELETE)
int hideSiteByIdAndUserId(@Param("detailId") String detailId, @Param("userId") String userId);
/** 标记站内通知已读 */
@Update("UPDATE `notify_detail` SET `read_at` = #{readAt}, `updated_at` = " + UNIX_TIME + " WHERE `id` = #{detailId} AND `msg_type` = 'INTERNAL' AND `send_to` = #{userId} AND `read_at` IS NULL " + NOT_DELETE)
@Update("UPDATE `notify_detail` SET `read_at` = #{readAt}, `updated_at` = " + UNIX_TIME + " WHERE `id` = #{detailId} AND `msg_type` = 'INTERNAL' AND `send_to` = #{userId} AND `send_at` < " + UNIX_TIME + " AND `read_at` IS NULL " + NOT_HIDDEN + NOT_DELETE)
int markSiteRead(@Param("detailId") String detailId, @Param("userId") String userId, @Param("readAt") long readAt);
/** 标记用户全部站内通知已读 */
@Update("UPDATE `notify_detail` SET `read_at` = #{readAt}, `updated_at` = " + UNIX_TIME + " WHERE `msg_type` = 'INTERNAL' AND `send_to` = #{userId} AND `read_at` IS NULL " + NOT_DELETE)
@Update("UPDATE `notify_detail` SET `read_at` = #{readAt}, `updated_at` = " + UNIX_TIME + " WHERE `msg_type` = 'INTERNAL' AND `send_to` = #{userId} AND `send_at` < " + UNIX_TIME + " AND `read_at` IS NULL " + NOT_HIDDEN + NOT_DELETE)
int markAllSiteRead(@Param("userId") String userId, @Param("readAt") long readAt);
}
@@ -1,8 +1,12 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Notify;
import com.imyeyu.api.modules.common.vo.NotifyManagementItem;
import org.apache.ibatis.annotations.Param;
import com.imyeyu.spring.mapper.BaseMapper;
import java.util.List;
/**
* 通知 Mapper
*
@@ -10,4 +14,25 @@ import com.imyeyu.spring.mapper.BaseMapper;
* @since 2026-05-13 14:46
*/
public interface NotifyMapper extends BaseMapper<Notify, String> {
/// 分页查询管理端通知
///
/// @param userId 管理员用户 ID
/// @param offset 分页偏移量
/// @param limit 分页大小
/// @return 通知列表
List<NotifyManagementItem> selectManagementPage(@Param("userId") String userId, @Param("offset") long offset, @Param("limit") long limit);
/// 统计管理端通知数量
///
/// @param userId 管理员用户 ID
/// @return 通知数量
long countManagement(@Param("userId") String userId);
/// 查询管理员可管理的通知
///
/// @param notifyId 通知 ID
/// @param userId 管理员用户 ID
/// @return 通知
Notify selectManagement(@Param("notifyId") String notifyId, @Param("userId") String userId);
}
@@ -25,18 +25,6 @@ public interface AttachmentService extends BaseService<Attachment, String> {
/// @return 克隆后的附件
Attachment clone(String id);
/**
* 按需获取或创建缩略图附件。
* <p>当附件为图片或视频时,若指定尺寸的缩略图已存在则直接返回缓存,
* 否则生成并持久化后返回。不支持缩略图的附件类型返回 {@code null}。</p>
*
* @param source 源附件(必须已持久化,mimeType 已填充)
* @param requestWidth 请求宽度,{@code null} 表示未指定
* @param requestHeight 请求高度,{@code null} 表示未指定
* @return 缩略图附件,不支持时返回 {@code null}
*/
Attachment fetchThumb(Attachment source, Integer requestWidth, Integer requestHeight);
Attachment getByBizId(Attachment.BizType bizType, String bizId);
Attachment getByAttachType(Attachment.BizType bizType, String bizId, String attachType);
@@ -57,6 +45,12 @@ public interface AttachmentService extends BaseService<Attachment, String> {
*/
List<Attachment> listByBizId(Attachment.BizType bizType, String bizId, String... attachTypes);
default Map<String, Attachment> mapFirstImageByIdList(Attachment.BizType bizType, Collection<String> bizIdList) {
return mapFirstImageByIdList(bizType, bizIdList, null);
}
Map<String, Attachment> mapFirstImageByIdList(Attachment.BizType bizType, Collection<String> bizIdList, String attachType);
/**
* 根据多个业务 ID 批量获取附件
*
@@ -0,0 +1,28 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.bean.attachment.ThumbnailFitMode;
import com.imyeyu.api.modules.common.entity.Attachment;
/// 附件缩略图变体服务
///
/// @author 夜雨
/// @since 2026-08-21
public interface AttachmentVariantService {
/// 按需获取或创建缩略图附件
///
/// @param source 源附件
/// @param requestWidth 请求宽度
/// @param requestHeight 请求高度
/// @param fitMode 缩放模式
/// @return 缩略图附件
Attachment fetchThumb(Attachment source, Integer requestWidth, Integer requestHeight, ThumbnailFitMode fitMode);
/// 删除源附件对应的全部缩略图变体
///
/// @param sourceAttachmentId 源附件 ID
void deleteBySourceId(String sourceAttachmentId);
/// 清理已过期的缩略图变体
void clearExpired();
}
@@ -41,11 +41,11 @@ public interface NotifyDetailService extends BaseService<NotifyDetail, String> {
/// @return 通知详情
NotifyDetail getInternalByUserId(String detailId, String userId);
/// 删除当前用户拥有的站内通知详情
/// 隐藏当前用户拥有的站内通知详情
///
/// @param detailId 通知详情 ID
/// @param userId 用户 ID
void deleteInternalByUserId(String detailId, String userId);
void hideInternalByUserId(String detailId, String userId);
/// 查询用户未读站内通知数量
///
@@ -1,6 +1,9 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Notify;
import com.imyeyu.api.modules.common.vo.NotifyManagementItem;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
/**
@@ -11,6 +14,32 @@ import com.imyeyu.spring.service.BaseService;
*/
public interface NotifyService extends BaseService<Notify, String> {
/// 分页查询当前用户创建的通知
///
/// @param page 分页参数
/// @param userId 用户 ID
/// @return 管理端通知列表
PageResult<NotifyManagementItem> pageManagement(Page<Notify> page, String userId);
/// 查询当前用户可管理的通知
///
/// @param notifyId 通知 ID
/// @param userId 用户 ID
/// @return 通知
Notify getManagement(String notifyId, String userId);
/// 删除当前用户创建的通知
///
/// @param notifyId 通知 ID
/// @param userId 用户 ID
void deleteManagement(String notifyId, String userId);
/// 取消当前用户创建的通知
///
/// @param notifyId 通知 ID
/// @param userId 用户 ID
void cancelManagement(String notifyId, String userId);
/// 取消通知中尚未成功发送的通知详情
///
/// @param notifyId 通知 ID
@@ -0,0 +1,30 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Notify;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
/**
*
*
* @author 夜雨
* @since 2026-05-14 12:12
*/
public interface SmsService {
/**
* 创建短信验证码通知
*
* @param detail 通知
* @return 通知详情 ID
*/
String captchaCreate(Notify detail);
/**
* 校验短信验证码
*
* @param detailId 通知详情 ID
* @param captcha 请求验证码
* @return 通知详情
*/
NotifyDetail captchaVerify(String detailId, String captcha);
}
@@ -39,4 +39,10 @@ public interface TagService extends BaseService<Tag, String> {
/// @param bizId 业务 ID
/// @param tagIdList 标签 ID 列表
void apply(TagApply.BizType bizType, String bizId, List<String> tagIdList);
/// 删除指定归属下的全部标签
///
/// @param ownerType 归属类型
/// @param ownerId 归属 ID
void deleteByOwner(Tag.OwnerType ownerType, String ownerId);
}
@@ -3,19 +3,14 @@ package com.imyeyu.api.modules.common.service.implement;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.bean.attachment.MediaAttach;
import com.imyeyu.api.modules.common.bean.attachment.Metadata;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.mapper.AttachmentMapper;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.util.JavaCV;
import com.imyeyu.api.modules.common.service.AttachmentVariantService;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArg;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.network.Network;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
@@ -26,7 +21,6 @@ import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.model.GridFSFile;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.tika.Tika;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
@@ -36,18 +30,16 @@ import org.springframework.transaction.annotation.Transactional;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
@@ -64,6 +56,7 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
private final ObjectMapper jackson;
private final GridFSBucket gridFSBucket;
private final GridFsTemplate gridFsTemplate;
private final AttachmentVariantService variantService;
@Override
protected BaseMapper<Attachment, String> mapper() {
@@ -124,6 +117,7 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
}
// 有新文件时,先查旧记录获取旧 mongoId 和兜底字段
Attachment old = mapper.selectRaw(attachment.getId());
TimiException.required(old, "not found attachment");
// 存储新文件
newMongoId = gridFsTemplate.store(stream, attachment.buildMongoName()).toString();
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(newMongoId)));
@@ -141,6 +135,8 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
// 更新数据库后删除旧 GridFS 文件
super.update(attachment);
// 源文件版本已变化,旧缩略图全部失效
variantService.deleteBySourceId(attachment.getId());
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(old.getMongoId())));
} catch (Exception e) {
// 新文件已上传但后续操作失败,清理新文件防止孤儿数据
@@ -155,36 +151,18 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(String id) {
mapper.selectThumbsBySourceId(id).forEach(thumb -> super.delete(thumb.getId()));
variantService.deleteBySourceId(id);
super.delete(id);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void destroy(String id) {
CallbackArg<Attachment> doDestroy = attach -> {
try {
if (attach.getMongoId() != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(attach.getMongoId())));
variantService.deleteBySourceId(id);
Attachment attachment = mapper.selectRaw(id);
if (attachment != null) {
destroyAttachment(attachment);
}
if (!attach.isDeleted()) {
attach.setDeletedAt(Time.now());
}
attach.setIsDestroyed(true);
attach.setDestroyAt(Time.now());
mapper.update(attach);
if (Ref.toType(MediaAttach.Type.class, attach.getAttachType()) == MediaAttach.Type.THUMB) {
// 缩略图记录物理删除
mapper.destroy(attach.getId());
}
} catch (Exception e) {
log.error("delete mongo file error, id={}", attach.getId(), e);
throw new TimiException(TimiCode.ERROR, "TODO delete mongo file error");
}
};
mapper.selectThumbsBySourceId(id).forEach(doDestroy::handler);
doDestroy.handler(mapper.selectRaw(id));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@@ -216,74 +194,20 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public Attachment fetchThumb(Attachment source, Integer requestWidth, Integer requestHeight) {
TimiException.requiredTrue(requestWidth != null || requestHeight != null, "required width or height");
String mimeType = source.getMimeType();
boolean isImage = mimeType != null && Set.of("image/png", "image/bmp", "image/jpeg").contains(mimeType);
boolean isVideo = mimeType != null && Set.of("video/mp4", "video/quicktime").contains(mimeType);
if (!isImage && !isVideo) {
return null;
}
Attachment cached = mapper.selectThumb(source.getBizType(), source.getId(), requestWidth, requestHeight);
if (cached != null) {
// 命中缓存直接返回
return cached;
}
private void destroyAttachment(Attachment attachment) {
try {
ByteArrayOutputStream thumbStream = new ByteArrayOutputStream();
CallbackArg<InputStream> applyThumbSize = inputStream -> {
try {
Thumbnails.Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
if (requestWidth != null && requestHeight != null) {
builder.size(requestWidth, requestHeight).keepAspectRatio(false).toOutputStream(thumbStream);
} else if (requestWidth != null) {
builder.width(requestWidth).keepAspectRatio(true).toOutputStream(thumbStream);
} else {
builder.height(requestHeight).keepAspectRatio(true).toOutputStream(thumbStream);
if (attachment.getMongoId() != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(attachment.getMongoId())));
}
} catch (IOException e) {
log.error("applyThumbSize error for fetchThumb", e);
throw new RuntimeException(e);
if (!attachment.isDeleted()) {
attachment.setDeletedAt(Time.now());
}
};
if (isImage) {
applyThumbSize.handler(source.openInputStream());
} else {
String name = source.getName();
File tempFile = IO.file("temp/%s_%s".formatted(UUID.randomUUID(), name));
try {
IO.toFile(tempFile, source.openInputStream());
ByteArrayOutputStream baos = JavaCV.captureThumbnail(IO.getInputStream(tempFile), 2);
applyThumbSize.handler(IO.toInputStream(baos));
} finally {
IO.destroy(tempFile);
}
}
Metadata.ThumbImage thumbMeta = new Metadata.ThumbImage();
thumbMeta.setSourceId(source.getId());
thumbMeta.setSourceMongoId(source.getMongoId());
thumbMeta.setSourceMimeType(mimeType);
thumbMeta.setRequestWidth(requestWidth);
thumbMeta.setRequestHeight(requestHeight);
Attachment thumb = new Attachment();
thumb.setName(Network.simpleURIFileName(source.getName()) + ".png");
thumb.setBizType(source.getBizType());
thumb.setBizId(source.getId());
thumb.setAttachType(MediaAttach.Type.THUMB.toString());
thumb.setMetadata(jackson.valueToTree(thumbMeta));
thumb.setInputStream(new ByteArrayInputStream(thumbStream.toByteArray()));
// 缩略图 7 天后过期,额外 1 天宽限供后台任务销毁
long expireAt = Time.now() + 7 * Time.D;
thumb.setDeletedAt(expireAt);
thumb.setDestroyAt(expireAt + Time.D);
create(thumb);
return thumb;
attachment.setIsDestroyed(true);
attachment.setDestroyAt(Time.now());
mapper.update(attachment);
} catch (Exception e) {
log.error("create thumbnail error", e);
throw new TimiException(TimiCode.ERROR, "exception.attachment.create.error");
log.error("delete mongo file error, id={}", attachment.getId(), e);
throw new TimiException(TimiCode.ERROR, "TODO delete mongo file error", e);
}
}
@@ -322,9 +246,14 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
return mapper.selectAllByBizIdList(bizType, List.of(bizId), List.of(attachTypes));
}
@Override
public Map<String, Attachment> mapFirstImageByIdList(Attachment.BizType bizType, Collection<String> bizIdList, String attachType) {
return mapper.selectImageByBizIdList(bizType, new HashSet<>(bizIdList), attachType).stream().collect(Collectors.toMap(Attachment::getBizId, Function.identity()));
}
@Override
public Map<String, List<Attachment>> mapByBizIdList(Attachment.BizType bizType, Collection<String> bizIdList, String... attachTypes) {
return mapper.selectAllByBizIdList(bizType, bizIdList, List.of(attachTypes)).stream().collect(Collectors.groupingBy(Attachment::getBizId));
return mapper.selectAllByBizIdList(bizType, new HashSet<>(bizIdList), List.of(attachTypes)).stream().collect(Collectors.groupingBy(Attachment::getBizId));
}
@Override
@@ -390,6 +319,8 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
// 将临时文件的 GridFS 存储转移至当前永久记录,废弃临时记录
Attachment current = mapper.selectRaw(item.getId());
Attachment tempFile = mapper.selectRaw(tempFileId);
// 源文件版本已变化,旧缩略图全部失效
variantService.deleteBySourceId(current.getId());
// 删除旧 GridFS 文件
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(current.getMongoId())));
// 将存储信息从临时记录迁移到永久记录
@@ -0,0 +1,377 @@
package com.imyeyu.api.modules.common.service.implement;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.bean.attachment.MediaAttach;
import com.imyeyu.api.modules.common.bean.attachment.ThumbnailFitMode;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.AttachmentVariant;
import com.imyeyu.api.modules.common.mapper.AttachmentVariantMapper;
import com.imyeyu.api.modules.common.service.AttachmentVariantService;
import com.imyeyu.api.util.JavaCV;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.Network;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.utils.Time;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.model.GridFSFile;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import net.coobird.thumbnailator.geometry.Positions;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.gridfs.GridFsTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Set;
import java.util.UUID;
/// 附件缩略图变体服务实现
///
/// 负责缩略图生成、变体缓存、并发控制和变体文件清理,源附件生命周期由 `AttachmentService` 负责
///
/// @author 夜雨
/// @since 2026-08-21
@Slf4j
@Service
@RequiredArgsConstructor
public class AttachmentVariantServiceImplement implements AttachmentVariantService {
private static final int THUMB_MAX_WIDTH = 4096;
private static final int THUMB_MAX_HEIGHT = 4096;
private static final long THUMB_MAX_PIXELS = 16_000_000L;
private static final long THUMB_CACHE_TTL = Time.D * 7;
private static final long THUMB_LOCK_TTL = Time.M * 10;
private static final double THUMB_TARGET_SECONDS = 2;
private static final int VARIANT_NAME_MAX_LENGTH = 255;
private static final String THUMB_OUTPUT_FORMAT = "png";
private static final String THUMB_ALGORITHM_VERSION = "v3";
private static final String THUMB_LOCK_KEY_PREFIX = "ATTACHMENT:THUMB:LOCK:";
private final AttachmentVariantMapper variantMapper;
private final ObjectMapper jackson;
private final GridFSBucket gridFSBucket;
private final GridFsTemplate gridFsTemplate;
@Qualifier("redisLocker")
private final Redis<String, Integer> redisLocker;
@Override
public Attachment fetchThumb(Attachment source, Integer requestWidth, Integer requestHeight, ThumbnailFitMode fitMode) {
TimiException.required(source, "not found source attachment");
TimiException.required(source.getId(), "not found source attachment.id");
TimiException.required(source.getMongoId(), "not found source attachment.mongoId");
TimiException.requiredTrue(requestWidth != null || requestHeight != null, "required width or height");
if (requestWidth != null && !(0 < requestWidth && requestWidth < THUMB_MAX_WIDTH + 1)) {
throw new TimiException(TimiCode.ARG_BAD, "attachment.thumbnail.width.invalid");
}
if (requestHeight != null && !(0 < requestHeight && requestHeight < THUMB_MAX_HEIGHT + 1)) {
throw new TimiException(TimiCode.ARG_BAD, "attachment.thumbnail.height.invalid");
}
if (requestWidth != null && requestHeight != null) {
long pixels = (long) requestWidth * requestHeight;
if (THUMB_MAX_PIXELS < pixels) {
throw new TimiException(TimiCode.ARG_BAD, "attachment.thumbnail.area.invalid");
}
}
fitMode = TimiJava.defaultIfNull(fitMode, ThumbnailFitMode.FIT);
String mimeType = source.getMimeType();
boolean isImage = mimeType != null && Set.of("image/png", "image/bmp", "image/jpeg").contains(mimeType);
boolean isVideo = mimeType != null && Set.of("video/mp4", "video/quicktime").contains(mimeType);
if (!isImage && !isVideo) {
throw new TimiException(TimiCode.ARG_BAD, "attachment.thumbnail.not.support");
}
int width = TimiJava.defaultIfNull(requestWidth, 0);
int height = TimiJava.defaultIfNull(requestHeight, 0);
long now = Time.now();
Attachment cached = variantMapper.selectValidAttachment(
source.getId(),
source.getMongoId(),
width,
height,
fitMode.toString(),
THUMB_OUTPUT_FORMAT,
THUMB_ALGORITHM_VERSION,
now
);
if (cached != null) {
Attachment refreshed = refreshCachedThumb(cached);
if (refreshed != null) {
return refreshed;
}
}
String lockKey = buildThumbLockKey(source, width, height, fitMode);
if (!redisLocker.lock(lockKey, 1, THUMB_LOCK_TTL)) {
throw new TimiException(TimiCode.ERROR_SERVICE_BUSY, "attachment.thumbnail.generating");
}
try {
cached = variantMapper.selectValidAttachment(
source.getId(),
source.getMongoId(),
width,
height,
fitMode.toString(),
THUMB_OUTPUT_FORMAT,
THUMB_ALGORITHM_VERSION,
Time.now()
);
if (cached != null) {
Attachment refreshed = refreshCachedThumb(cached);
if (refreshed != null) {
return refreshed;
}
}
AttachmentVariant oldVariant = variantMapper.selectByKey(
source.getId(),
source.getMongoId(),
width,
height,
fitMode.toString(),
THUMB_OUTPUT_FORMAT,
THUMB_ALGORITHM_VERSION
);
if (oldVariant != null) {
deleteVariant(oldVariant);
}
File thumbFile = null;
AttachmentVariant variant = null;
boolean variantInserted = false;
try {
thumbFile = createThumbFile(source, requestWidth, requestHeight, fitMode, isImage);
variant = new AttachmentVariant();
variant.setId(UUID.randomUUID().toString());
variant.setName(buildVariantName(source.getName()));
variant.setMimeType("image/" + THUMB_OUTPUT_FORMAT);
long expireAt = Time.now() + THUMB_CACHE_TTL;
variant.setSourceAttachmentId(source.getId());
variant.setSourceMongoId(source.getMongoId());
variant.setSourceMd5(source.getMd5());
variant.setRequestWidth(width);
variant.setRequestHeight(height);
variant.setFitMode(fitMode);
variant.setOutputFormat(THUMB_OUTPUT_FORMAT);
variant.setAlgorithmVersion(THUMB_ALGORITHM_VERSION);
variant.setCreatedAt(Time.now());
variant.setExpireAt(expireAt);
BufferedImage image = ImageIO.read(thumbFile);
if (image != null) {
ObjectNode metadata = jackson.createObjectNode();
metadata.put("width", image.getWidth());
metadata.put("height", image.getHeight());
variant.setMetadata(metadata);
}
try (InputStream inputStream = Files.newInputStream(thumbFile.toPath())) {
String mongoId = gridFsTemplate.store(inputStream, variant.getName()).toString();
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(mongoId)));
variant.setMongoId(mongoId);
variant.setSize(gridFSFile.getLength());
try (InputStream md5InputStream = gridFSBucket.openDownloadStream(gridFSFile.getObjectId())) {
variant.setMd5(IO.md5(md5InputStream));
}
}
try {
variantMapper.insert(variant);
variantInserted = true;
} catch (DuplicateKeyException e) {
destroyVariantFile(variant);
Attachment concurrent = variantMapper.selectValidAttachment(
source.getId(),
source.getMongoId(),
width,
height,
fitMode.toString(),
THUMB_OUTPUT_FORMAT,
THUMB_ALGORITHM_VERSION,
Time.now()
);
if (concurrent != null) {
Attachment refreshed = refreshCachedThumb(concurrent);
if (refreshed != null) {
return refreshed;
}
}
throw e;
}
return toThumbAttachment(source, variant);
} catch (Exception e) {
if (variant != null) {
destroyVariantFile(variant);
if (variantInserted) {
variantMapper.delete(variant.getId());
}
}
throw e;
} finally {
IO.destroy(thumbFile);
}
} catch (TimiException e) {
throw e;
} catch (Exception e) {
log.error("create thumbnail error", e);
throw new TimiException(TimiCode.ERROR, "exception.attachment.create.error", e);
} finally {
redisLocker.releaseLock(lockKey);
}
}
/// 延长已命中缩略图缓存的有效期
///
/// @param cached 已命中的缩略图
/// @return 更新成功时返回刷新后的缩略图,记录已不存在或已过期时返回 `null`
private Attachment refreshCachedThumb(Attachment cached) {
long now = Time.now();
long expireAt = now + THUMB_CACHE_TTL;
if (!(0 < variantMapper.updateExpireAt(cached.getId(), now, expireAt))) {
return null;
}
cached.setDestroyAt(expireAt);
return cached;
}
private String buildThumbLockKey(Attachment source, int width, int height, ThumbnailFitMode fitMode) {
return "%s%s:%s:%s:%s:%s".formatted(
THUMB_LOCK_KEY_PREFIX,
source.getId(),
source.getMongoId(),
width,
height,
fitMode
);
}
/// 构建不超过数据库字段长度的变体名称
///
/// @param sourceName 源附件名称
/// @return 变体名称
private String buildVariantName(String sourceName) {
String suffix = "." + THUMB_OUTPUT_FORMAT;
String baseName = Network.simpleURIFileName(sourceName);
int maxBaseLength = VARIANT_NAME_MAX_LENGTH - suffix.length();
if (maxBaseLength < baseName.codePointCount(0, baseName.length())) {
baseName = baseName.substring(0, baseName.offsetByCodePoints(0, maxBaseLength));
}
return baseName + suffix;
}
private File createThumbFile(
Attachment source,
Integer requestWidth,
Integer requestHeight,
ThumbnailFitMode fitMode,
boolean isImage
) throws Exception {
File thumbFile = Files.createTempFile("timi-thumb-", ".png").toFile();
try {
if (isImage) {
try (InputStream inputStream = source.openInputStream()) {
resize(inputStream, thumbFile, requestWidth, requestHeight, fitMode);
}
return thumbFile;
}
try (InputStream inputStream = source.openInputStream()) {
ByteArrayOutputStream frame = JavaCV.captureThumbnail(inputStream, THUMB_TARGET_SECONDS);
if (!(0 < frame.size())) {
throw new TimiException(TimiCode.ERROR_NOT_SUPPORT, "attachment.thumbnail.video.frame.empty");
}
try (InputStream frameInputStream = new ByteArrayInputStream(frame.toByteArray())) {
resize(frameInputStream, thumbFile, requestWidth, requestHeight, fitMode);
}
return thumbFile;
}
} catch (Exception e) {
IO.destroy(thumbFile);
throw e;
}
}
private void resize(
InputStream inputStream,
File outputFile,
Integer requestWidth,
Integer requestHeight,
ThumbnailFitMode fitMode
) throws IOException {
Thumbnails.Builder<? extends InputStream> builder = Thumbnails.of(inputStream)
.useExifOrientation(true)
.outputFormat(THUMB_OUTPUT_FORMAT);
if (requestWidth != null && requestHeight != null) {
switch (fitMode) {
case FIT -> builder.size(requestWidth, requestHeight).keepAspectRatio(true);
case COVER -> builder.size(requestWidth, requestHeight).keepAspectRatio(true).crop(Positions.CENTER);
case STRETCH -> builder.size(requestWidth, requestHeight).keepAspectRatio(false);
}
} else if (requestWidth != null) {
builder.width(requestWidth).keepAspectRatio(true);
} else {
builder.height(requestHeight).keepAspectRatio(true);
}
builder.toFile(outputFile);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void deleteBySourceId(String sourceAttachmentId) {
for (AttachmentVariant variant : variantMapper.selectBySourceAttachmentId(sourceAttachmentId)) {
deleteVariant(variant);
}
}
private void deleteVariant(AttachmentVariant variant) {
destroyVariantFile(variant);
variantMapper.delete(variant.getId());
}
private void destroyVariantFile(AttachmentVariant variant) {
if (variant.getMongoId() != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(variant.getMongoId())));
}
}
private Attachment toThumbAttachment(Attachment source, AttachmentVariant variant) {
Attachment thumb = new Attachment();
thumb.setId(variant.getId());
thumb.setBizType(source.getBizType());
thumb.setBizId(source.getId());
thumb.setAttachType(MediaAttach.Type.THUMB.toString());
thumb.setMongoId(variant.getMongoId());
thumb.setName(variant.getName());
thumb.setMimeType(variant.getMimeType());
thumb.setMetadata(variant.getMetadata());
thumb.setSize(variant.getSize());
thumb.setMd5(variant.getMd5());
thumb.setIsDestroyed(false);
thumb.setDestroyAt(variant.getExpireAt());
return thumb;
}
@Override
public void clearExpired() {
for (AttachmentVariant variant : variantMapper.selectExpired(Time.now())) {
try {
deleteVariant(variant);
} catch (Exception e) {
log.error("clear expired thumbnail variant error, id={}", variant.getId(), e);
}
}
}
}
@@ -60,7 +60,7 @@ public class MailQueueServiceImplement extends AbstractQueueService<MailQueue, N
message.setFrom(queue.getSendFrom());
message.setTo(queue.getSendTo());
message.setSubject(queue.getSubject());
message.setText(queue.getData());
message.setText(queue.getContent());
mailSender.send(message);
log.info("邮件发送成功,目标:{}", queue.getSendTo());
}
@@ -1,10 +1,12 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Notify;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
import com.imyeyu.api.modules.common.mapper.NotifyDetailMapper;
import com.imyeyu.api.modules.common.mapper.NotifyMapper;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.NotifyDetailService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
@@ -19,6 +21,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
/**
* 通知详情服务实现
@@ -31,6 +34,7 @@ import java.util.List;
public class NotifyDetailServiceImplement extends AbstractEntityService<NotifyDetail, String> implements NotifyDetailService {
private final NotifyMapper notifyMapper;
private final AttachmentService attachmentService;
private final NotifyDetailMapper mapper;
@@ -53,15 +57,22 @@ public class NotifyDetailServiceImplement extends AbstractEntityService<NotifyDe
if (detail.getMsgType() == NotifyDetail.MsgType.INTERNAL) {
TimiException.required(detail.getSendTo(), "未找到站内通知接收用户 ID");
}
detail.setContentType(TimiJava.defaultIfNull(detail.getContentType(), detail.getMsgType() == NotifyDetail.MsgType.INTERNAL
? NotifyDetail.ContentType.TIP_TAP
: NotifyDetail.ContentType.TEXT));
detail.setRetry(TimiJava.defaultIfNull(detail.getRetry(), 1));
detail.setSendAt(TimiJava.defaultIfNull(detail.getSendAt(), Time.now()));
detail.setTimeoutAt(TimiJava.defaultIfNull(detail.getTimeoutAt(), detail.getSendAt() + Time.D));
detail.setStatus(detail.getMsgType() == NotifyDetail.MsgType.INTERNAL
boolean scheduled = Time.now() < detail.getSendAt();
detail.setStatus(detail.getMsgType() == NotifyDetail.MsgType.INTERNAL && !scheduled
? NotifyDetail.Status.REMINDED
: NotifyDetail.Status.WAITING);
detail.setReadAt(null);
super.create(detail);
if (detail.getMsgType() == NotifyDetail.MsgType.INTERNAL) {
attachmentService.updateByBizId(Attachment.BizType.NOTIFY_DETAIL, detail.getId(), detail.getAttachmentList());
}
if (detail.getMsgType().isQueue()) {
// 加入外部消息队列
@@ -77,7 +88,9 @@ public class NotifyDetailServiceImplement extends AbstractEntityService<NotifyDe
public List<NotifyDetail> listByNotifyId(String notifyId) {
NotifyDetail example = new NotifyDetail();
example.setNotifyId(notifyId);
return mapper.selectAllByExample(example);
List<NotifyDetail> detailList = mapper.selectAllByExample(example);
loadAttachments(detailList);
return detailList;
}
/// 删除通知详情
@@ -135,9 +148,11 @@ public class NotifyDetailServiceImplement extends AbstractEntityService<NotifyDe
result.setTotal(unreadOnly
? mapper.countUnreadSiteByUserId(userId)
: mapper.countSiteByUserId(userId));
result.setList(unreadOnly
List<NotifyDetail> detailList = unreadOnly
? mapper.selectUnreadSiteByUserId(userId, validPage.getOffset(), validPage.getLimit())
: mapper.selectSiteByUserId(userId, validPage.getOffset(), validPage.getLimit()));
: mapper.selectSiteByUserId(userId, validPage.getOffset(), validPage.getLimit());
loadAttachments(detailList);
result.setList(detailList);
return result;
}
@@ -152,19 +167,51 @@ public class NotifyDetailServiceImplement extends AbstractEntityService<NotifyDe
TimiException.required(userId, "未找到用户 ID");
NotifyDetail detail = mapper.selectSiteByIdAndUserId(detailId, userId);
TimiException.required(detail, "未找到站内通知");
loadAttachments(detail);
return detail;
}
/// 删除当前用户拥有的站内通知详情
/// 回填通知详情的媒体附件
///
/// @param detailList 通知详情列表
private void loadAttachments(List<NotifyDetail> detailList) {
List<String> detailIdList = detailList.stream()
.filter(detail -> detail.getMsgType() == NotifyDetail.MsgType.INTERNAL)
.map(NotifyDetail::getId)
.toList();
if (detailIdList.isEmpty()) {
return;
}
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(
Attachment.BizType.NOTIFY_DETAIL,
detailIdList
);
for (NotifyDetail detail : detailList) {
if (detail.getMsgType() == NotifyDetail.MsgType.INTERNAL) {
detail.setAttachmentList(attachmentMap.getOrDefault(detail.getId(), List.of()));
}
}
}
/// 回填通知详情的媒体附件
///
/// @param detail 通知详情
private void loadAttachments(NotifyDetail detail) {
if (detail.getMsgType() == NotifyDetail.MsgType.INTERNAL) {
detail.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.NOTIFY_DETAIL, detail.getId()));
}
}
/// 隐藏当前用户拥有的站内通知详情
///
/// @param detailId 通知详情 ID
/// @param userId 用户 ID
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
public void deleteInternalByUserId(String detailId, String userId) {
public void hideInternalByUserId(String detailId, String userId) {
TimiException.required(detailId, "未找到通知详情 ID");
TimiException.required(userId, "未找到用户 ID");
int affected = mapper.deleteSiteByIdAndUserId(detailId, userId);
int affected = mapper.hideSiteByIdAndUserId(detailId, userId);
TimiException.requiredTrue(0 < affected, "未找到站内通知");
}
@@ -8,10 +8,13 @@ import com.imyeyu.api.modules.common.service.NotifyDetailService;
import com.imyeyu.api.modules.common.service.NotifyService;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.common.vo.NotifyManagementItem;
import com.imyeyu.api.modules.user.service.UserLoginService;
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 lombok.RequiredArgsConstructor;
@@ -53,12 +56,10 @@ public class NotifyServiceImplement extends AbstractEntityService<Notify, String
try {
TimiException.required(notify, "未找到通知");
TimiException.required(notify.getDetailList(), "未找到通知详情列表");
String sendFrom = userLoginService.isLogged() ? userLoginService.getRequireLoginUserId() : null;
notify.setCreatedBy(sendFrom);
super.create(notify);
String sendFrom = null;
if (userLoginService.isLogged()) {
sendFrom = userLoginService.getRequireLoginUserId();
}
for (NotifyDetail detail : notify.getDetailList()) {
detail.setNotifyId(notify.getId());
if (TimiJava.isNotEmpty(sendFrom)) {
@@ -82,6 +83,55 @@ public class NotifyServiceImplement extends AbstractEntityService<Notify, String
}
}
/// 分页查询当前用户创建的通知
///
/// @param page 分页参数
/// @param userId 用户 ID
/// @return 管理端通知列表
@Override
public PageResult<NotifyManagementItem> pageManagement(Page<Notify> page, String userId) {
TimiException.required(userId, "未找到用户 ID");
Page<Notify> validPage = TimiJava.defaultIfNull(page, new Page<>());
PageResult<NotifyManagementItem> result = new PageResult<>();
result.setTotal(mapper.countManagement(userId));
result.setList(mapper.selectManagementPage(userId, validPage.getOffset(), validPage.getLimit()));
return result;
}
/// 查询当前用户可管理的通知
///
/// @param notifyId 通知 ID
/// @param userId 用户 ID
/// @return 通知
@Override
public Notify getManagement(String notifyId, String userId) {
TimiException.required(notifyId, "未找到通知 ID");
TimiException.required(userId, "未找到用户 ID");
Notify notify = mapper.selectManagement(notifyId, userId);
TimiException.required(notify, "未找到可管理的通知");
return notify;
}
/// 删除当前用户创建的通知
///
/// @param notifyId 通知 ID
/// @param userId 用户 ID
@Override
public void deleteManagement(String notifyId, String userId) {
getManagement(notifyId, userId);
delete(notifyId);
}
/// 取消当前用户创建的通知
///
/// @param notifyId 通知 ID
/// @param userId 用户 ID
@Override
public void cancelManagement(String notifyId, String userId) {
getManagement(notifyId, userId);
cancel(notifyId);
}
/// 删除通知并级联软删除通知详情
///
/// @param notifyId 通知 ID
@@ -1,6 +1,5 @@
package com.imyeyu.api.modules.common.service.implement;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.modules.common.config.NotifyQueueConfig;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
@@ -11,9 +10,9 @@ import com.imyeyu.api.modules.common.service.SmsQueueService;
import com.imyeyu.api.modules.common.util.QCloudSmsProxy;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.utils.Regex;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
import com.imyeyu.utils.Regex;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -56,12 +55,7 @@ public class SmsQueueServiceImplement extends AbstractQueueService<SmsQueue, Not
@Override
protected void doSend(SmsQueue queue) {
try {
String[] args = new String[0];
if (queue.getArgs() != null && !queue.getArgs().isBlank()) {
JsonNode argsNode = jackson.readTree(queue.getArgs());
TimiException.requiredTrue(argsNode != null && argsNode.isArray(), "短信参数必须是数组");
args = jackson.treeToValue(argsNode, String[].class);
}
String[] args = jackson.treeToValue(queue.getArgs(), String[].class);
SendSmsResponse response = smsProxy.send(queue.getSendTo(), queue.getTemplateId(), args);
TimiException.required(response, "未找到短信响应");
SendStatus[] statusSet = response.getSendStatusSet();
@@ -0,0 +1,85 @@
package com.imyeyu.api.modules.common.service.implement;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.imyeyu.api.modules.common.config.NotifyQueueConfig;
import com.imyeyu.api.modules.common.entity.Notify;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
import com.imyeyu.api.modules.common.service.NotifyDetailService;
import com.imyeyu.api.modules.common.service.NotifyService;
import com.imyeyu.api.modules.common.service.SmsService;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.utils.Regex;
import com.imyeyu.utils.Text;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
/**
*
*
* @author 夜雨
* @since 2026-05-14 12:12
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SmsServiceImplement implements SmsService {
@Value("${spring.profiles.active}")
private String env;
private final NotifyQueueConfig config;
private final ObjectMapper jackson;
private final NotifyService notifyService;
private final NotifyDetailService notifyDetailService;
@Override
public String captchaCreate(Notify notify) {
NotifyQueueConfig.Sms.Captcha captchaConf = config.getSms().getCaptcha();
NotifyDetail detail = notify.getDetailList().getFirst();
TimiException.requiredTrue(Regex.isMatch(Regex.MOBILE_PHONE, detail.getSendTo()), "invalid mobilePhone");
String code = Text.randomString("0123456789", 6);
if (!env.equalsIgnoreCase("prod")) {
code = "123456";
}
detail.setMsgType(NotifyDetail.MsgType.SMS);
detail.setTemplateId(captchaConf.getTemplateId());
detail.setArgs(jackson.valueToTree(List.of(code, captchaConf.getTtlMinute())));
notifyService.create(notify);
return notifyDetailService.listByNotifyId(notify.getId()).getFirst().getId();
}
@Override
public NotifyDetail captchaVerify(String detailId, String captcha) {
NotifyQueueConfig.Sms.Captcha captchaConf = config.getSms().getCaptcha();
NotifyDetail detail = notifyDetailService.get(detailId);
TimiException.required(detail, "短信验证码无效");
TimiException.required(detail.getSendAt(), "短信验证码无效");
TimiException.requiredTrue(NotifyDetail.MsgType.SMS == detail.getMsgType(), "短信验证码无效");
if (Time.M * captchaConf.getTtlMinute() < Time.now() - detail.getSendAt()) {
throw new TimiException(TimiCode.ARG_EXPIRED, "验证码过期");
}
if (detail.getArgs() instanceof ArrayNode array) {
String dbCaptcha = array.get(captchaConf.getArgIndex()).asText();
if (!dbCaptcha.equalsIgnoreCase(captcha)) {
throw new TimiException(TimiCode.RESULT_BAD, "验证码错误");
}
} else {
throw new TimiException(TimiCode.ERROR, "not array args");
}
detail.setNotify(notifyService.get(detail.getNotifyId()));
return detail;
}
}
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Tag;
import com.imyeyu.api.modules.common.entity.TagApply;
import com.imyeyu.api.modules.common.mapper.TagApplyMapper;
@@ -14,6 +15,7 @@ import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.HashSet;
@@ -104,6 +106,19 @@ public class TagServiceImplement extends AbstractEntityService<Tag, String> impl
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void deleteByOwner(Tag.OwnerType ownerType, String ownerId) {
TimiException.required(ownerType, "not found ownerType");
TimiException.required(ownerId, "not found ownerId");
Tag example = new Tag();
example.setOwnerType(ownerType);
example.setOwnerId(ownerId);
for (Tag tag : mapper.selectAllByExample(example)) {
super.delete(tag.getId());
}
}
private List<TagApply> listApply(TagApply.BizType bizType, String bizId) {
TimiException.required(bizType, "not found bizType");
TimiException.required(bizId, "not found bizId");
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.common.task;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.AttachmentVariantService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
@@ -23,6 +24,7 @@ import java.util.List;
public class AttachmentClearTask {
private final AttachmentService attachmentService;
private final AttachmentVariantService attachmentVariantService;
@Scheduled(cron = "0 0 1 * * ?")
public void run() {
@@ -31,8 +33,7 @@ public class AttachmentClearTask {
List<Attachment> needDestroyList = attachmentService.listNeedDestroy();
if (needDestroyList.isEmpty()) {
log.info("nothing attachment need clear");
return;
}
} else {
for (Attachment attach : needDestroyList) {
try {
attachmentService.destroy(attach.getId());
@@ -41,6 +42,8 @@ public class AttachmentClearTask {
log.error("clear attachment error: id[{}]", attach.getId(), e);
}
}
}
attachmentVariantService.clearExpired();
log.info("end clear expired attachments");
} catch (Exception e) {
log.error("attachment clear task error", e);
@@ -0,0 +1,57 @@
package com.imyeyu.api.modules.common.vo;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
import lombok.Data;
/// 管理端通知列表项
///
/// @author 夜雨
/// @since 2026-08-25
@Data
public class NotifyManagementItem {
/// 通知 ID
private String id;
/// 创建者用户 ID
private String createdBy;
/// 创建时间
private Long createdAt;
/// 更新时间
private Long updatedAt;
/// 通知标题
private String subject;
/// 通知内容
private String content;
/// 内容类型
private NotifyDetail.ContentType contentType;
/// 发送时间
private Long sendAt;
/// 接收用户数量
private Long recipientCount;
/// 已读用户数量
private Long readCount;
/// 等待发送数量
private Long waitingCount;
/// 已发送数量
private Long remindedCount;
/// 超时数量
private Long timeoutCount;
/// 已取消数量
private Long cancelCount;
/// 失败数量
private Long failCount;
}
@@ -0,0 +1,15 @@
package com.imyeyu.api.modules.common.vo;
import lombok.Data;
///
///
/// @author 夜雨
/// @since 2026-08-21 17:59
@Data
public class SmsCaptchaVerifyRequest {
private String detailId;
private String captcha;
}
@@ -21,6 +21,14 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
STORE_DELETE("STORE:DELETE", "门店删除", false, true),
STORE_DIARY_CREATE("STORE_DIARY:CREATE", "门店日记创建", true, false),
STORE_DIARY_READ("STORE_DIARY:READ", "门店日记读取", true, false),
STORE_DIARY_UPDATE("STORE_DIARY:UPDATE", "门店日记修改", true, false),
STORE_DIARY_DELETE("STORE_DIARY:DELETE", "门店日记删除", true, false),
CUSTOMER_CREATE("CUSTOMER:CREATE", "客户创建", false, false),
CUSTOMER_READ("CUSTOMER:READ", "客户读取", false, false),
@@ -43,12 +51,48 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
EVENT_RECORD_READ("EVENT_RECORD:READ", "登记事件记录读取", false, false),
EVENT_RECORD_VERIFY("EVENT_RECORD:VERIFY", "登记事件核销", true, false),
EVENT_RECORD_EXPORT("EVENT_RECORD:EXPORT", "登记事件记录导出", true, false),
EVENT_RECORD_UPDATE("EVENT_RECORD:UPDATE", "登记事件记录修改", false, false),
EVENT_RECORD_DELETE("EVENT_RECORD:DELETE", "登记事件记录删除", false, false),
PACKAGE_CONFIG_CREATE("PACKAGE_CONFIG:CREATE", "套餐配置创建", true, true),
PACKAGE_CONFIG_READ("PACKAGE_CONFIG:READ", "套餐配置读取", true, true),
PACKAGE_CONFIG_UPDATE("PACKAGE_CONFIG:UPDATE", "套餐配置修改", true, true),
PACKAGE_CONFIG_DELETE("PACKAGE_CONFIG:DELETE", "套餐配置删除", true, true),
CUSTOMER_PACKAGE_CREATE("CUSTOMER_PACKAGE:CREATE", "客户套餐创建", true, true),
CUSTOMER_PACKAGE_READ("CUSTOMER_PACKAGE:READ", "客户套餐读取", true, true),
CUSTOMER_PACKAGE_CANCEL("CUSTOMER_PACKAGE:CANCEL", "客户套餐取消", true, true),
CUSTOMER_PACKAGE_PRICE_OVERRIDE("CUSTOMER_PACKAGE:PRICE_OVERRIDE", "客户套餐成交价和权益配额调整", true, true),
PACKAGE_DELIVERY_CREATE("PACKAGE_DELIVERY:CREATE", "套餐发放创建", true, true),
PACKAGE_DELIVERY_READ("PACKAGE_DELIVERY:READ", "套餐发放读取", true, true),
PACKAGE_DELIVERY_VERIFY("PACKAGE_DELIVERY:VERIFY", "套餐发放核销", true, true),
PACKAGE_DELIVERY_REVOKE("PACKAGE_DELIVERY:REVOKE", "套餐发放冲正", true, true),
FUND_RECORD_READ("FUND_RECORD:READ", "资金流水读取", true, true),
FUND_RECEIVE_CREATE("FUND_RECEIVE:CREATE", "线下收款创建", true, true),
FUND_RECEIVE_VERIFY("FUND_RECEIVE:VERIFY", "线下收款核销", true, true),
FUND_REFUND("FUND:REFUND", "套餐退款", true, true),
FUND_REVOKE("FUND:REVOKE", "资金流水冲正", true, true),
POINT_ACCOUNT_READ("POINT_ACCOUNT:READ", "客户积分账户读取", true, true),
POINT_LEDGER_READ("POINT_LEDGER:READ", "客户积分流水读取", true, true),
@@ -16,7 +16,7 @@ public enum GaoRoleCode implements BuiltinRoleCode {
GLOBAL_MANAGER("门店管理", null),
/// 店长
STORE_MANAGER("店长", GLOBAL_MANAGER.name());
STORE_MANAGER("店长", null);
final String defaultName;
final String parentCode;
@@ -3,9 +3,11 @@ package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.service.GaoCustomerChartService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import lombok.RequiredArgsConstructor;
@@ -26,6 +28,7 @@ import java.util.List;
public class GaoCustomerChartController {
private final GaoCustomerChartService service;
private final GaoStoreScopeService storeScopeService;
/// 查询客户登记每日统计,可同时支撑登记趋势和登记事件结构图表
@AOPLog
@@ -33,6 +36,8 @@ public class GaoCustomerChartController {
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/event/record/daily")
public List<GaoEventRecordDailyStatView> eventRecordDaily(@RequestBody GaoCustomerChartReq req) {
TimiException.required(req, "not found req");
req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
return service.eventRecordDailyStat(req);
}
@@ -42,6 +47,8 @@ public class GaoCustomerChartController {
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/event/record/calendar")
public List<GaoEventRecordCalendarView> eventRecordCalendar(@RequestBody GaoCustomerChartReq req) {
TimiException.required(req, "not found req");
req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
return service.eventRecordCalendar(req);
}
}
@@ -1,21 +1,17 @@
package com.imyeyu.api.modules.gao.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStateReq;
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.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
@@ -50,11 +46,9 @@ import java.util.Map;
public class GaoCustomerController {
private final UserService userService;
private final RoleChecker roleChecker;
private final AttachmentService attachmentService;
private final GaoCustomerService service;
private final GaoStoreService gaoStoreService;
private final UserLoginService userLoginService;
private final GaoStoreScopeService storeScopeService;
@AOPLog
@RequestRateLimit
@@ -62,6 +56,8 @@ public class GaoCustomerController {
@PostMapping("/list")
@JsonView(ResponseView.Public.class)
public PageResult<GaoCustomer> list(@RequestBody GaoCustomerPage page) {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
PageResult<GaoCustomer> result = service.pageByQuery(page);
List<String> idList = result.getList().stream().map(GaoCustomer::getId).distinct().toList();
@@ -79,6 +75,8 @@ public class GaoCustomerController {
@JsonView(ResponseView.Public.class)
public GaoCustomer detail(@RequestParam String id) {
GaoCustomer customer = service.get(id);
TimiException.required(customer, "not found customer");
storeScopeService.checkStoreId(customer.getStoreId());
if (TimiJava.isNotEmpty(customer.getIntroducerCustomerId())) {
GaoCustomer introducerCustomer = service.get(customer.getIntroducerCustomerId());
introducerCustomer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, introducerCustomer.getId()));
@@ -97,9 +95,7 @@ public class GaoCustomerController {
@PostMapping("/create")
@JsonView(ResponseView.Public.class)
public String create(@RequestBody GaoCustomer customer) {
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
customer.setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId());
}
customer.setStoreId(storeScopeService.resolveStoreId(customer.getStoreId()));
service.create(customer);
return customer.getId();
}
@@ -109,9 +105,12 @@ public class GaoCustomerController {
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_UPDATE)
@PostMapping("/update")
public void update(@RequestBody GaoCustomer customer) {
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
customer.setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId());
}
TimiException.required(customer, "not found customer");
TimiException.required(customer.getId(), "not found customer.id");
GaoCustomer dbCustomer = service.get(customer.getId());
TimiException.required(dbCustomer, "not found customer");
storeScopeService.checkStoreId(dbCustomer.getStoreId());
customer.setStoreId(dbCustomer.getStoreId());
service.updateWithAttachment(customer);
}
@@ -120,6 +119,9 @@ public class GaoCustomerController {
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
GaoCustomer customer = service.get(id);
TimiException.required(customer, "not found customer");
storeScopeService.checkStoreId(customer.getStoreId());
service.delete(id);
}
@@ -131,6 +133,7 @@ public class GaoCustomerController {
public GaoCustomer findById(@RequestParam String id) {
GaoCustomer customer = service.get(id);
TimiException.required(customer, "not found customer");
storeScopeService.checkStoreId(customer.getStoreId());
customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId()));
return customer;
}
@@ -141,7 +144,7 @@ public class GaoCustomerController {
@PostMapping("/find/code")
@JsonView(ResponseView.Public.class)
public GaoCustomer findByCode(@RequestParam String storeId, @RequestParam String code) {
GaoCustomer customer = service.getByCode(storeId, code);
GaoCustomer customer = service.getByCode(storeScopeService.resolveStoreId(storeId), code);
if (customer == null) {
return null;
}
@@ -155,7 +158,7 @@ public class GaoCustomerController {
@PostMapping("/find/name")
@JsonView(ResponseView.Public.class)
public GaoCustomer findByName(@RequestParam String storeId, @RequestParam String name) {
GaoCustomer customer = service.getByName(storeId, name);
GaoCustomer customer = service.getByName(storeScopeService.resolveStoreId(storeId), name);
if (customer == null) {
return null;
}
@@ -169,6 +172,12 @@ public class GaoCustomerController {
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_EXPORT)
@PostMapping("/export")
public void export(@RequestBody Page<GaoCustomer> page) throws IOException {
TimiException.required(page, "not found page");
if (!storeScopeService.hasGlobalScope()) {
GaoCustomer example = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoCustomer());
example.setStoreId(storeScopeService.resolveStoreId(example.getStoreId()));
page.setEqualsExample(example);
}
HttpServletResponse resp = TimiSpring.getResponse();
String contentDisposition = Network.getFileDownloadHeader("客户列表-%s.xlsx".formatted(Time.serialize.format(Time.now())));
resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
@@ -182,6 +191,9 @@ public class GaoCustomerController {
@PostMapping("/invited/list")
@JsonView(ResponseView.Public.class)
public List<GaoCustomer> listInvited(@RequestParam String introducerCustomerId) {
GaoCustomer introducerCustomer = service.get(introducerCustomerId);
TimiException.required(introducerCustomer, "not found introducerCustomer");
storeScopeService.checkStoreId(introducerCustomer.getStoreId());
List<GaoCustomer> result = service.listInvited(introducerCustomerId);
List<String> idList = result.stream().map(GaoCustomer::getId).distinct().toList();
Map<String, List<Attachment>> attachMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, idList);
@@ -196,6 +208,9 @@ public class GaoCustomerController {
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_UPDATE)
@PostMapping("/invite/save")
public void saveInvite(@RequestParam String introducerCustomerId, @RequestParam String customerId) {
GaoCustomer customer = service.get(customerId);
TimiException.required(customer, "not found customer");
storeScopeService.checkStoreId(customer.getStoreId());
service.saveInviteRelation(introducerCustomerId, customerId);
}
@@ -204,6 +219,9 @@ public class GaoCustomerController {
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_UPDATE)
@PostMapping("/invite/delete")
public void deleteInvite(@RequestParam String customerId) {
GaoCustomer customer = service.get(customerId);
TimiException.required(customer, "not found customer");
storeScopeService.checkStoreId(customer.getStoreId());
service.deleteInviteRelation(customerId);
}
@@ -213,6 +231,8 @@ public class GaoCustomerController {
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/state/daily")
public List<GaoCustomerDailyStateView> dailyStat(@RequestBody GaoCustomerTrendStateReq req) {
TimiException.required(req, "not found req");
req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
return service.stateDailyTrend(req);
}
@@ -222,6 +242,6 @@ public class GaoCustomerController {
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/state/gender")
public List<GaoCustomerGenderStatView> genderStat(@RequestParam String storeId) {
return service.stateGender(storeId);
return service.stateGender(storeScopeService.resolveStoreId(storeId));
}
}
@@ -0,0 +1,274 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery;
import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem;
import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundConfirmNoRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundReceiveRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundRefundRequest;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
/// GAO 客户套餐和收退款接口
///
/// @author Codex
/// @since 2026-08-31
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/customer/package")
public class GaoCustomerPackageController {
private final GaoCustomerPackageService service;
private final GaoCustomerService customerService;
private final GaoEventRecordService eventRecordService;
private final GaoPackageDeliveryService deliveryService;
private final GaoFundRecordService fundRecordService;
private final AttachmentService attachmentService;
private final GaoStoreScopeService storeScopeService;
private final UserService userService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_PACKAGE_READ)
@PostMapping("/list")
public PageResult<GaoCustomerPackage> list(@RequestBody GaoCustomerPackagePage page) {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
PageResult<GaoCustomerPackage> result = service.pageByQuery(page);
if (result.getList().isEmpty()) {
return result;
}
List<String> customerIdList = result.getList().stream()
.map(GaoCustomerPackage::getCustomerId)
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, GaoCustomer> customerMap = customerService.mapByIdList(customerIdList);
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList, GaoCustomer.AttachType.PHOTO.name());
for (GaoCustomerPackage packageEntity : result.getList()) {
GaoCustomer customer = customerMap.get(packageEntity.getCustomerId());
if (customer != null) {
customer.setAttachmentList(attachmentMap.get(customer.getId()));
}
packageEntity.setCustomer(customer);
}
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_PACKAGE_READ)
@PostMapping("/detail")
public GaoCustomerPackage detail(@RequestParam String id) {
GaoCustomerPackage result = service.get(id);
TimiException.required(result, "not found customer package");
storeScopeService.checkStoreId(result.getStoreId());
fillTransient(List.of(result));
GaoCustomer customer = customerService.get(result.getCustomerId());
if (customer != null) {
customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId()));
}
result.setCustomer(customer);
GaoEventRecord purchaseRecord = eventRecordService.get(result.getPurchaseEventRecordId());
if (purchaseRecord != null) {
purchaseRecord.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_EVENT_RECORD, purchaseRecord.getId()));
}
result.setPurchaseEventRecord(purchaseRecord);
result.setDeliveryList(deliveryService.listByCustomerPackageId(result.getId()));
fillDeliveryTransient(result.getDeliveryList());
result.setFundRecordList(fundRecordService.listByCustomerPackageId(result.getId()));
fillFundTransient(result.getFundRecordList());
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_PACKAGE_CREATE)
@PostMapping("/purchase")
public GaoCustomerPackage purchase(@RequestBody GaoCustomerPackagePurchaseRequest request) {
GaoCustomerPackage result = service.purchase(request);
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_PACKAGE_CANCEL)
@PostMapping("/cancel")
public GaoCustomerPackage cancel(@RequestBody GaoCustomerPackageCancelRequest request) {
GaoCustomerPackage result = service.cancel(request);
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE)
@PostMapping("/payment/receive")
public GaoCustomerPackage receive(@RequestBody GaoFundReceiveRequest request) {
fundRecordService.receive(request);
return packageResponse(request.getCustomerPackageId());
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY)
@PostMapping("/payment/verify")
public GaoCustomerPackage verifyPayment(@RequestParam String id) {
fundRecordService.verify(id);
return packageAfterFundAction(id);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY)
@PostMapping("/payment/reject")
public GaoCustomerPackage rejectPayment(@RequestBody GaoFundActionRequest request) {
fundRecordService.reject(request);
return packageAfterFundAction(request.getFundRecordId());
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE)
@PostMapping("/payment/cancel")
public GaoCustomerPackage cancelPayment(@RequestBody GaoFundActionRequest request) {
fundRecordService.cancel(request);
return packageAfterFundAction(request.getFundRecordId());
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_REFUND)
@PostMapping("/refund")
public GaoCustomerPackage refund(@RequestBody GaoFundRefundRequest request) {
fundRecordService.refund(request);
return packageResponse(request.getCustomerPackageId());
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_REFUND)
@PostMapping("/refund/confirm-none")
public GaoCustomerPackage confirmNoRefund(@RequestBody GaoFundConfirmNoRefundRequest request) {
fundRecordService.confirmNoRefund(request);
return packageResponse(request.getCustomerPackageId());
}
private GaoCustomerPackage packageAfterFundAction(String fundRecordId) {
var record = fundRecordService.get(fundRecordId);
TimiException.required(record, "not found fund record");
return packageResponse(record.getBizId());
}
private GaoCustomerPackage packageResponse(String packageId) {
GaoCustomerPackage result = service.get(packageId);
TimiException.required(result, "not found customer package");
fillTransient(List.of(result));
return result;
}
private void fillTransient(List<GaoCustomerPackage> packageList) {
if (packageList == null || packageList.isEmpty()) {
return;
}
Map<String, List<GaoCustomerPackageItem>> itemMap = service.mapItemListByCustomerPackageIdList(packageList.stream().map(GaoCustomerPackage::getId).toList());
List<String> userIdList = packageList.stream()
.flatMap(packageEntity -> Stream.of(packageEntity.getOperatorUserId(), packageEntity.getCancelOperatorUserId()))
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, User> userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
for (GaoCustomerPackage packageEntity : packageList) {
List<GaoCustomerPackageItem> itemList = itemMap.getOrDefault(packageEntity.getId(), List.of());
for (GaoCustomerPackageItem item : itemList) {
fillDerived(item);
}
packageEntity.setItemList(itemList);
packageEntity.setOperator(userMap.get(packageEntity.getOperatorUserId()));
packageEntity.setCancelOperator(userMap.get(packageEntity.getCancelOperatorUserId()));
}
}
private void fillDeliveryTransient(List<GaoPackageDelivery> deliveryList) {
if (deliveryList == null || deliveryList.isEmpty()) {
return;
}
List<String> idList = deliveryList.stream().map(GaoPackageDelivery::getId).toList();
Map<String, List<GaoPackageDeliveryItem>> itemMap = deliveryService.mapItemListByDeliveryIdList(idList);
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PACKAGE_DELIVERY, idList);
List<String> userIdList = deliveryList.stream()
.flatMap(delivery -> Stream.of(delivery.getOperatorUserId(), delivery.getVerifierUserId()))
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, User> userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
for (GaoPackageDelivery delivery : deliveryList) {
delivery.setItemList(itemMap.getOrDefault(delivery.getId(), List.of()));
delivery.setAttachmentList(attachmentMap.getOrDefault(delivery.getId(), List.of()));
delivery.setOperator(userMap.get(delivery.getOperatorUserId()));
delivery.setVerifier(userMap.get(delivery.getVerifierUserId()));
}
}
private void fillFundTransient(List<GaoFundRecord> recordList) {
if (recordList == null || recordList.isEmpty()) {
return;
}
List<String> idList = recordList.stream().map(GaoFundRecord::getId).toList();
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_FUND_RECORD, idList);
List<String> userIdList = recordList.stream()
.flatMap(record -> Stream.of(record.getOperatorUserId(), record.getVerifierUserId()))
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, User> userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
for (GaoFundRecord record : recordList) {
record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of()));
record.setOperator(userMap.get(record.getOperatorUserId()));
record.setVerifier(userMap.get(record.getVerifierUserId()));
}
}
private void fillDerived(GaoCustomerPackageItem item) {
BigDecimal reserved = item.getReservedQuantity() == null ? BigDecimal.ZERO : item.getReservedQuantity();
BigDecimal delivered = item.getDeliveredQuantity() == null ? BigDecimal.ZERO : item.getDeliveredQuantity();
BigDecimal total = item.getTotalQuantity() == null ? BigDecimal.ZERO : item.getTotalQuantity();
item.setPendingQuantity(reserved);
item.setAvailableQuantity(total.subtract(reserved).subtract(delivered));
item.setRemainingQuantity(total.subtract(delivered));
}
}
@@ -1,14 +1,16 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
import com.imyeyu.api.modules.gao.service.GaoEventService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
@@ -33,28 +35,76 @@ import java.util.Map;
public class GaoEventController {
private final GaoEventService service;
private final RoleChecker roleChecker;
private final GaoStoreService gaoStoreService;
private final UserLoginService userLoginService;
private final GaoPackageConfigService packageConfigService;
private final GaoStoreScopeService storeScopeService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.EVENT_READ)
@PostMapping("/list")
public PageResult<GaoEvent> list(@RequestBody Page<GaoEvent> page) {
PageResult<GaoEvent> result = service.page(page);
Map<String, List<String>> roleMap = service.mapRoleCodeListByEventIdList(result.getList().stream().map(GaoEvent::getId).toList());
for (GaoEvent event : result.getList()) {
event.setRoleCodeList(roleMap.get(event.getId()));
}
return result;
TimiException.required(page, "not found page");
resolveStoreScope(page);
PageResult<GaoEvent> result = service.pageWithoutDeleted(page);
return attachEventRelations(result);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.EVENT_CREATE)
@PostMapping("/list/history")
public PageResult<GaoEvent> listHistory(@RequestBody Page<GaoEvent> page) {
TimiException.required(page, "not found page");
resolveStoreScope(page);
return attachEventRelations(service.page(page));
}
private void resolveStoreScope(Page<GaoEvent> page) {
if (!storeScopeService.hasGlobalScope()) {
GaoEvent example = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoEvent());
example.setStoreId(storeScopeService.resolveStoreId(example.getStoreId()));
page.setEqualsExample(example);
}
}
private PageResult<GaoEvent> attachEventRelations(PageResult<GaoEvent> result) {
Map<String, List<String>> roleMap = service.mapRoleCodeListByEventIdList(result.getList().stream().map(GaoEvent::getId).toList());
for (GaoEvent event : result.getList()) {
event.setRoleCodeList(roleMap.get(event.getId()));
}
service.attachOptionGroupList(result.getList());
attachPackageConfig(result.getList());
return result;
}
private void attachPackageConfig(List<GaoEvent> eventList) {
List<String> packageConfigIdList = eventList.stream()
.map(GaoEvent::getPackageConfigId)
.filter(TimiJava::isNotEmpty)
.distinct()
.toList();
if (packageConfigIdList.isEmpty()) {
return;
}
Map<String, GaoPackageConfig> packageConfigMap = packageConfigService.mapByIdList(packageConfigIdList);
Map<String, List<GaoPackageConfigItem>> itemMap = packageConfigService.mapItemListByPackageConfigIdList(packageConfigIdList);
for (GaoEvent event : eventList) {
GaoPackageConfig packageConfig = packageConfigMap.get(event.getPackageConfigId());
if (packageConfig != null) {
packageConfig.setItemList(itemMap.getOrDefault(packageConfig.getId(), List.of()));
event.setPackageConfig(packageConfig);
}
}
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.EVENT_READ)
@PostMapping("/list/valid")
public List<GaoEvent> listValid(@RequestParam String storeId) {
return service.listValid(storeId);
List<GaoEvent> result = service.listValid(storeScopeService.resolveStoreId(storeId));
attachPackageConfig(result);
return result;
}
@AOPLog
@@ -63,7 +113,11 @@ public class GaoEventController {
@PostMapping("/detail")
public GaoEvent detail(@RequestParam String id) {
GaoEvent event = service.get(id);
TimiException.required(event, "not found event");
storeScopeService.checkStoreId(event.getStoreId());
event.setRoleCodeList(service.listRoleCode(event.getId()));
service.attachOptionGroupList(List.of(event));
attachPackageConfig(List.of(event));
return event;
}
@@ -72,9 +126,7 @@ public class GaoEventController {
@RequireGaoPermission(GaoPermissionCode.EVENT_CREATE)
@PostMapping("/create")
public void create(@RequestBody GaoEvent event) {
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
event.setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId());
}
event.setStoreId(storeScopeService.resolveStoreId(event.getStoreId()));
service.create(event);
}
@@ -83,17 +135,36 @@ public class GaoEventController {
@RequireGaoPermission(GaoPermissionCode.EVENT_UPDATE)
@PostMapping("/update")
public void update(@RequestBody GaoEvent event) {
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
event.setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId());
}
TimiException.required(event, "not found event");
TimiException.required(event.getId(), "not found event.id");
GaoEvent dbEvent = service.get(event.getId());
TimiException.required(dbEvent, "not found event");
storeScopeService.checkStoreId(dbEvent.getStoreId());
event.setStoreId(dbEvent.getStoreId());
service.update(event);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.EVENT_UPDATE)
@PostMapping("/options/update")
public void updateOptions(@RequestParam String id, @RequestBody List<GaoEventOptionGroup> optionGroupList) {
GaoEvent event = service.get(id);
TimiException.required(event, "not found event");
storeScopeService.checkStoreId(event.getStoreId());
service.updateOptionGroupList(id, optionGroupList);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.EVENT_UPDATE)
@PostMapping("/sort")
public void sort(@RequestBody List<String> idList) {
for (String id : TimiJava.safeIterable(idList)) {
GaoEvent event = service.get(id);
TimiException.required(event, "not found event");
storeScopeService.checkStoreId(event.getStoreId());
}
service.sort(idList);
}
@@ -102,6 +173,9 @@ public class GaoEventController {
@RequireGaoPermission(GaoPermissionCode.EVENT_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
GaoEvent event = service.get(id);
TimiException.required(event, "not found event");
storeScopeService.checkStoreId(event.getStoreId());
service.delete(id);
}
}
@@ -1,27 +1,25 @@
package com.imyeyu.api.modules.gao.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoEventService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView;
import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordPageResult;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage;
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.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.Network;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog;
@@ -52,13 +50,11 @@ import java.util.Map;
public class GaoEventRecordController {
private final UserService userService;
private final RoleChecker roleChecker;
private final GaoEventService eventService;
private final AttachmentService attachmentService;
private final GaoCustomerService customerService;
private final GaoStoreService gaoStoreService;
private final GaoStoreScopeService storeScopeService;
private final GaoEventRecordService service;
private final UserLoginService userLoginService;
@AOPLog
@RequestRateLimit
@@ -66,6 +62,8 @@ public class GaoEventRecordController {
@PostMapping("/list")
@JsonView(ResponseView.Public.class)
public GaoEventRecordPageResult list(@RequestBody GaoEventRecordPage page) {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
PageResult<GaoEventRecord> pageResult = service.pageByRange(page);
GaoEventRecordPageResult result = new GaoEventRecordPageResult();
result.setTotal(pageResult.getTotal());
@@ -81,6 +79,7 @@ public class GaoEventRecordController {
record.setCustomer(customerMap.get(record.getCustomerId()));
record.getCustomer().setAttachmentList(attachMap.get(record.getCustomerId()));
}
service.attachSelectedOptionGroupList(result.getList());
}
return result;
}
@@ -108,22 +107,39 @@ public class GaoEventRecordController {
@JsonView(ResponseView.Public.class)
public GaoEventRecord detail(@RequestParam String id) {
GaoEventRecord record = service.get(id);
TimiException.required(record, "not found event record");
storeScopeService.checkStoreId(record.getStoreId());
record.setCustomer(customerService.get(record.getCustomerId()));
record.getCustomer().setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, record.getCustomerId()));
record.setEvent(eventService.get(record.getEventId()));
eventService.attachOptionGroupList(List.of(record.getEvent()));
record.setOperator(userService.get(record.getOperatorUserId()));
record.setUpdater(userService.get(record.getUpdatedBy()));
if (record.getVerifierUserId() != null) {
record.setVerifier(userService.get(record.getVerifierUserId()));
}
record.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_EVENT_RECORD, record.getId()));
service.attachSelectedOptionGroupList(List.of(record));
return record;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_VERIFY)
@PostMapping("/verify")
public void verify(@RequestParam String id) {
GaoEventRecord record = service.get(id);
TimiException.required(record, "not found event record");
storeScopeService.checkStoreId(record.getStoreId());
service.verify(id);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_CREATE)
@PostMapping("/create")
public void create(@RequestBody GaoEventRecord record) {
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
record.setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId());
}
resolveCreateRecordStore(record);
service.create(record);
}
@@ -132,11 +148,8 @@ public class GaoEventRecordController {
@RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_CREATE)
@PostMapping("/create/batch")
public void createBatch(@RequestBody List<GaoEventRecord> recordList) {
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
String storeId = gaoStoreService.getBelongIdByRequiredLoginUserId();
for (GaoEventRecord record : recordList) {
record.setStoreId(storeId);
}
for (GaoEventRecord record : TimiJava.safeIterable(recordList)) {
resolveCreateRecordStore(record);
}
service.createBatch(recordList);
}
@@ -146,11 +159,17 @@ public class GaoEventRecordController {
@RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_CREATE)
@PostMapping("/create/customer")
public void createWithCustomer(@RequestBody GaoCustomer customer) {
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
String storeId = gaoStoreService.getBelongIdByRequiredLoginUserId();
for (GaoEventRecord record : TimiJava.safeIterable(customer.getEventRecordList())) {
record.setStoreId(storeId);
TimiException.required(customer, "not found customer");
if (TimiJava.isNotEmpty(customer.getId())) {
GaoCustomer dbCustomer = customerService.get(customer.getId());
TimiException.required(dbCustomer, "not found customer");
storeScopeService.checkStoreId(dbCustomer.getStoreId());
customer.setStoreId(dbCustomer.getStoreId());
} else {
customer.setStoreId(storeScopeService.resolveStoreId(customer.getStoreId()));
}
for (GaoEventRecord record : TimiJava.safeIterable(customer.getEventRecordList())) {
resolveCreateRecordStore(record);
}
service.createWithCustomer(customer);
}
@@ -160,9 +179,12 @@ public class GaoEventRecordController {
@RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_UPDATE)
@PostMapping("/update")
public void update(@RequestBody GaoEventRecord record) {
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
record.setStoreId(gaoStoreService.getBelongIdByRequiredLoginUserId());
}
TimiException.required(record, "not found event record");
TimiException.required(record.getId(), "not found event record.id");
GaoEventRecord dbRecord = service.get(record.getId());
TimiException.required(dbRecord, "not found event record");
storeScopeService.checkStoreId(dbRecord.getStoreId());
record.setStoreId(dbRecord.getStoreId());
service.update(record);
}
@@ -171,6 +193,9 @@ public class GaoEventRecordController {
@RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
GaoEventRecord record = service.get(id);
TimiException.required(record, "not found event record");
storeScopeService.checkStoreId(record.getStoreId());
service.delete(id);
}
@@ -180,6 +205,8 @@ public class GaoEventRecordController {
@RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_EXPORT)
@PostMapping("/export")
public void export(@RequestBody GaoEventRecordPage page) throws IOException {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
HttpServletResponse resp = TimiSpring.getResponse();
String contentDisposition = Network.getFileDownloadHeader("登记事件记录-%s.xlsx".formatted(Time.serialize.format(Time.now())));
resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
@@ -193,6 +220,8 @@ public class GaoEventRecordController {
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/rank")
public PageResult<GaoCustomerEventRankView> eventRank(@RequestBody GaoEventRecordRankPage page) {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
PageResult<GaoCustomerEventRankView> result = service.pageRankByRange(page);
List<String> customerIdList = result.getList().stream().map(GaoCustomerEventRankView::getCustomerId).distinct().toList();
if (customerIdList.isEmpty()) {
@@ -209,4 +238,22 @@ public class GaoEventRecordController {
}
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/option/stat")
public List<GaoEventOptionStatView> optionStat(@RequestBody GaoEventRecordPage page) {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return service.optionStatByRange(page);
}
private void resolveCreateRecordStore(GaoEventRecord record) {
TimiException.required(record, "not found event record");
GaoEvent event = eventService.get(record.getEventId());
TimiException.required(event, "not found event");
storeScopeService.checkStoreId(event.getStoreId());
record.setStoreId(event.getStoreId());
}
}
@@ -0,0 +1,95 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
/// GAO 资金流水接口
///
/// @author Codex
/// @since 2026-08-31
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/fund/record")
public class GaoFundRecordController {
private final GaoFundRecordService service;
private final GaoStoreScopeService storeScopeService;
private final AttachmentService attachmentService;
private final UserService userService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECORD_READ)
@PostMapping("/list")
public PageResult<GaoFundRecord> list(@RequestBody GaoFundRecordPage page) {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
PageResult<GaoFundRecord> result = service.pageByQuery(page);
fillTransient(result.getList());
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECORD_READ)
@PostMapping("/detail")
public GaoFundRecord detail(@RequestParam String id) {
GaoFundRecord result = service.get(id);
TimiException.required(result, "not found fund record");
storeScopeService.checkStoreId(result.getStoreId());
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_REVOKE)
@PostMapping("/revoke")
public GaoFundRecord revoke(@RequestBody GaoFundRevokeRequest request) {
GaoFundRecord result = service.revoke(request);
fillTransient(List.of(result));
return result;
}
private void fillTransient(List<GaoFundRecord> recordList) {
if (recordList == null || recordList.isEmpty()) {
return;
}
List<String> idList = recordList.stream().map(GaoFundRecord::getId).toList();
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_FUND_RECORD, idList);
List<String> userIdList = recordList.stream()
.flatMap(record -> Stream.of(record.getOperatorUserId(), record.getVerifierUserId()))
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, User> userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
for (GaoFundRecord record : recordList) {
record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of()));
record.setOperator(userMap.get(record.getOperatorUserId()));
record.setVerifier(userMap.get(record.getVerifierUserId()));
}
}
}
@@ -0,0 +1,158 @@
package com.imyeyu.api.modules.gao.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.bean.PermissionMatchMode;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Notify;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.NotifyService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoUser;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.service.GaoUserService;
import com.imyeyu.api.modules.user.entity.User;
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.TimiException;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/// GAO 通知接口
///
/// 这里的通知接收用户必须属于当前操作人的 GAO 用户范围,当前登录用户本人除外。
///
/// @author Codex
/// @since 2026-08-25
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/notify")
public class GaoNotifyController {
private final GaoUserService gaoUserService;
private final GaoStoreScopeService storeScopeService;
private final NotifyService notifyService;
private final UserLoginService userLoginService;
private final UserService userService;
private final AttachmentService attachmentService;
/// 查询可作为通知接收人的 GAO 用户
///
/// 门店管理范围可以查询全部门店或指定门店,店长只能查询当前门店。
///
/// @param page 分页参数
/// @return 可发送用户分页结果
@JsonView(ResponseView.Public.class)
@RequiredToken
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.NOTIFY_CREATE)
@RequireGaoPermission(value = {GaoPermissionCode.USER_READ, GaoPermissionCode.STORE_READ}, mode = PermissionMatchMode.ANY)
@PostMapping("/recipient/list")
public PageResult<GaoUser> recipientList(@RequestBody Page<GaoUser> page) {
TimiException.required(page, "未找到分页参数");
GaoUser example = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoUser());
if (storeScopeService.hasGlobalScope()) {
if (TimiJava.isNotEmpty(example.getStoreId())) {
storeScopeService.checkStoreId(example.getStoreId());
}
} else {
example.setStoreId(storeScopeService.resolveStoreId(example.getStoreId()));
}
page.setEqualsExample(example);
PageResult<GaoUser> result = gaoUserService.pageRecipients(page);
fillUser(result.getList());
return result;
}
/// 创建 GAO 站内通知
///
/// 只允许发送站内通知,并逐个校验接收用户是否属于当前操作人的门店范围;当前用户可以接收自己发送的通知。
///
/// @param notify 通知及通知详情
/// @return 创建后的通知
@RequiredToken
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.NOTIFY_CREATE)
@RequireGaoPermission(value = {GaoPermissionCode.USER_READ, GaoPermissionCode.STORE_READ}, mode = PermissionMatchMode.ANY)
@PostMapping("/create")
public Notify create(@RequestBody Notify notify) {
checkRecipientBoundary(notify);
notifyService.create(notify);
return notify;
}
/// 校验通知接收用户边界
///
/// @param notify 通知
private void checkRecipientBoundary(Notify notify) {
TimiException.required(notify, "未找到通知");
List<NotifyDetail> detailList = notify.getDetailList();
TimiException.required(detailList, "未找到通知详情列表");
TimiException.requiredTrue(!detailList.isEmpty(), "通知详情列表不能为空");
Set<String> recipientIdSet = detailList.stream().map(detail -> {
TimiException.required(detail, "未找到通知详情");
TimiException.requiredTrue(detail.getMsgType() == NotifyDetail.MsgType.INTERNAL, "GAO 通知仅支持站内通知");
TimiException.required(detail.getSendTo(), "未找到通知接收用户 ID");
if (detail.getSendAt() != null) {
TimiException.requiredTrue(Time.now() < detail.getSendAt(), "定时发送时间必须晚于当前时间");
}
return detail.getSendTo();
}).collect(Collectors.toSet());
TimiException.requiredTrue(recipientIdSet.size() == detailList.size(), "通知接收用户不能重复");
String loginUserId = userLoginService.getRequireLoginUserId();
Set<String> gaoRecipientIdSet = recipientIdSet.stream()
.filter(recipientId -> !loginUserId.equals(recipientId))
.collect(Collectors.toSet());
Map<String, GaoUser> gaoUserMap = gaoUserService.mapByUserIdList(gaoRecipientIdSet);
TimiException.requiredTrue(gaoUserMap.size() == gaoRecipientIdSet.size(), "包含无效的通知接收用户");
for (GaoUser gaoUser : gaoUserMap.values()) {
TimiException.requiredTrue(Boolean.TRUE.equals(gaoUser.getEnabled()), "通知接收用户已停用");
storeScopeService.checkStoreId(gaoUser.getStoreId());
}
}
/// 回填 GAO 用户的核心用户资料
///
/// @param gaoUserList GAO 用户列表
private void fillUser(List<GaoUser> gaoUserList) {
if (TimiJava.isEmpty(gaoUserList)) {
return;
}
List<String> userIdList = gaoUserList.stream().map(GaoUser::getUserId).toList();
Map<String, User> userMap = userService.mapByIdList(userIdList);
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(
Attachment.BizType.USER,
userIdList,
User.AttachType.AVATAR.toString()
);
for (GaoUser gaoUser : gaoUserList) {
User user = userMap.get(gaoUser.getUserId());
if (user == null) {
continue;
}
user.setAttachmentList(attachmentMap.getOrDefault(user.getId(), List.of()));
gaoUser.setUser(user);
}
}
}
@@ -0,0 +1,121 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/// GAO 套餐配置接口
///
/// @author Codex
/// @since 2026-08-31
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/package/config")
public class GaoPackageConfigController {
private final GaoPackageConfigService service;
private final GaoStoreScopeService storeScopeService;
private final UserService userService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_READ)
@PostMapping("/list")
public PageResult<GaoPackageConfig> list(@RequestBody GaoPackageConfigPage page) {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
PageResult<GaoPackageConfig> result = service.pageByQuery(page);
fillTransient(result.getList());
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_READ)
@PostMapping("/list/valid")
public List<GaoPackageConfig> listValid(@RequestParam String storeId) {
List<GaoPackageConfig> result = service.listValid(storeScopeService.resolveStoreId(storeId));
fillTransient(result);
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_READ)
@PostMapping("/detail")
public GaoPackageConfig detail(@RequestParam String id) {
GaoPackageConfig config = service.get(id);
TimiException.required(config, "not found package config");
storeScopeService.checkStoreId(config.getStoreId());
fillTransient(List.of(config));
return config;
}
private void fillTransient(List<GaoPackageConfig> configList) {
if (configList == null || configList.isEmpty()) {
return;
}
Map<String, List<GaoPackageConfigItem>> itemMap = service.mapItemListByPackageConfigIdList(configList.stream().map(GaoPackageConfig::getId).toList());
List<String> updaterIdList = configList.stream().map(GaoPackageConfig::getUpdatedBy).filter(value -> value != null && !value.isBlank()).distinct().toList();
Map<String, User> updaterMap = updaterIdList.isEmpty() ? Map.of() : userService.mapByIdList(updaterIdList);
for (GaoPackageConfig config : configList) {
config.setItemList(itemMap.getOrDefault(config.getId(), List.of()));
config.setUpdater(updaterMap.get(config.getUpdatedBy()));
}
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_CREATE)
@PostMapping("/create")
public String create(@RequestBody GaoPackageConfig config) {
TimiException.required(config, "not found package config");
config.setStoreId(storeScopeService.resolveStoreId(config.getStoreId()));
service.create(config);
return config.getId();
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_UPDATE)
@PostMapping("/update")
public void update(@RequestBody GaoPackageConfig config) {
TimiException.required(config, "not found package config");
TimiException.required(config.getId(), "not found packageConfig.id");
GaoPackageConfig dbConfig = service.get(config.getId());
TimiException.required(dbConfig, "not found package config");
storeScopeService.checkStoreId(dbConfig.getStoreId());
config.setStoreId(dbConfig.getStoreId());
service.update(config);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_CONFIG_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
GaoPackageConfig config = service.get(id);
TimiException.required(config, "not found package config");
storeScopeService.checkStoreId(config.getStoreId());
service.delete(id);
}
}
@@ -0,0 +1,140 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery;
import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem;
import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryCreateRequest;
import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage;
import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryRevokeRequest;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
/// GAO 套餐发放接口
///
/// @author Codex
/// @since 2026-08-31
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/package/delivery")
public class GaoPackageDeliveryController {
private final GaoPackageDeliveryService service;
private final GaoStoreScopeService storeScopeService;
private final AttachmentService attachmentService;
private final UserService userService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_READ)
@PostMapping("/list")
public PageResult<GaoPackageDelivery> list(@RequestBody GaoPackageDeliveryPage page) {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
PageResult<GaoPackageDelivery> result = service.pageByQuery(page);
fillTransient(result.getList());
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_READ)
@PostMapping("/detail")
public GaoPackageDelivery detail(@RequestParam String id) {
GaoPackageDelivery result = service.get(id);
TimiException.required(result, "not found delivery");
storeScopeService.checkStoreId(result.getStoreId());
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_CREATE)
@PostMapping("/create")
public GaoPackageDelivery create(@RequestBody GaoPackageDeliveryCreateRequest request) {
GaoPackageDelivery result = service.createDelivery(request);
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_VERIFY)
@PostMapping("/verify")
public GaoPackageDelivery verify(@RequestParam String id) {
GaoPackageDelivery result = service.verify(id);
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_VERIFY)
@PostMapping("/reject")
public GaoPackageDelivery reject(@RequestBody GaoPackageDeliveryActionRequest request) {
GaoPackageDelivery result = service.reject(request);
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_CREATE)
@PostMapping("/cancel")
public GaoPackageDelivery cancel(@RequestBody GaoPackageDeliveryActionRequest request) {
GaoPackageDelivery result = service.cancel(request);
fillTransient(List.of(result));
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_REVOKE)
@PostMapping("/revoke")
public GaoPackageDelivery revoke(@RequestBody GaoPackageDeliveryRevokeRequest request) {
GaoPackageDelivery result = service.revoke(request);
fillTransient(List.of(result));
return result;
}
private void fillTransient(List<GaoPackageDelivery> deliveryList) {
if (deliveryList == null || deliveryList.isEmpty()) {
return;
}
List<String> idList = deliveryList.stream().map(GaoPackageDelivery::getId).toList();
Map<String, List<GaoPackageDeliveryItem>> itemMap = service.mapItemListByDeliveryIdList(idList);
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PACKAGE_DELIVERY, idList);
List<String> userIdList = deliveryList.stream()
.flatMap(delivery -> Stream.of(delivery.getOperatorUserId(), delivery.getVerifierUserId()))
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, User> userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
for (GaoPackageDelivery delivery : deliveryList) {
delivery.setItemList(itemMap.getOrDefault(delivery.getId(), List.of()));
delivery.setAttachmentList(attachmentMap.getOrDefault(delivery.getId(), List.of()));
delivery.setOperator(userMap.get(delivery.getOperatorUserId()));
delivery.setVerifier(userMap.get(delivery.getVerifierUserId()));
}
}
}
@@ -1,17 +1,18 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoPointAccountPage;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -25,23 +26,33 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/gao/customer/point")
public class GaoPointAccountController {
private final RoleChecker roleChecker;
private final GaoStoreService storeService;
private final GaoStoreScopeService storeScopeService;
private final GaoPointAccountService accountService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_ACCOUNT_READ)
@PostMapping("/account/detail")
@PostMapping("/account/list")
public PageResult<GaoPointAccount> list(@RequestBody GaoPointAccountPage page) {
TimiException.required(page, "not found page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return accountService.pageByQuery(page);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_ACCOUNT_READ)
@PostMapping("/account/detail/customer")
public GaoPointAccount detail(@RequestParam String customerId, @RequestParam(required = false) String storeId) {
storeId = resolveStoreId(storeId);
storeId = storeScopeService.resolveStoreId(storeId);
return accountService.getByCustomerId(storeId, customerId);
}
private String resolveStoreId(String requestedStoreId) {
if (roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
return requestedStoreId;
}
return storeService.getBelongIdByRequiredLoginUserId();
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_ACCOUNT_READ)
@PostMapping("/account/detail")
public GaoPointAccount detailById(@RequestParam String id, @RequestParam(required = false) String storeId) {
return accountService.getByIdAndStoreId(storeScopeService.resolveStoreId(storeId), id);
}
}
@@ -1,16 +1,13 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
import com.imyeyu.api.modules.gao.service.GaoPointLedgerService;
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
@@ -30,8 +27,7 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/gao/customer/point")
public class GaoPointLedgerController {
private final RoleChecker roleChecker;
private final GaoStoreService storeService;
private final GaoStoreScopeService storeScopeService;
private final GaoPointLedgerService ledgerService;
private final GaoPointTriggerService triggerService;
@@ -41,7 +37,7 @@ public class GaoPointLedgerController {
@PostMapping("/ledger/list")
public PageResult<GaoPointLedger> list(@RequestBody GaoPointLedgerPage page) {
TimiException.required(page, "not found page");
page.setStoreId(resolveStoreId(page.getStoreId()));
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return ledgerService.pageByQuery(page);
}
@@ -50,14 +46,7 @@ public class GaoPointLedgerController {
@RequireGaoPermission(GaoPermissionCode.POINT_ADJUST)
@PostMapping("/adjust")
public void adjust(@RequestBody GaoPointAdjustRequest request) {
request.setStoreId(resolveStoreId(request.getStoreId()));
request.setStoreId(storeScopeService.resolveStoreId(request.getStoreId()));
triggerService.adjust(request);
}
private String resolveStoreId(String requestedStoreId) {
if (roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
return requestedStoreId;
}
return storeService.getBelongIdByRequiredLoginUserId();
}
}
@@ -1,13 +1,10 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
@@ -32,8 +29,7 @@ import java.util.List;
@RequestMapping("/gao/customer/point")
public class GaoPointRuleController {
private final RoleChecker roleChecker;
private final GaoStoreService storeService;
private final GaoStoreScopeService storeScopeService;
private final GaoPointRuleService ruleService;
@AOPLog
@@ -42,9 +38,9 @@ public class GaoPointRuleController {
@PostMapping("/rule/list")
public PageResult<GaoPointRule> list(@RequestBody Page<GaoPointRule> page) {
TimiException.required(page, "not found page");
if (!isGlobalManager()) {
if (!storeScopeService.hasGlobalScope()) {
GaoPointRule example = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoPointRule());
example.setStoreId(resolveStoreId(null));
example.setStoreId(storeScopeService.resolveStoreId(example.getStoreId()));
page.setEqualsExample(example);
}
return ruleService.page(page);
@@ -57,7 +53,7 @@ public class GaoPointRuleController {
public GaoPointRule detail(@RequestParam String id) {
GaoPointRule rule = ruleService.get(id);
TimiException.required(rule, "not found point rule");
checkStore(rule.getStoreId());
storeScopeService.checkStoreId(rule.getStoreId());
return rule;
}
@@ -66,7 +62,7 @@ public class GaoPointRuleController {
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_CREATE)
@PostMapping("/rule/create")
public String create(@RequestBody GaoPointRule rule) {
rule.setStoreId(resolveStoreId(rule.getStoreId()));
rule.setStoreId(storeScopeService.resolveStoreId(rule.getStoreId()));
ruleService.create(rule);
return rule.getId();
}
@@ -76,7 +72,7 @@ public class GaoPointRuleController {
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_UPDATE)
@PostMapping("/rule/update")
public void update(@RequestBody GaoPointRule rule) {
rule.setStoreId(resolveStoreId(rule.getStoreId()));
rule.setStoreId(storeScopeService.resolveStoreId(rule.getStoreId()));
ruleService.update(rule);
}
@@ -87,7 +83,7 @@ public class GaoPointRuleController {
public void sort(@RequestBody List<String> idList) {
if (idList != null) {
for (GaoPointRule rule : ruleService.listByIdList(idList)) {
checkStore(rule.getStoreId());
storeScopeService.checkStoreId(rule.getStoreId());
}
}
ruleService.sort(idList);
@@ -100,24 +96,7 @@ public class GaoPointRuleController {
public void delete(@RequestParam String id) {
GaoPointRule rule = ruleService.get(id);
TimiException.required(rule, "not found point rule");
checkStore(rule.getStoreId());
storeScopeService.checkStoreId(rule.getStoreId());
ruleService.delete(id);
}
private String resolveStoreId(String requestedStoreId) {
if (isGlobalManager()) {
return requestedStoreId;
}
return storeService.getBelongIdByRequiredLoginUserId();
}
private void checkStore(String storeId) {
if (!isGlobalManager()) {
TimiException.requiredTrue(resolveStoreId(null).equals(storeId), "无权操作其他门店数据");
}
}
private boolean isGlobalManager() {
return roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name());
}
}
@@ -4,6 +4,9 @@ import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoStoreBusinessDay;
import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
@@ -27,12 +30,17 @@ import java.util.List;
public class GaoStoreBusinessDayController {
private final GaoStoreBusinessDayService service;
private final GaoStoreScopeService storeScopeService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STORE_READ)
@PostMapping("/list")
public PageResult<GaoStoreBusinessDay> list(@RequestBody Page<GaoStoreBusinessDay> page) {
TimiException.required(page, "not found page");
GaoStoreBusinessDay example = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoStoreBusinessDay());
example.setStoreId(storeScopeService.resolveStoreId(example.getStoreId()));
page.setEqualsExample(example);
return service.page(page);
}
@@ -41,7 +49,7 @@ public class GaoStoreBusinessDayController {
@RequireGaoPermission(GaoPermissionCode.STORE_READ)
@PostMapping("/calendar")
public List<GaoStoreBusinessDay> calendar(@RequestParam String storeId, @RequestParam Integer beginDateValue, @RequestParam Integer endDateValue) {
return service.listByDateRange(storeId, beginDateValue, endDateValue);
return service.listByDateRange(storeScopeService.resolveStoreId(storeId), beginDateValue, endDateValue);
}
@AOPLog
@@ -49,7 +57,10 @@ public class GaoStoreBusinessDayController {
@RequireGaoPermission(GaoPermissionCode.STORE_READ)
@PostMapping("/detail")
public GaoStoreBusinessDay detail(@RequestParam String id) {
return service.get(id);
GaoStoreBusinessDay businessDay = service.get(id);
TimiException.required(businessDay, "not found businessDay");
storeScopeService.checkStoreId(businessDay.getStoreId());
return businessDay;
}
@AOPLog
@@ -57,6 +68,7 @@ public class GaoStoreBusinessDayController {
@RequireGaoPermission(GaoPermissionCode.STORE_UPDATE)
@PostMapping("/create")
public String create(@RequestBody GaoStoreBusinessDay businessDay) {
businessDay.setStoreId(storeScopeService.resolveStoreId(businessDay.getStoreId()));
service.create(businessDay);
return businessDay.getId();
}
@@ -66,6 +78,12 @@ public class GaoStoreBusinessDayController {
@RequireGaoPermission(GaoPermissionCode.STORE_UPDATE)
@PostMapping("/update")
public void update(@RequestBody GaoStoreBusinessDay businessDay) {
TimiException.required(businessDay, "not found businessDay");
TimiException.required(businessDay.getId(), "not found businessDay.id");
GaoStoreBusinessDay dbBusinessDay = service.get(businessDay.getId());
TimiException.required(dbBusinessDay, "not found businessDay");
storeScopeService.checkStoreId(dbBusinessDay.getStoreId());
businessDay.setStoreId(dbBusinessDay.getStoreId());
service.update(businessDay);
}
@@ -74,6 +92,9 @@ public class GaoStoreBusinessDayController {
@RequireGaoPermission(GaoPermissionCode.STORE_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
GaoStoreBusinessDay businessDay = service.get(id);
TimiException.required(businessDay, "not found businessDay");
storeScopeService.checkStoreId(businessDay.getStoreId());
service.delete(id);
}
}
@@ -3,12 +3,14 @@ package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.service.GaoStoreChartService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordNumberValueDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import lombok.RequiredArgsConstructor;
@@ -30,6 +32,7 @@ import java.util.List;
public class GaoStoreChartController {
private final GaoStoreChartService service;
private final GaoStoreScopeService storeScopeService;
/// 查询客户增长趋势
@AOPLog
@@ -37,6 +40,8 @@ public class GaoStoreChartController {
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/daily")
public List<GaoCustomerDailyStateView> customerDaily(@RequestBody GaoStoreChartReq req) {
TimiException.required(req, "not found req");
req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
return service.customerDailyTrend(req);
}
@@ -46,7 +51,7 @@ public class GaoStoreChartController {
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/gender")
public List<GaoCustomerGenderStatView> customerGender(@RequestParam String storeId) {
return service.customerGender(storeId);
return service.customerGender(storeScopeService.resolveStoreId(storeId));
}
/// 查询登记事件每日统计,可同时支撑登记趋势和事件结构图表
@@ -55,6 +60,8 @@ public class GaoStoreChartController {
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/event/record/daily")
public List<GaoEventRecordDailyStatView> eventRecordDaily(@RequestBody GaoStoreChartReq req) {
TimiException.required(req, "not found req");
req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
return service.eventRecordDailyStat(req);
}
@@ -64,6 +71,8 @@ public class GaoStoreChartController {
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/event/record/number-value/daily")
public List<GaoEventRecordNumberValueDailyStatView> eventRecordNumberValueDaily(@RequestBody GaoStoreChartReq req) {
TimiException.required(req, "not found req");
req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
return service.eventRecordNumberValueDailyStat(req);
}
@@ -73,6 +82,8 @@ public class GaoStoreChartController {
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/event/record/calendar")
public List<GaoEventRecordCalendarView> eventRecordCalendar(@RequestBody GaoStoreChartReq req) {
TimiException.required(req, "not found req");
req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
return service.eventRecordCalendar(req);
}
}
@@ -1,13 +1,13 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoStore;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.user.service.PermissionChecker;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.Network;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog;
@@ -37,14 +37,20 @@ import java.util.List;
public class GaoStoreController {
private final GaoStoreService service;
private final GaoStoreScopeService storeScopeService;
private final UserLoginService userLoginService;
private final PermissionChecker permissionChecker;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STORE_READ)
@PostMapping("/list")
public PageResult<GaoStore> list(@RequestBody Page<GaoStore> page) {
TimiException.required(page, "not found page");
if (!storeScopeService.hasGlobalScope()) {
GaoStore example = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoStore());
example.setId(storeScopeService.resolveStoreId(example.getId()));
page.setEqualsExample(example);
}
return service.page(page);
}
@@ -53,7 +59,9 @@ public class GaoStoreController {
@RequireGaoPermission(GaoPermissionCode.STORE_READ)
@PostMapping("/detail")
public GaoStore detail(@RequestParam String id) {
return service.get(id);
GaoStore store = service.get(id);
storeScopeService.checkStore(store);
return store;
}
@AOPLog
@@ -70,12 +78,13 @@ public class GaoStoreController {
@RequireGaoPermission(GaoPermissionCode.STORE_UPDATE)
@PostMapping("/update")
public void update(@RequestBody GaoStore store) {
if (permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.STORE_CREATE.getValue())) {
if (storeScopeService.hasGlobalScope()) {
// 门店管理
service.update(store);
} else {
// 店长
GaoStore dbStore = service.get(store.getId());
storeScopeService.checkStore(dbStore);
dbStore.setName(store.getName());
dbStore.setTelephone(store.getTelephone());
dbStore.setAddress(store.getAddress());
@@ -98,6 +107,11 @@ public class GaoStoreController {
@RequireGaoPermission(GaoPermissionCode.STORE_UPDATE)
@PostMapping("/sort")
public void sort(@RequestBody List<String> idList) {
if (!storeScopeService.hasGlobalScope()) {
for (String id : TimiJava.safeIterable(idList)) {
storeScopeService.checkStore(service.get(id));
}
}
service.sort(idList);
}
@@ -107,6 +121,12 @@ public class GaoStoreController {
@RequireGaoPermission(GaoPermissionCode.STORE_EXPORT)
@PostMapping("/export")
public void export(@RequestBody Page<GaoStore> page) throws IOException {
TimiException.required(page, "not found page");
if (!storeScopeService.hasGlobalScope()) {
GaoStore example = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoStore());
example.setId(storeScopeService.resolveStoreId(example.getId()));
page.setEqualsExample(example);
}
HttpServletResponse resp = TimiSpring.getResponse();
String contentDisposition = Network.getFileDownloadHeader("门店列表-%s.xlsx".formatted(Time.serialize.format(Time.now())));
resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
@@ -0,0 +1,141 @@
package com.imyeyu.api.modules.gao.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.modules.common.entity.Article;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.service.GaoStoreDiaryService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoStoreDiaryPage;
import com.imyeyu.api.modules.user.entity.User;
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.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
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;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/// GAO 门店日记接口
///
/// @author Codex
/// @since 2026-08-31
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/store/diary")
public class GaoStoreDiaryController {
private final GaoStoreDiaryService service;
private final GaoStoreScopeService storeScopeService;
private final UserLoginService userLoginService;
private final UserService userService;
private final AttachmentService attachmentService;
/// 分页查询门店日记
///
/// @param page 分页查询条件
/// @return 门店日记分页结果
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STORE_DIARY_READ)
@PostMapping("/list")
@JsonView(ResponseView.Public.class)
public PageResult<Article> list(@RequestBody GaoStoreDiaryPage page) {
TimiException.required(page, "未找到分页参数");
TimiException.required(page.getBeginCreatedAt(), "未找到门店日记开始时间");
TimiException.required(page.getEndCreatedAt(), "未找到门店日记结束时间");
TimiException.requiredTrue(page.getBeginCreatedAt() < page.getEndCreatedAt(), "门店日记时间范围无效");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
PageResult<Article> result = service.page(page);
{
List<String> articleIdList = result.getList().stream().map(Article::getId).toList();
Map<String, Attachment> attachMap = attachmentService.mapFirstImageByIdList(Attachment.BizType.ARTICLE, articleIdList);
for (Article article : result.getList()) {
Attachment attachment = attachMap.get(article.getId());
article.setAttachmentList(TimiJava.ofList(attachment));
}
}
return result;
}
/// 查询门店日记详情
///
/// @param id 日记 ID
/// @return 门店日记
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STORE_DIARY_READ)
@PostMapping("/detail")
@JsonView(ResponseView.Public.class)
public Article detail(@RequestParam String id) {
Article article = service.get(id);
storeScopeService.checkStoreId(article.getBizId());
article.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.ARTICLE, article.getId()));
if (TimiJava.isNotEmpty(article.getUpdatedBy())) {
User updater = userService.get(article.getUpdatedBy());
if (updater != null) {
updater.setAttachmentList(attachmentService.listByBizId(
Attachment.BizType.USER,
updater.getId(),
User.AttachType.AVATAR.toString()
));
}
article.setUpdater(updater);
}
return article;
}
/// 创建门店日记
///
/// @param article 门店日记
/// @return 日记 ID
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STORE_DIARY_CREATE)
@PostMapping("/create")
public String create(@RequestBody Article article) {
article.setBizId(storeScopeService.resolveStoreId(article.getBizId()));
service.create(article, userLoginService.getRequireLoginUserId());
return article.getId();
}
/// 修改门店日记
///
/// @param article 门店日记
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STORE_DIARY_UPDATE)
@PostMapping("/update")
public void update(@RequestBody Article article) {
TimiException.required(article, "未找到门店日记");
TimiException.required(article.getId(), "未找到门店日记 ID");
Article dbArticle = service.get(article.getId());
storeScopeService.checkStoreId(dbArticle.getBizId());
service.update(article, userLoginService.getRequireLoginUserId());
}
/// 删除门店日记
///
/// @param id 日记 ID
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STORE_DIARY_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
Article article = service.get(id);
storeScopeService.checkStoreId(article.getBizId());
service.delete(article.getId());
}
}
@@ -6,9 +6,8 @@ import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoUser;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.service.GaoUserInviteService;
import com.imyeyu.api.modules.gao.service.GaoUserService;
import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateRequest;
@@ -19,15 +18,14 @@ import com.imyeyu.api.modules.gao.vo.GaoUserInviteValidateRequest;
import com.imyeyu.api.modules.gao.vo.GaoUserInviteValidateResponse;
import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.api.modules.user.service.RoleService;
import com.imyeyu.api.modules.user.service.AuthorizationScopeService;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.UserRoleService;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.api.modules.user.vo.LoginResponse;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.CaptchaValid;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
@@ -54,12 +52,11 @@ import java.util.UUID;
public class GaoUserController {
private final UserService userService;
private final RoleService roleService;
private final RoleChecker roleChecker;
private final AuthorizationScopeService authorizationScopeService;
private final GaoUserService service;
private final GaoUserInviteService inviteService;
private final UserRoleService userRoleService;
private final GaoStoreService gaoStoreService;
private final GaoStoreScopeService storeScopeService;
private final UserLoginService userLoginService;
private final AttachmentService attachmentService;
@@ -80,12 +77,11 @@ public class GaoUserController {
@AOPLog
@JsonView(ResponseView.Public.class)
@CaptchaValid
@RequestRateLimit(value = 5, inSeconds = 60)
@PostMapping("/invite/register")
public LoginResponse registerByInvite(@RequestBody @Valid GaoUserInviteRegisterRequest req) {
GaoUserInviteRegisterData data = req.getData();
String userId = inviteService.register(data);
String userId = inviteService.register(data, req.getDetailId(), req.getCaptcha());
return userLoginService.login(UUID.randomUUID().toString(), userId);
}
@@ -95,10 +91,11 @@ public class GaoUserController {
@PostMapping("/list")
@JsonView(ResponseView.Public.class)
public PageResult<GaoUser> list(@RequestBody Page<GaoUser> page) {
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
TimiException.required(page, "not found page");
if (!storeScopeService.hasGlobalScope()) {
// 非全局管理只能看所在店用户
page.setEqualsExample(TimiJava.defaultIfNull(page.getEqualsExample(), new GaoUser()));
page.getEqualsExample().setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId());
page.getEqualsExample().setStoreId(storeScopeService.resolveStoreId(page.getEqualsExample().getStoreId()));
}
PageResult<GaoUser> result = service.page(page);
{
@@ -120,6 +117,8 @@ public class GaoUserController {
@JsonView(ResponseView.Public.class)
public GaoUser detail(@RequestParam String id) {
GaoUser result = service.get(id);
TimiException.required(result, "not found gao user");
storeScopeService.checkStoreId(result.getStoreId());
{
User user = userService.get(result.getUserId());
user.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.USER, user.getId()));
@@ -137,9 +136,7 @@ public class GaoUserController {
@PostMapping("/create")
@JsonView(ResponseView.Public.class)
public GaoUser create(@RequestBody GaoUser gaoUser) {
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
gaoUser.setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId());
}
gaoUser.setStoreId(storeScopeService.resolveStoreId(gaoUser.getStoreId()));
service.create(gaoUser);
return service.get(gaoUser.getId());
}
@@ -150,6 +147,12 @@ public class GaoUserController {
@PostMapping("/update")
@JsonView(ResponseView.Public.class)
public GaoUser update(@RequestBody GaoUser gaoUser) {
TimiException.required(gaoUser, "not found gao user");
TimiException.required(gaoUser.getId(), "not found gao user.id");
GaoUser dbGaoUser = service.get(gaoUser.getId());
TimiException.required(dbGaoUser, "not found gao user");
storeScopeService.checkStoreId(dbGaoUser.getStoreId());
gaoUser.setStoreId(dbGaoUser.getStoreId());
service.update(gaoUser);
return service.get(gaoUser.getId());
}
@@ -160,6 +163,12 @@ public class GaoUserController {
@PostMapping("/transfer/store")
@JsonView(ResponseView.Public.class)
public GaoUser transferStore(@RequestBody GaoUser gaoUser) {
TimiException.required(gaoUser, "not found gao user");
TimiException.required(gaoUser.getId(), "not found gao user.id");
GaoUser dbGaoUser = service.get(gaoUser.getId());
TimiException.required(dbGaoUser, "not found gao user");
storeScopeService.checkStoreId(dbGaoUser.getStoreId());
gaoUser.setStoreId(storeScopeService.resolveStoreId(gaoUser.getStoreId()));
service.transferStore(gaoUser);
return service.get(gaoUser.getId());
}
@@ -169,6 +178,9 @@ public class GaoUserController {
@RequireGaoPermission(GaoPermissionCode.USER_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
GaoUser gaoUser = service.get(id);
TimiException.required(gaoUser, "not found gao user");
storeScopeService.checkStoreId(gaoUser.getStoreId());
service.delete(id);
}
@@ -177,7 +189,6 @@ public class GaoUserController {
@RequireGaoPermission(GaoPermissionCode.USER_READ)
@PostMapping("/role/list")
public List<Role> roleList() {
// TODO 此接口获取本模块所有角色,需要限制角色
return roleService.listByModuleCode(ModuleCode.GAO);
return authorizationScopeService.listGrantableRole(userLoginService.getRequireLoginUserId(), ModuleCode.GAO);
}
}
@@ -61,15 +61,15 @@ public class GaoCustomer extends UUIDEntity {
/// 姓名
private String name;
@Column("zh_cn_pinyin_full")
@Column("name_pinyin_full")
@JsonIgnore
private String namePinyinFull;
@Column("zh_cn_pinyin_initial")
@Column("name_pinyin_initial")
@JsonIgnore
private String namePinyinInitial;
@Column("zh_cn_pinyin_mixed")
@Column("name_pinyin_mixed")
@JsonIgnore
private String namePinyinMixed;
@@ -0,0 +1,145 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/// GAO 客户实际购买的套餐
///
/// @author Codex
/// @since 2026-08-31
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoCustomerPackage extends UUIDEntity {
/// 客户套餐状态
public enum Status {
/// 履约中
ACTIVE,
/// 已全部履约
FULFILLED,
/// 已取消
CANCELLED
}
/// 付款状态
public enum PaymentStatus {
/// 未付款
UNPAID,
/// 部分付款
PARTIALLY_PAID,
/// 已付清
PAID
}
/// 退款状态
public enum RefundStatus {
/// 无需退款
NOT_REQUIRED,
/// 待退款或待确认无需退款
PENDING,
/// 已完成退款处理
REFUNDED
}
/// 门店 ID
private String storeId;
/// 客户 ID
private String customerId;
/// 来源套餐配置 ID
private String packageConfigId;
/// 购买事件登记 ID
private String purchaseEventRecordId;
/// 购买幂等请求号
private String requestNo;
/// 套餐名称快照
private String packageNameSnapshot;
/// 标准价快照,单位分
private Long standardAmountSnapshot;
/// 实际成交价,单位分
private Long saleAmount;
/// 待核销收款合计,单位分
private Long pendingPaidAmount;
/// 累计有效收款,单位分
private Long paidAmount;
/// 累计有效退款,单位分
private Long refundedAmount;
/// 套餐状态
private Status status;
/// 付款状态
private PaymentStatus paymentStatus;
/// 退款状态
private RefundStatus refundStatus;
/// 购买时间
private Long purchasedAt;
/// 购买登记人
private String operatorUserId;
/// 取消时间
private Long cancelledAt;
/// 取消操作人
private String cancelOperatorUserId;
/// 取消原因
private String cancelReason;
/// 备注
private String remark;
/// 客户
@Transient
private GaoCustomer customer;
/// 购买事件登记
@Transient
private GaoEventRecord purchaseEventRecord;
/// 购买操作人
@Transient
private User operator;
/// 取消操作人
@Transient
private User cancelOperator;
/// 产品权益列表
@Transient
private List<GaoCustomerPackageItem> itemList;
/// 发放历史
@Transient
private List<GaoPackageDelivery> deliveryList;
/// 资金历史
@Transient
private List<GaoFundRecord> fundRecordList;
}
@@ -0,0 +1,53 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/// GAO 客户套餐产品权益快照
///
/// @author Codex
/// @since 2026-08-31
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoCustomerPackageItem extends UUIDEntity {
/// 客户套餐 ID
private String customerPackageId;
/// 产品名称快照
private String productNameSnapshot;
/// 产品单位快照
private String unitSnapshot;
/// 总权益数量
private BigDecimal totalQuantity;
/// 待核销占用数量
private BigDecimal reservedQuantity;
/// 已核销净发放数量
private BigDecimal deliveredQuantity;
/// 排序值
private Integer sort;
/// 备注
private String remark;
/// 待核销数量
@Transient
private BigDecimal pendingQuantity;
/// 当前可发数量
@Transient
private BigDecimal availableQuantity;
/// 权益剩余数量
@Transient
private BigDecimal remainingQuantity;
}
@@ -18,6 +18,16 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true)
public class GaoEvent extends UUIDEntity {
/// 事件业务类型
public enum BizType {
/// 普通登记事件
GENERAL,
/// 购买产品套餐
PACKAGE_PURCHASE
}
///
/// 登记事件状态
///
@@ -93,6 +103,16 @@ public class GaoEvent extends UUIDEntity {
/// 统计图表颜色
private String color;
/// 业务类型
private BizType bizType;
/// 购买套餐事件绑定的套餐配置 ID
private String packageConfigId;
/// 购买套餐事件绑定的套餐配置
@Transient
private GaoPackageConfig packageConfig;
/// 登记事件名称
private String name;
@@ -117,6 +137,9 @@ public class GaoEvent extends UUIDEntity {
/// true 为登记时要求拍照
private Boolean requirePhoto;
/// true 为登记记录需要核销
private Boolean requireVerification;
/// 登记限制类型
private LimitType limitType;
@@ -135,4 +158,8 @@ public class GaoEvent extends UUIDEntity {
/// 可登记角色代码列表
@Transient
private List<String> roleCodeList;
/// 登记事件选项组列表
@Transient
private List<GaoEventOptionGroup> optionGroupList;
}
@@ -0,0 +1,23 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/// 登记事件选项
///
/// @author Codex
/// @since 2026-08-30
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoEventOption extends UUIDEntity {
/// 选项组 ID
private String groupId;
/// 选项名称
private String label;
/// 排序值
private Integer sort;
}
@@ -0,0 +1,46 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/// 登记事件选项组
///
/// @author Codex
/// @since 2026-08-30
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoEventOptionGroup extends UUIDEntity {
/// 选项选择模式
public enum SelectionMode {
/// 只能选择一个选项
SINGLE,
/// 可以选择多个选项
MULTIPLE
}
/// 登记事件 ID
private String eventId;
/// 选项组名称
private String name;
/// 选择模式
private SelectionMode selectionMode;
/// true 为登记时必须选择至少一个选项
private Boolean required;
/// 排序值
private Integer sort;
/// 选项列表
@Transient
private List<GaoEventOption> optionList;
}
@@ -18,6 +18,23 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true)
public class GaoEventRecord extends UUIDEntity {
///
/// 登记记录核销状态
///
/// @author Codex
/// @since 2026-08-29
public enum VerificationStatus {
/// 该事件不需要核销
NOT_REQUIRED,
/// 等待核销
PENDING,
/// 已核销
VERIFIED
}
///
/// 登记事件记录附件类型
///
@@ -49,6 +66,15 @@ public class GaoEventRecord extends UUIDEntity {
/// 本次登记提交的整数业务数值
private Long numberValue;
/// 核销状态
private VerificationStatus verificationStatus;
/// 核销用户 ID
private String verifierUserId;
/// 核销时间
private Long verifiedAt;
/// 备注
private String remark;
@@ -61,6 +87,9 @@ public class GaoEventRecord extends UUIDEntity {
/// 营业日递增序号
private Long businessDayNo;
/// 最后修改用户 ID
private String updatedBy;
@Transient
private GaoCustomer customer;
@@ -70,10 +99,21 @@ public class GaoEventRecord extends UUIDEntity {
@Transient
private User operator;
@Transient
private User verifier;
/// 最后修改用户
@Transient
private User updater;
@Transient
private List<Attachment> attachmentList;
/// 客户 ID 列表,批量登记时使用
@Transient
private List<String> customerIdList;
/// 登记记录已选择的选项组列表
@Transient
private List<GaoEventRecordOptionGroup> selectedOptionGroupList;
}
@@ -0,0 +1,35 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/// 登记事件记录选项关系
///
/// @author Codex
/// @since 2026-08-30
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoEventRecordOption extends UUIDEntity {
/// 登记事件记录 ID
private String eventRecordId;
/// 选项组 ID
private String groupId;
/// 选项 ID
private String optionId;
/// 选项组排序快照
private Integer groupSort;
/// 选项排序快照
private Integer optionSort;
/// 选项组名称快照
private String groupNameSnapshot;
/// 选项名称快照
private String optionLabelSnapshot;
}
@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.gao.entity;
import lombok.Data;
import java.util.List;
/// 登记事件记录选项组视图
///
/// @author Codex
/// @since 2026-08-30
@Data
public class GaoEventRecordOptionGroup {
/// 选项组 ID
private String groupId;
/// 选项组名称
private String groupName;
/// 已选择的选项列表
private List<GaoEventOption> selectedOptionList;
}
@@ -0,0 +1,115 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/// GAO 通用资金操作流水
///
/// @author Codex
/// @since 2026-08-31
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoFundRecord extends UUIDEntity {
/// 资金业务类型
public enum BizType {
/// 客户套餐
CUSTOMER_PACKAGE
}
/// 资金操作类型
public enum OperationType {
/// 线下收款
RECEIVE,
/// 退款
REFUND,
/// 确认无需退款
CONFIRM_NO_REFUND,
/// 资金操作冲正
REVOKE
}
/// 资金操作状态
public enum Status {
/// 待店长核销
PENDING,
/// 已生效
VERIFIED,
/// 店长已拒绝
REJECTED,
/// 提交人已撤销
CANCELLED
}
/// 门店 ID
private String storeId;
/// 客户 ID
private String customerId;
/// 资金业务类型
private BizType bizType;
/// 业务对象 ID
private String bizId;
/// 操作类型
private OperationType operationType;
/// 操作状态
private Status status;
/// 金额,单位分
private Long amount;
/// 冲正时关联原资金记录
private String sourceRecordId;
/// 幂等请求号
private String requestNo;
/// 提交操作的用户 ID
private String operatorUserId;
/// 收款核销用户 ID
private String verifierUserId;
/// 收款核销时间
private Long verifiedAt;
/// 线下操作发生时间
private Long occurredAt;
/// 原因
private String reason;
/// 备注
private String remark;
/// 操作人
@Transient
private User operator;
/// 核销人
@Transient
private User verifier;
/// 附件
@Transient
private List<Attachment> attachmentList;
}
@@ -0,0 +1,66 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/// GAO 套餐配置
///
/// @author Codex
/// @since 2026-08-31
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoPackageConfig extends UUIDEntity {
/// 套餐配置状态
public enum Status {
/// 草稿,不可销售
DRAFT,
/// 启用,可在有效期内销售
ACTIVE,
/// 停用,不可销售
INACTIVE
}
/// 门店 ID
private String storeId;
/// 套餐名称
private String name;
/// 套餐说明
private String description;
/// 标准总价,单位分
private Long standardAmount;
/// 状态
private Status status;
/// 生效开始时间戳,毫秒,空为不限制
private Long beginAt;
/// 生效结束时间戳,毫秒,空为不限制
private Long endAt;
/// 排序值
private Integer sort;
/// 最后修改用户 ID
private String updatedBy;
/// 最后修改用户
@Transient
private User updater;
/// 配置产品列表
@Transient
private List<GaoPackageConfigItem> itemList;
}
@@ -0,0 +1,34 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/// GAO 套餐配置产品明细
///
/// @author Codex
/// @since 2026-08-31
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoPackageConfigItem extends UUIDEntity {
/// 套餐配置 ID
private String packageConfigId;
/// 产品名称
private String productName;
/// 产品单位
private String unit;
/// 默认数量
private BigDecimal defaultQuantity;
/// 排序值
private Integer sort;
/// 备注
private String remark;
}
@@ -0,0 +1,107 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/// GAO 套餐发放操作
///
/// @author Codex
/// @since 2026-08-31
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoPackageDelivery extends UUIDEntity {
/// 发放操作类型
public enum OperationType {
/// 正常发放
DELIVER,
/// 整单冲正
REVOKE
}
/// 发放状态
public enum Status {
/// 待核销
PENDING,
/// 已核销
VERIFIED,
/// 店长已拒绝
REJECTED,
/// 提交人已撤销
CANCELLED,
/// 已核销后被整单冲正
REVOKED
}
/// 客户套餐 ID
private String customerPackageId;
/// 门店 ID
private String storeId;
/// 客户 ID
private String customerId;
/// 操作类型
private OperationType operationType;
/// 发放状态
private Status status;
/// 客户端幂等请求号
private String requestNo;
/// 提交人
private String operatorUserId;
/// 实际发放时间
private Long deliveredAt;
/// 核销或拒绝操作人
private String verifierUserId;
/// 核销或拒绝时间
private Long verifiedAt;
/// 冲正单指向原发放单
private String sourceDeliveryId;
/// 拒绝、撤销或冲正原因
private String reason;
/// 备注
private String remark;
/// 操作人
@Transient
private User operator;
/// 核销或拒绝操作人
@Transient
private User verifier;
/// 原发放单
@Transient
private GaoPackageDelivery sourceDelivery;
/// 发放明细
@Transient
private List<GaoPackageDeliveryItem> itemList;
/// 附件
@Transient
private List<Attachment> attachmentList;
}
@@ -0,0 +1,43 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/// GAO 套餐发放产品明细
///
/// @author Codex
/// @since 2026-08-31
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoPackageDeliveryItem extends UUIDEntity {
/// 发放单 ID
private String deliveryId;
/// 客户套餐产品 ID
private String customerPackageItemId;
/// 本次数量
private BigDecimal quantity;
/// 操作前占用数量快照
private BigDecimal reservedBefore;
/// 操作后占用数量快照
private BigDecimal reservedAfter;
/// 操作前已发数量快照
private BigDecimal deliveredBefore;
/// 操作后已发数量快照
private BigDecimal deliveredAfter;
/// 产品名称快照
private String productNameSnapshot;
/// 产品单位快照
private String unitSnapshot;
}
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -24,6 +25,9 @@ public class GaoPointAccount extends UUIDEntity {
/// 累计获得积分
private Long totalEarned;
/// 累计消耗积分
private Long totalConsumed;
/// 累计撤销积分
private Long totalRevoked;
@@ -32,4 +36,8 @@ public class GaoPointAccount extends UUIDEntity {
/// 乐观锁版本
private Long version;
/// 客户信息
@Transient
private GaoCustomer customer;
}
@@ -17,9 +17,12 @@ public class GaoPointLedger extends UUIDEntity {
/// 流水变化类型
public enum ChangeType {
/// 登记事件积分变化
/// 登记事件发放积分
EARN,
/// 登记事件消费积分
CONSUME,
/// 删除登记记录撤销积分
REVOKE,
@@ -6,6 +6,7 @@ import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import java.util.Collection;
import java.util.List;
@@ -48,4 +49,11 @@ public interface GaoCustomerMapper extends BaseMapper<GaoCustomer, String> {
/// @param page 查询条件
/// @return 客户列表
List<GaoCustomer> selectByQuery(GaoCustomerPage page);
/// 原子调整邀请客户数
///
/// @param customerId 客户 ID
/// @param delta 变化量
@Update("UPDATE `gao_customer` SET `invited_count` = GREATEST(IFNULL(`invited_count`, 0) + #{delta}, 0), `updated_at` = " + UNIX_TIME + " WHERE `id` = #{customerId}" + NOT_DELETE)
void updateInvitedCountDelta(@Param("customerId") String customerId, @Param("delta") Long delta);
}
@@ -0,0 +1,30 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/// GAO 客户套餐产品权益 Mapper
///
/// @author Codex
/// @since 2026-08-31
public interface GaoCustomerPackageItemMapper extends BaseMapper<GaoCustomerPackageItem, String> {
/// 按客户套餐查询产品权益
List<GaoCustomerPackageItem> selectByCustomerPackageId(@Param("customerPackageId") String customerPackageId);
/// 批量查询客户套餐产品权益
List<GaoCustomerPackageItem> selectByCustomerPackageIdList(@Param("customerPackageIdList") Collection<String> customerPackageIdList);
/// 锁定客户套餐产品
GaoCustomerPackageItem selectForUpdate(@Param("id") String id);
/// 调整待核销占用数量
int updateReservedQuantity(@Param("id") String id, @Param("delta") java.math.BigDecimal delta);
/// 调整已核销发放数量
int updateDeliveredQuantity(@Param("id") String id, @Param("delta") java.math.BigDecimal delta);
}
@@ -0,0 +1,40 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
/// GAO 客户套餐 Mapper
///
/// @author Codex
/// @since 2026-08-31
public interface GaoCustomerPackageMapper extends BaseMapper<GaoCustomerPackage, String> {
/// 按门店和请求号查询购买结果
GaoCustomerPackage selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo);
/// 按请求号查询购买结果,用于配置删除后的幂等重试
GaoCustomerPackage selectByRequestNoAnyStore(@Param("requestNo") String requestNo);
/// 锁定客户套餐
GaoCustomerPackage selectForUpdate(@Param("id") String id);
/// 按购买事件登记查询客户套餐
GaoCustomerPackage selectByPurchaseEventRecordId(@Param("purchaseEventRecordId") String purchaseEventRecordId);
/// 按条件统计客户套餐
long countByQuery(GaoCustomerPackagePage page);
/// 按条件查询客户套餐
java.util.List<GaoCustomerPackage> selectByQuery(GaoCustomerPackagePage page);
/// 原子调整待核销收款
int updatePendingPaidAmountDelta(@Param("id") String id, @Param("delta") Long delta);
/// 原子调整有效收款
int updatePaidAmountDelta(@Param("id") String id, @Param("delta") Long delta);
/// 带状态条件更新套餐状态
int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoCustomerPackage.Status fromStatus, @Param("toStatus") GaoCustomerPackage.Status toStatus, @Param("cancelledAt") Long cancelledAt, @Param("cancelOperatorUserId") String cancelOperatorUserId, @Param("cancelReason") String cancelReason, @Param("refundStatus") GaoCustomerPackage.RefundStatus refundStatus);
}
@@ -1,8 +1,11 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.SelectProvider;
import java.util.Collection;
import java.util.List;
@@ -14,7 +17,16 @@ import java.util.List;
/// @since 2026-07-27 15:53
public interface GaoEventMapper extends BaseMapper<GaoEvent, String> {
@SelectProvider(type = GaoEventSQLProvider.class, method = "selectPageWithoutDeleted")
List<GaoEvent> selectPageWithoutDeleted(Page<GaoEvent> page);
@SelectProvider(type = GaoEventSQLProvider.class, method = "countPageWithoutDeleted")
long countPageWithoutDeleted(Page<GaoEvent> page);
List<GaoEvent> selectByIdList(Collection<String> idList);
List<GaoEvent> selectValidList(@Param("storeId") String storeId, @Param("now") Long now);
@Select("SELECT COUNT(*) FROM `gao_event` WHERE `package_config_id` = #{packageConfigId} AND `deleted_at` IS NULL")
long countByPackageConfigId(String packageConfigId);
}
@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/// 登记事件选项组 Mapper
///
/// @author Codex
/// @since 2026-08-30
public interface GaoEventOptionGroupMapper extends BaseMapper<GaoEventOptionGroup, String> {
List<GaoEventOptionGroup> selectAllByEventIdList(@Param("eventIdList") Collection<String> eventIdList);
}
@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoEventOption;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/// 登记事件选项 Mapper
///
/// @author Codex
/// @since 2026-08-30
public interface GaoEventOptionMapper extends BaseMapper<GaoEventOption, String> {
List<GaoEventOption> selectAllByGroupIdList(@Param("groupIdList") Collection<String> groupIdList);
}
@@ -8,9 +8,11 @@ import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordNumberValueDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage;
import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView;
import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@@ -20,6 +22,13 @@ import java.util.List;
/// @since 2026-07-27 14:11
public interface GaoEventRecordMapper extends BaseMapper<GaoEventRecord, String> {
/// 查询事件登记记录数量
///
/// @param eventId 事件 ID
/// @return 登记记录数量
@Select("SELECT COUNT(*) FROM `gao_event_record` WHERE `event_id` = #{eventId} AND `deleted_at` IS NULL")
long countByEventId(String eventId);
Long count(String storeId, String customerId, String eventId, Long beginAt, Long endAt);
/// 查询事件中已携带数值的登记记录数量
@@ -44,6 +53,15 @@ public interface GaoEventRecordMapper extends BaseMapper<GaoEventRecord, String>
/// @param earnedPointValue 获得积分总和
void updateEarnedPointValue(@Param("id") String id, @Param("earnedPointValue") Long earnedPointValue);
/// 原子更新登记记录核销状态
///
/// @param id 登记记录 ID
/// @param verifierUserId 核销用户 ID
/// @param verifiedAt 核销时间
/// @param updatedBy 最后修改用户 ID
/// @return 更新行数
int verify(@Param("id") String id, @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt, @Param("updatedBy") String updatedBy);
/// 按门店和登记日期统计登记记录数
///
/// @param storeId 门店 ID
@@ -71,6 +89,12 @@ public interface GaoEventRecordMapper extends BaseMapper<GaoEventRecord, String>
/// @return 登记数值合计
Long sumNumberValueByRange(GaoEventRecordPage page);
/// 按选项统计登记记录数量
///
/// @param page 登记记录范围查询请求
/// @return 选项统计列表
List<GaoEventOptionStatView> selectOptionStatByRange(GaoEventRecordPage page);
/// 统计客户登记排行榜总数
///
/// @param page 排行榜分页请求
@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoEventRecordOption;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/// 登记事件记录选项 Mapper
///
/// @author Codex
/// @since 2026-08-30
public interface GaoEventRecordOptionMapper extends BaseMapper<GaoEventRecordOption, String> {
List<GaoEventRecordOption> selectAllByEventRecordIdList(@Param("eventRecordIdList") Collection<String> eventRecordIdList);
}
@@ -0,0 +1,32 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.util.SQLProvider;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.builder.annotation.ProviderContext;
/// 登记事件分页查询 SQL 提供器
///
/// @author Codex
public class GaoEventSQLProvider extends SQLProvider {
/// 分页查询未删除的登记事件
public String selectPageWithoutDeleted(ProviderContext context, @Param("page") Page<?> page) {
EntityMeta meta = getEntityMeta(context);
return appendWithoutDeleted(buildSelectByPageSQL(meta, meta.getTable(), page, "#{offset}", "#{limit}"));
}
/// 统计未删除的登记事件
public String countPageWithoutDeleted(ProviderContext context, @Param("page") Page<?> page) {
EntityMeta meta = getEntityMeta(context);
return appendWithoutDeleted(buildCountByPageSQL(meta, meta.getTable(), page));
}
private String appendWithoutDeleted(String sql) {
return sql.replace(
"WHERE 1 = 1",
"WHERE 1 = 1 AND (`status` IS NULL OR `status` <> '%s')".formatted(GaoEvent.Status.DELETED.name())
);
}
}
@@ -0,0 +1,39 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// GAO 资金流水 Mapper
///
/// @author Codex
/// @since 2026-08-31
public interface GaoFundRecordMapper extends BaseMapper<GaoFundRecord, String> {
/// 按门店和请求号查询资金操作
GaoFundRecord selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo);
/// 锁定资金记录
GaoFundRecord selectForUpdate(@Param("id") String id);
/// 查询资金冲正记录
GaoFundRecord selectRevokeBySourceRecordId(@Param("sourceRecordId") String sourceRecordId);
/// 查询客户套餐待核销收款数量
long countPendingReceiveByBizId(@Param("bizId") String bizId);
/// 查询客户套餐资金历史
List<GaoFundRecord> selectByBizId(@Param("bizId") String bizId);
/// 按条件统计资金记录
long countByQuery(GaoFundRecordPage page);
/// 按条件查询资金记录
List<GaoFundRecord> selectByQuery(GaoFundRecordPage page);
/// 带状态条件更新资金记录
int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoFundRecord.Status fromStatus, @Param("toStatus") GaoFundRecord.Status toStatus, @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt, @Param("reason") String reason);
}
@@ -0,0 +1,21 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/// GAO 套餐配置产品 Mapper
///
/// @author Codex
/// @since 2026-08-31
public interface GaoPackageConfigItemMapper extends BaseMapper<GaoPackageConfigItem, String> {
/// 按套餐配置 ID 查询产品
List<GaoPackageConfigItem> selectByPackageConfigId(@Param("packageConfigId") String packageConfigId);
/// 批量查询套餐配置产品
List<GaoPackageConfigItem> selectByPackageConfigIdList(@Param("packageConfigIdList") Collection<String> packageConfigIdList);
}
@@ -0,0 +1,31 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/// GAO 套餐配置 Mapper
///
/// @author Codex
/// @since 2026-08-31
public interface GaoPackageConfigMapper extends BaseMapper<GaoPackageConfig, String> {
/// 按 ID 列表查询套餐配置
List<GaoPackageConfig> selectByIdList(@Param("idList") Collection<String> idList);
/// 查询有效可购买套餐
List<GaoPackageConfig> selectValidList(@Param("storeId") String storeId, @Param("now") Long now);
/// 按条件统计套餐配置
long countByQuery(GaoPackageConfigPage page);
/// 按条件查询套餐配置
List<GaoPackageConfig> selectByQuery(GaoPackageConfigPage page);
/// 查询门店内同名配置
GaoPackageConfig selectByStoreIdAndName(@Param("storeId") String storeId, @Param("name") String name, @Param("excludeId") String excludeId);
}
@@ -0,0 +1,21 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/// GAO 套餐发放明细 Mapper
///
/// @author Codex
/// @since 2026-08-31
public interface GaoPackageDeliveryItemMapper extends BaseMapper<GaoPackageDeliveryItem, String> {
/// 按发放单查询明细
List<GaoPackageDeliveryItem> selectByDeliveryId(@Param("deliveryId") String deliveryId);
/// 批量查询发放明细
List<GaoPackageDeliveryItem> selectByDeliveryIdList(@Param("deliveryIdList") Collection<String> deliveryIdList);
}
@@ -0,0 +1,42 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery;
import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// GAO 套餐发放 Mapper
///
/// @author Codex
/// @since 2026-08-31
public interface GaoPackageDeliveryMapper extends BaseMapper<GaoPackageDelivery, String> {
/// 按门店和请求号查询发放结果
GaoPackageDelivery selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo);
/// 锁定发放单
GaoPackageDelivery selectForUpdate(@Param("id") String id);
/// 查询原发放单的冲正记录
GaoPackageDelivery selectRevokeBySourceDeliveryId(@Param("sourceDeliveryId") String sourceDeliveryId);
/// 查询客户套餐待核销发放数量
long countPendingByCustomerPackageId(@Param("customerPackageId") String customerPackageId);
/// 查询客户套餐已核销正常发放数量
long countVerifiedByCustomerPackageId(@Param("customerPackageId") String customerPackageId);
/// 查询客户套餐发放历史
java.util.List<GaoPackageDelivery> selectByCustomerPackageId(@Param("customerPackageId") String customerPackageId);
/// 按条件统计发放单
long countByQuery(GaoPackageDeliveryPage page);
/// 按条件查询发放单
List<GaoPackageDelivery> selectByQuery(GaoPackageDeliveryPage page);
/// 带状态条件更新发放单
int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoPackageDelivery.Status fromStatus, @Param("toStatus") GaoPackageDelivery.Status toStatus, @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt, @Param("reason") String reason);
}
@@ -1,9 +1,12 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
import com.imyeyu.api.modules.gao.vo.GaoPointAccountPage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// GAO 客户积分账户 Mapper
///
/// @author Codex
@@ -17,6 +20,25 @@ public interface GaoPointAccountMapper extends BaseMapper<GaoPointAccount, Strin
/// @return 积分账户
GaoPointAccount selectByStoreIdAndCustomerId(@Param("storeId") String storeId, @Param("customerId") String customerId);
/// 按账户 ID 和门店查询积分账户
///
/// @param storeId 门店 ID,可为空
/// @param id 账户 ID
/// @return 积分账户
GaoPointAccount selectByIdAndStoreId(@Param("storeId") String storeId, @Param("id") String id);
/// 统计积分账户数量
///
/// @param page 查询条件
/// @return 账户数量
long countByQuery(GaoPointAccountPage page);
/// 分页查询积分账户
///
/// @param page 查询条件
/// @return 账户列表
List<GaoPointAccount> selectByQuery(GaoPointAccountPage page);
/// 创建客户积分账户,已存在时保持原账户不变
///
/// @param account 积分账户
@@ -33,9 +55,10 @@ public interface GaoPointAccountMapper extends BaseMapper<GaoPointAccount, Strin
/// @param id 账户 ID
/// @param delta 本次余额变化
/// @param totalEarnedDelta 累计获得积分变化
/// @param totalConsumedDelta 累计消耗积分变化
/// @param totalRevokedDelta 累计撤销积分变化
/// @param totalAdjustedDelta 累计人工调整积分变化
/// @param updatedAt 更新时间
/// @return 更新行数
int updateBalance(@Param("id") String id, @Param("delta") long delta, @Param("totalEarnedDelta") long totalEarnedDelta, @Param("totalRevokedDelta") long totalRevokedDelta, @Param("totalAdjustedDelta") long totalAdjustedDelta, @Param("updatedAt") long updatedAt);
int updateBalance(@Param("id") String id, @Param("delta") long delta, @Param("totalEarnedDelta") long totalEarnedDelta, @Param("totalConsumedDelta") long totalConsumedDelta, @Param("totalRevokedDelta") long totalRevokedDelta, @Param("totalAdjustedDelta") long totalAdjustedDelta, @Param("updatedAt") long updatedAt);
}
@@ -19,11 +19,11 @@ public interface GaoPointLedgerMapper extends BaseMapper<GaoPointLedger, String>
/// @return 流水
GaoPointLedger selectByIdempotencyKey(@Param("idempotencyKey") String idempotencyKey);
/// 查询登记记录产生的原始获得积分流水
/// 查询登记记录产生的原始规则积分流水
///
/// @param eventRecordId 登记记录 ID
/// @return 获得积分流水
List<GaoPointLedger> selectEarnByEventRecordId(@Param("eventRecordId") String eventRecordId);
/// @return 原始规则积分流水
List<GaoPointLedger> selectRuleChangeByEventRecordId(@Param("eventRecordId") String eventRecordId);
/// 批量查询幂等键对应的流水
///
@@ -0,0 +1,26 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.common.entity.Article;
import com.imyeyu.api.modules.gao.vo.GaoStoreDiaryPage;
import com.imyeyu.spring.mapper.BaseMapper;
import java.util.List;
/// GAO 门店日记 Mapper
///
/// @author Codex
/// @since 2026-08-31
public interface GaoStoreDiaryMapper extends BaseMapper<Article, String> {
/// 查询月份内的门店日记
///
/// @param page 月份分页查询请求
/// @return 门店日记列表
List<Article> selectByMonth(GaoStoreDiaryPage page);
/// 统计月份内的门店日记数量
///
/// @param page 月份分页查询请求
/// @return 门店日记数量
long countByMonth(GaoStoreDiaryPage page);
}
@@ -1,7 +1,12 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoUser;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
///
/// GAO 用户 Mapper
@@ -9,4 +14,10 @@ import com.imyeyu.spring.mapper.BaseMapper;
/// @author Codex
/// @since 2026-07-30
public interface GaoUserMapper extends BaseMapper<GaoUser, String> {
List<GaoUser> selectByUserIdList(Collection<String> userIdList);
List<GaoUser> selectRecipientPage(@Param("page") Page<GaoUser> page);
long countRecipients(@Param("page") Page<GaoUser> page);
}
@@ -0,0 +1,87 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/// GAO 客户套餐服务
///
/// @author Codex
/// @since 2026-08-31
public interface GaoCustomerPackageService extends BaseService<GaoCustomerPackage, String> {
/// 按条件查询客户套餐
PageResult<GaoCustomerPackage> pageByQuery(GaoCustomerPackagePage page);
/// 创建客户套餐及购买事件登记
GaoCustomerPackage purchase(GaoCustomerPackagePurchaseRequest request);
/// 取消客户套餐
GaoCustomerPackage cancel(GaoCustomerPackageCancelRequest request);
/// 锁定客户套餐
///
/// @param id 客户套餐 ID
/// @return 已锁定的客户套餐,不存在时为空
GaoCustomerPackage getForUpdate(String id);
/// 查询客户套餐产品权益
///
/// @param customerPackageId 客户套餐 ID
/// @return 产品权益列表
List<GaoCustomerPackageItem> listItemByCustomerPackageId(String customerPackageId);
/// 批量查询客户套餐产品权益
///
/// @param customerPackageIdList 客户套餐 ID 列表
/// @return 按客户套餐 ID 分组的产品权益
Map<String, List<GaoCustomerPackageItem>> mapItemListByCustomerPackageIdList(Collection<String> customerPackageIdList);
/// 锁定客户套餐产品权益
///
/// @param itemId 客户套餐产品权益 ID
/// @return 已锁定的产品权益,不存在时为空
GaoCustomerPackageItem getItemForUpdate(String itemId);
/// 原子调整待核销发放占用数量
///
/// @param itemId 客户套餐产品权益 ID
/// @param delta 调整量
/// @return 受影响行数
int updateItemReservedQuantity(String itemId, BigDecimal delta);
/// 原子调整已核销发放数量
///
/// @param itemId 客户套餐产品权益 ID
/// @param delta 调整量
/// @return 受影响行数
int updateItemDeliveredQuantity(String itemId, BigDecimal delta);
/// 原子调整待核销收款金额
///
/// @param id 客户套餐 ID
/// @param delta 调整量
/// @return 受影响行数
int updatePendingPaidAmountDelta(String id, Long delta);
/// 原子调整有效收款金额
///
/// @param id 客户套餐 ID
/// @param delta 调整量
/// @return 受影响行数
int updatePaidAmountDelta(String id, Long delta);
/// 保存客户套餐聚合状态
///
/// @param packageEntity 客户套餐聚合
void updateAggregateState(GaoCustomerPackage packageEntity);
}
@@ -45,6 +45,11 @@ public interface GaoCustomerService extends BaseService<GaoCustomer, String> {
List<GaoCustomer> listByIdList(Collection<String> idList);
/// 删除门店下的全部客户及其关联业务数据
///
/// @param storeId 门店 ID
void deleteByStoreId(String storeId);
default Map<String, GaoCustomer> mapByIdList(Collection<String> idList) {
return listByIdList(idList).stream().collect(Collectors.toMap(GaoCustomer::getId, item -> item));
}
@@ -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.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage;
@@ -24,12 +25,34 @@ public interface GaoEventRecordService extends BaseService<GaoEventRecord, Strin
/// @return 登记事件记录列表
List<GaoEventRecord> listByCustomerId(String customerId);
/// 为登记记录附加已选择的选项组
///
/// @param recordList 登记记录列表
void attachSelectedOptionGroupList(List<GaoEventRecord> recordList);
/// 核销登记事件记录
///
/// @param id 登记记录 ID
void verify(String id);
default void createBatch(List<GaoEventRecord> recordList) {
for (GaoEventRecord record : TimiJava.safeIterable(recordList)) {
create(record);
}
}
/// 创建单条普通事件登记
///
/// @param record 事件登记
/// @return 创建后的事件登记
GaoEventRecord createOne(GaoEventRecord record);
/// 由套餐购买服务创建购买事件登记
///
/// @param record 购买事件登记
/// @return 创建后的事件登记
GaoEventRecord createPackagePurchaseRecord(GaoEventRecord record);
void createWithCustomer(GaoCustomer customer);
/// 按任意时间范围查询登记事件记录
@@ -44,6 +67,12 @@ public interface GaoEventRecordService extends BaseService<GaoEventRecord, Strin
/// @return 登记数值合计
Long sumNumberValueByRange(GaoEventRecordPage req);
/// 按选项统计登记记录数量
///
/// @param req 登记记录范围查询请求
/// @return 选项统计列表
List<GaoEventOptionStatView> optionStatByRange(GaoEventRecordPage req);
/// 按时间范围查询客户登记排行榜
///
/// @param page 范围查询请求,不传时间为全部时间
@@ -1,6 +1,9 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import java.util.Collection;
@@ -14,6 +17,12 @@ import java.util.Map;
/// @since 2026-07-27
public interface GaoEventService extends BaseService<GaoEvent, String> {
/// 分页查询未删除的登记事件
///
/// @param page 分页参数
/// @return 登记事件分页结果
PageResult<GaoEvent> pageWithoutDeleted(Page<GaoEvent> page);
Map<String, GaoEvent> mapByIdList(Collection<String> idList);
Map<String, List<String>> mapRoleCodeListByEventIdList(Collection<String> eventIdList);
@@ -24,6 +33,22 @@ public interface GaoEventService extends BaseService<GaoEvent, String> {
/// @return 登记事件列表
List<GaoEvent> listValid(String storeId);
/// 为登记事件附加选项组和选项
///
/// @param eventList 登记事件列表
void attachOptionGroupList(Collection<GaoEvent> eventList);
/// 更新登记事件的选项组配置
///
/// @param eventId 登记事件 ID
/// @param optionGroupList 选项组配置列表
void updateOptionGroupList(String eventId, List<GaoEventOptionGroup> optionGroupList);
/// 删除门店下的全部登记事件及角色关联
///
/// @param storeId 门店 ID
void deleteByStoreId(String storeId);
/// 查询登记事件要求的角色
///
/// @param eventId 登记事件 ID
@@ -0,0 +1,53 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.vo.GaoFundActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundConfirmNoRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoFundReceiveRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundRefundRequest;
import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
/// GAO 通用资金流水服务
///
/// @author Codex
/// @since 2026-08-31
public interface GaoFundRecordService extends BaseService<GaoFundRecord, String> {
/// 按条件查询资金流水
PageResult<GaoFundRecord> pageByQuery(GaoFundRecordPage page);
/// 查询客户套餐资金历史
List<GaoFundRecord> listByCustomerPackageId(String customerPackageId);
/// 登记线下收款
GaoFundRecord receive(GaoFundReceiveRequest request);
/// 核销收款
GaoFundRecord verify(String fundRecordId);
/// 拒绝待核销收款
GaoFundRecord reject(GaoFundActionRequest request);
/// 撤销自己提交的待核销收款
GaoFundRecord cancel(GaoFundActionRequest request);
/// 登记退款
GaoFundRecord refund(GaoFundRefundRequest request);
/// 确认无需退款
GaoFundRecord confirmNoRefund(GaoFundConfirmNoRefundRequest request);
/// 冲正资金流水
GaoFundRecord revoke(GaoFundRevokeRequest request);
/// 统计客户套餐待核销收款数量
///
/// @param bizId 业务 ID
/// @return 待核销收款数量
long countPendingReceiveByBizId(String bizId);
}
@@ -0,0 +1,42 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/// GAO 套餐配置服务
///
/// @author Codex
/// @since 2026-08-31
public interface GaoPackageConfigService extends BaseService<GaoPackageConfig, String> {
/// 按条件查询套餐配置
PageResult<GaoPackageConfig> pageByQuery(GaoPackageConfigPage page);
/// 查询有效可购买套餐
List<GaoPackageConfig> listValid(String storeId);
/// 批量查询套餐配置
///
/// @param idList 套餐配置 ID 列表
/// @return 按套餐配置 ID 映射的配置
Map<String, GaoPackageConfig> mapByIdList(Collection<String> idList);
/// 查询套餐配置产品明细
///
/// @param packageConfigId 套餐配置 ID
/// @return 产品明细列表
List<GaoPackageConfigItem> listItemByPackageConfigId(String packageConfigId);
/// 批量查询套餐配置产品明细
///
/// @param packageConfigIdList 套餐配置 ID 列表
/// @return 按套餐配置 ID 分组的产品明细
Map<String, List<GaoPackageConfigItem>> mapItemListByPackageConfigIdList(Collection<String> packageConfigIdList);
}
@@ -0,0 +1,60 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery;
import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem;
import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryCreateRequest;
import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage;
import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryRevokeRequest;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/// GAO 套餐发放服务
///
/// @author Codex
/// @since 2026-08-31
public interface GaoPackageDeliveryService extends BaseService<GaoPackageDelivery, String> {
/// 按条件查询发放单
PageResult<GaoPackageDelivery> pageByQuery(GaoPackageDeliveryPage page);
/// 查询客户套餐发放历史
List<GaoPackageDelivery> listByCustomerPackageId(String customerPackageId);
/// 批量查询发放明细
///
/// @param deliveryIdList 发放单 ID 列表
/// @return 按发放单 ID 分组的发放明细
Map<String, List<GaoPackageDeliveryItem>> mapItemListByDeliveryIdList(Collection<String> deliveryIdList);
/// 创建待核销发放单
GaoPackageDelivery createDelivery(GaoPackageDeliveryCreateRequest request);
/// 核销发放单
GaoPackageDelivery verify(String deliveryId);
/// 拒绝待核销发放单
GaoPackageDelivery reject(GaoPackageDeliveryActionRequest request);
/// 撤销自己提交的待核销发放单
GaoPackageDelivery cancel(GaoPackageDeliveryActionRequest request);
/// 整单冲正已核销发放单
GaoPackageDelivery revoke(GaoPackageDeliveryRevokeRequest request);
/// 统计客户套餐待核销发放单数量
///
/// @param customerPackageId 客户套餐 ID
/// @return 待核销发放单数量
long countPendingByCustomerPackageId(String customerPackageId);
/// 统计客户套餐已核销正常发放单数量
///
/// @param customerPackageId 客户套餐 ID
/// @return 已核销正常发放单数量
long countVerifiedByCustomerPackageId(String customerPackageId);
}
@@ -1,6 +1,8 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
import com.imyeyu.api.modules.gao.vo.GaoPointAccountPage;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
/// GAO 客户积分账户服务
@@ -9,6 +11,12 @@ import com.imyeyu.spring.service.BaseService;
/// @since 2026-08-11
public interface GaoPointAccountService extends BaseService<GaoPointAccount, String> {
/// 分页查询积分账户
///
/// @param page 查询条件
/// @return 账户分页
PageResult<GaoPointAccount> pageByQuery(GaoPointAccountPage page);
/// 查询客户当前有效积分账户
///
/// @param storeId 门店 ID
@@ -16,6 +24,18 @@ public interface GaoPointAccountService extends BaseService<GaoPointAccount, Str
/// @return 积分账户,不存在时返回 null
GaoPointAccount getByCustomerId(String storeId, String customerId);
/// 按账户 ID 查询积分账户
///
/// @param storeId 门店 ID,可为空
/// @param id 账户 ID
/// @return 积分账户,不存在时返回 null
GaoPointAccount getByIdAndStoreId(String storeId, String id);
/// 删除门店下的全部积分账户
///
/// @param storeId 门店 ID
void deleteByStoreId(String storeId);
/// 获取并锁定客户积分账户,不存在时创建
///
/// @param storeId 门店 ID
@@ -28,8 +48,9 @@ public interface GaoPointAccountService extends BaseService<GaoPointAccount, Str
/// @param accountId 账户 ID
/// @param delta 本次余额变化
/// @param totalEarnedDelta 累计获得积分变化
/// @param totalConsumedDelta 累计消耗积分变化
/// @param totalRevokedDelta 累计撤销积分变化
/// @param totalAdjustedDelta 累计人工调整积分变化
/// @return 更新行数
int updateBalance(String accountId, long delta, long totalEarnedDelta, long totalRevokedDelta, long totalAdjustedDelta);
int updateBalance(String accountId, long delta, long totalEarnedDelta, long totalConsumedDelta, long totalRevokedDelta, long totalAdjustedDelta);
}
@@ -15,18 +15,18 @@ import com.imyeyu.spring.service.BaseService;
/// @since 2026-08-11
public interface GaoPointLedgerService extends BaseService<GaoPointLedger, String> {
/// 写入登记获得积分流水
/// 写入登记规则积分流水
///
/// @param record 登记记录
/// @param rule 命中规则
/// @param pointDelta 积分变化
/// @param sourcePayload 来源快照
/// @return 积分流水
GaoPointLedger earn(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload);
GaoPointLedger applyRule(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload);
/// 写入删除登记记录的撤销流水
///
/// @param originLedger 原始获得积分流水
/// @param originLedger 原始规则积分流水
/// @param record 登记记录
/// @return 撤销流水
GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record);
@@ -25,6 +25,11 @@ public interface GaoPointRuleService extends BaseService<GaoPointRule, String> {
/// @return 规则列表
List<GaoPointRule> listByIdList(List<String> idList);
/// 删除门店下的全部积分规则
///
/// @param storeId 门店 ID
void deleteByStoreId(String storeId);
/// 按客户端提交的顺序更新启用规则优先级
///
/// @param idList 规则 ID 列表
@@ -36,4 +36,9 @@ public interface GaoStoreBusinessDayService extends BaseService<GaoStoreBusiness
/// @param endDateValue 结束日期值,格式为 yyyyMMdd
/// @return 营业日列表
java.util.List<GaoStoreBusinessDay> listByDateRange(String storeId, Integer beginDateValue, Integer endDateValue);
/// 删除门店下的全部营业日
///
/// @param storeId 门店 ID
void deleteByStoreId(String storeId);
}
@@ -0,0 +1,46 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.common.entity.Article;
import com.imyeyu.api.modules.gao.vo.GaoStoreDiaryPage;
import com.imyeyu.spring.bean.PageResult;
/// GAO 门店日记服务
///
/// @author Codex
/// @since 2026-08-31
public interface GaoStoreDiaryService {
/// 分页查询门店日记
///
/// @param page 分页查询条件
/// @return 门店日记分页结果
PageResult<Article> page(GaoStoreDiaryPage page);
/// 查询门店日记详情
///
/// @param id 日记 ID
/// @return 门店日记
Article get(String id);
/// 创建门店日记
///
/// @param article 门店日记
/// @param userId 操作用户 ID
void create(Article article, String userId);
/// 修改门店日记
///
/// @param article 门店日记
/// @param userId 操作用户 ID
void update(Article article, String userId);
/// 删除门店日记
///
/// @param id 日记 ID
void delete(String id);
/// 删除门店下的全部日记
///
/// @param storeId 门店 ID
void deleteByStoreId(String storeId);
}
@@ -0,0 +1,33 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoStore;
/// GAO 门店资源范围服务
///
/// 同时拥有门店管理和店长角色时,按门店管理角色使用全局范围
///
/// @author Codex
/// @since 2026-08-23
public interface GaoStoreScopeService {
/// 判断当前用户是否拥有 GAO 全局范围
///
/// @return `true` 为允许访问全部门店
boolean hasGlobalScope();
/// 将请求门店 ID 解析为当前可用门店 ID
///
/// @param requestedStoreId 请求中的门店 ID,可为空
/// @return 门店 ID,全局范围可为空
String resolveStoreId(String requestedStoreId);
/// 检查门店是否在当前用户可管理范围内
///
/// @param storeId 门店 ID
void checkStoreId(String storeId);
/// 检查门店实体是否在当前用户可管理范围内
///
/// @param store 门店
void checkStore(GaoStore store);
}
@@ -26,6 +26,8 @@ public interface GaoUserInviteService {
/// 使用邀请注册 GAO 用户
///
/// @param data 注册数据
/// @param smsDetailId 短信通知详情 ID
/// @param smsCaptcha 短信验证码
/// @return 注册成功的用户 ID
String register(GaoUserInviteRegisterData data);
String register(GaoUserInviteRegisterData data, String smsDetailId, String smsCaptcha);
}

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