Compare commits

...
24 Commits
Author SHA1 Message Date
timi 425922fd04 Merge pull request 'v1.0.44' (#56) from dev into master
Reviewed-on: #56
2026-09-12 17:42:51 +00:00
Timi b24d6b3a01 v1.0.44
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-13 01:37:51 +08:00
Timi c35132d06b total_earned DESC order for point account 2026-09-13 01:37:41 +08:00
Timi 91072d6d50 auto complete order 2026-09-13 01:25:48 +08:00
timi d65424e5bf Merge pull request 'v1.0.43' (#55) from dev into master
Reviewed-on: #55
2026-09-11 17:26:21 +00:00
Timi f0b72b70e7 v1.0.43
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-12 01:24:03 +08:00
Timi daa5a6aa5e fix off product sku for keep fulfillment 2026-09-12 01:23:47 +08:00
timi 2a35793baf Merge pull request 'v1.0.42' (#54) from dev into master
Reviewed-on: #54
2026-09-11 16:16:04 +00:00
Timi 4a157e9578 v1.0.42
CI / build-deploy (pull_request) Successful in 24s
CI / notify-on-failure (pull_request) Skipped
2026-09-12 00:11:21 +08:00
Timi eabb4c6aa6 fix event record CONTINUOUS_DAYS rank 2026-09-12 00:11:07 +08:00
timi d22df698b6 Merge pull request 'v1.0.41' (#53) from dev into master
Reviewed-on: #53
2026-09-11 15:55:02 +00:00
Timi cb6051cc41 v1.0.41
CI / build-deploy (pull_request) Successful in 59s
CI / notify-on-failure (pull_request) Skipped
2026-09-11 23:53:06 +08:00
Timi 1d1009b121 add customer list for GaoCustomerDailyStateView 2026-09-11 23:52:43 +08:00
Timi 94f3fbb867 add customer age and disease chart 2026-09-11 20:24:09 +08:00
timi 1771bf9909 Merge pull request 'v1.0.40' (#52) from dev into master
Reviewed-on: #52
2026-09-10 16:29:28 +00:00
Timi fc4d55517b v1.0.40
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-11 00:26:14 +08:00
Timi ec72a99d74 response customer pinyin 2026-09-11 00:24:58 +08:00
Timi a431db6e6e fix article detail request method 2026-09-10 20:27:53 +08:00
Timi 9cfb31b015 support pinyin for product 2026-09-10 20:27:32 +08:00
timi 15d69f9361 Merge pull request 'v1.0.39' (#51) from dev into master
Reviewed-on: #51
2026-09-10 06:10:30 +00:00
Timi b000390a75 v1.0.39
CI / build-deploy (pull_request) Successful in 24s
CI / notify-on-failure (pull_request) Skipped
2026-09-10 14:09:49 +08:00
Timi 1b9f92caf5 add setting map 2026-09-10 14:09:38 +08:00
Timi 7b08641acc add customer order map 2026-09-10 14:09:30 +08:00
Timi 66c13062b2 fix notify fail 2026-09-10 12:47:29 +08:00
59 changed files with 909 additions and 201 deletions
+6 -1
View File
@@ -11,7 +11,7 @@
<groupId>com.imyeyu.timiserverapi</groupId>
<artifactId>TimiServerAPI</artifactId>
<version>1.0.38</version>
<version>1.0.44</version>
<packaging>jar</packaging>
<name>TimiServerAPI</name>
<description>imyeyu.com API</description>
@@ -215,6 +215,11 @@
<artifactId>tinypinyin</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>hanlp</artifactId>
<version>portable-1.8.4</version>
</dependency>
<dependency>
<groupId>com.imyeyu.font.icon</groupId>
<artifactId>timi-icon-font</artifactId>
@@ -39,6 +39,7 @@ public class ArticleController {
/// @return 文章分页
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.ARTICLE_READ)
@PostMapping("/list")
public PageResult<Article> list(@RequestBody Page<Article> page) {
return service.page(page);
@@ -50,7 +51,7 @@ public class ArticleController {
/// @return 文章详情
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@PostMapping("/detail")
@RequestMapping("/detail")
public Article detail(@RequestParam String id) {
Article article = service.get(id);
article.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.ARTICLE, article.getId()));
@@ -15,8 +15,12 @@ import lombok.extern.slf4j.Slf4j;
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;
/**
*
*
@@ -26,11 +30,30 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/setting")
@RequestMapping("/setting")
public class SettingController {
private final SettingService service;
/**
* 查询公开配置映射
*
* @param bizType 业务类型
* @param bizId 业务 ID,可为空
* @param code 配置代码列表,可为空
* @return 公开配置映射
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@RequestMapping("/map")
public Map<String, Setting> map(
@RequestParam String bizType,
@RequestParam(required = false) String bizId,
@RequestParam(required = false) List<String> code
) {
return service.mapPublic(bizType, bizId, code);
}
/**
* 查询配置列表
*
@@ -20,11 +20,11 @@ public class Notify extends UUIDEntity {
/** 业务类型 */
private String bizType;
/** 提醒业务类型 */
/** 业务提醒类型 */
private String bizRemindedType;
/** 提醒业务 ID */
private String bizRemindedId;
/** 业务提醒查重键 */
private String bizRemindedKey;
/** 参数 */
private String args;
@@ -2,9 +2,8 @@ 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 org.apache.ibatis.annotations.Select;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -16,9 +15,8 @@ import java.util.List;
*/
public interface NotifyMapper extends BaseMapper<Notify, String> {
/// 查询业务提醒是否已创建
@Select("SELECT EXISTS (SELECT 1 FROM `notify` WHERE `biz_type` = #{bizType} AND `biz_reminded_type` = #{bizRemindedType} AND `biz_reminded_id` = #{bizRemindedId} AND `deleted_at` IS NULL)")
boolean existsByBizReminded(@Param("bizType") String bizType, @Param("bizRemindedType") String bizRemindedType, @Param("bizRemindedId") String bizRemindedId);
/// 原子创建业务提醒
int insertIfAbsent(Notify notify);
/// 分页查询管理端通知
///
@@ -3,6 +3,8 @@ package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.spring.mapper.BaseMapper;
import java.util.List;
/**
* 系统配置
*
@@ -10,4 +12,6 @@ import com.imyeyu.spring.mapper.BaseMapper;
* @since 2021-07-20 22:26
*/
public interface SettingMapper extends BaseMapper<Setting, String> {
List<Setting> selectPublicByQuery(String bizType, String bizId, List<String> codeList);
}
@@ -14,13 +14,10 @@ import com.imyeyu.spring.service.BaseService;
*/
public interface NotifyService extends BaseService<Notify, String> {
/// 查询业务提醒是否已创建
/// 原子创建业务提醒,重复查重键不会创建通知详情
///
/// @param bizType 业务类型
/// @param bizRemindedType 被提醒业务类型
/// @param bizRemindedId 被提醒业务 ID
/// @return true 为已创建
boolean existsByBizReminded(String bizType, String bizRemindedType, String bizRemindedId);
/// @param notify 通知
void createIfAbsent(Notify notify);
/// 分页查询当前用户创建的通知
///
@@ -7,6 +7,7 @@ import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
import java.util.Map;
/**
* 配置服务
@@ -24,6 +25,16 @@ public interface SettingService extends BaseService<Setting, String> {
List<Setting> listByBizId(String bizType, String bizId);
/**
* 查询公开配置映射
*
* @param bizType 业务类型
* @param bizId 业务 ID,可为空
* @param code 配置代码列表,可为空
* @return 公开配置映射
*/
Map<String, Setting> mapPublic(String bizType, String bizId, List<String> code);
default List<Setting> listBySettingSupport(SettingSupport settingSupport) {
String bizType = settingSupport.getClass().getSimpleName().toUpperCase();
return listByBizId(bizType, settingSupport.getId());
@@ -7,9 +7,8 @@ import com.imyeyu.api.modules.common.mapper.NotifyMapper;
import com.imyeyu.api.modules.common.service.NotifyDetailService;
import com.imyeyu.api.modules.common.service.NotifyService;
import com.imyeyu.api.modules.common.vo.NotifyManagementItem;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.system.audit.AuditOperation;
import com.imyeyu.api.modules.system.audit.OperationAudit;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
@@ -17,12 +16,13 @@ import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
/**
* 通知服务实现
@@ -30,13 +30,10 @@ import java.util.List;
* @author 夜雨
* @since 2026-05-13 14:46
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class NotifyServiceImplement extends AbstractEntityService<Notify, String> implements NotifyService {
private final LoggerService loggerService;
private final UserLoginService userLoginService;
private final NotifyDetailService detailService;
private final NotifyMapper mapper;
@@ -46,43 +43,53 @@ public class NotifyServiceImplement extends AbstractEntityService<Notify, String
return mapper;
}
@Override
public boolean existsByBizReminded(String bizType, String bizRemindedType, String bizRemindedId) {
return mapper.existsByBizReminded(bizType, bizRemindedType, bizRemindedId);
}
/// 创建通知并级联创建通知详情
///
/// @param notify 通知
@OperationAudit(AuditOperation.COMMON_NOTIFY_CREATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(Notify notify) {
Logger logger = new Logger(Logger.Module.COMMON, "COMMON_NOTIFY_CREATE");
try {
TimiException.required(notify, "未找到通知");
TimiException.required(notify.getDetailList(), "未找到通知详情列表");
prepareCreate(notify);
super.create(notify);
createDetails(notify);
}
notify.setCreatedBy(TimiJava.defaultIfEmpty(notify.getCreatedBy(), TimiJava.ZERO_UUID));
super.create(notify);
/// 原子创建业务提醒并级联创建通知详情
///
/// @param notify 通知
@OperationAudit(AuditOperation.COMMON_NOTIFY_CREATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void createIfAbsent(Notify notify) {
prepareCreate(notify);
if (mapper.insertIfAbsent(notify) == 0) {
return;
}
createDetails(notify);
}
for (NotifyDetail detail : notify.getDetailList()) {
detail.setNotifyId(notify.getId());
detail.setSendFrom(TimiJava.defaultIfEmpty(detail.getSendFrom(), TimiJava.ZERO_UUID));
detailService.create(detail);
}
logger.setLevel(Logger.Level.INFO);
logger.setResult(notify.getId());
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("创建通知失败", e);
throw new TimiException(TimiCode.ERROR, "创建通知失败", e);
} finally {
loggerService.create(logger);
/// 校验并补全通知创建字段
///
/// @param notify 通知
private void prepareCreate(Notify notify) {
TimiException.required(notify, "未找到通知");
TimiException.required(notify.getDetailList(), "未找到通知详情列表");
notify.setId(TimiJava.defaultIfEmpty(notify.getId(), UUID.randomUUID().toString()));
notify.setCreatedBy(TimiJava.defaultIfEmpty(notify.getCreatedBy(), TimiJava.ZERO_UUID));
notify.setCreatedAt(TimiJava.defaultIfNull(notify.getCreatedAt(), Time.now()));
notify.setUpdatedAt(null);
notify.setDeletedAt(null);
}
/// 创建通知详情
///
/// @param notify 通知
private void createDetails(Notify notify) {
for (NotifyDetail detail : notify.getDetailList()) {
detail.setNotifyId(notify.getId());
detail.setSendFrom(TimiJava.defaultIfEmpty(detail.getSendFrom(), TimiJava.ZERO_UUID));
detailService.create(detail);
}
}
@@ -120,6 +127,8 @@ public class NotifyServiceImplement extends AbstractEntityService<Notify, String
/// @param notifyId 通知 ID
/// @param userId 用户 ID
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
@OperationAudit(AuditOperation.COMMON_NOTIFY_DELETE)
public void deleteManagement(String notifyId, String userId) {
getManagement(notifyId, userId);
delete(notifyId);
@@ -130,6 +139,7 @@ public class NotifyServiceImplement extends AbstractEntityService<Notify, String
/// @param notifyId 通知 ID
/// @param userId 用户 ID
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
public void cancelManagement(String notifyId, String userId) {
getManagement(notifyId, userId);
cancel(notifyId);
@@ -138,32 +148,17 @@ public class NotifyServiceImplement extends AbstractEntityService<Notify, String
/// 删除通知并级联软删除通知详情
///
/// @param notifyId 通知 ID
@OperationAudit(AuditOperation.COMMON_NOTIFY_DELETE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(String notifyId) {
Logger logger = new Logger(Logger.Module.COMMON, "COMMON_NOTIFY_DELETE");
try {
TimiException.required(notifyId, "未找到通知 ID");
TimiException.required(get(notifyId), "未找到通知");
List<NotifyDetail> detailList = detailService.listByNotifyId(notifyId);
for (NotifyDetail detail : detailList) {
detailService.delete(detail.getId());
}
super.delete(notifyId);
logger.setLevel(Logger.Level.INFO);
logger.setResult(notifyId);
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("删除通知失败", e);
throw new TimiException(TimiCode.ERROR, "删除通知失败", e);
} finally {
loggerService.create(logger);
TimiException.required(notifyId, "未找到通知 ID");
TimiException.required(get(notifyId), "未找到通知");
List<NotifyDetail> detailList = detailService.listByNotifyId(notifyId);
for (NotifyDetail detail : detailList) {
detailService.delete(detail.getId());
}
super.delete(notifyId);
}
/// 取消通知中尚未成功发送的通知详情
@@ -17,6 +17,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 系统配置服务实现
@@ -53,6 +55,12 @@ public class SettingServiceImplement extends AbstractEntityService<Setting, Stri
return mapper.selectAllByExample(example);
}
@Override
public Map<String, Setting> mapPublic(String bizType, String bizId, List<String> code) {
TimiException.required(bizType, "not found bizType");
return mapper.selectPublicByQuery(bizType, bizId, code).stream().collect(Collectors.toMap(Setting::getCode, Function.identity()));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void updateByBizId(String bizType, String bizId, List<Setting> list) {
@@ -5,6 +5,8 @@ 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.GaoCustomerDiseaseWordReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.java.bean.timi.TimiException;
@@ -51,4 +53,15 @@ public class GaoCustomerChartController {
req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
return service.eventRecordCalendar(req);
}
/// 查询客户病症词频
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/disease/word")
public List<GaoCustomerDiseaseWordView> customerDiseaseWord(@RequestBody GaoCustomerDiseaseWordReq req) {
TimiException.required(req, "not found req");
req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId()));
return service.customerDiseaseWord(req);
}
}
@@ -4,6 +4,7 @@ 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.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
@@ -54,6 +55,15 @@ public class GaoStoreChartController {
return service.customerGender(storeScopeService.resolveStoreId(storeId));
}
/// 查询客户年龄分布
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/age")
public List<GaoCustomerAgeStatView> customerAge(@RequestParam String storeId) {
return service.customerAge(storeScopeService.resolveStoreId(storeId));
}
/// 查询登记事件每日统计,可同时支撑登记趋势和事件结构图表
@AOPLog
@RequestRateLimit
@@ -6,7 +6,6 @@ import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.TagApply;
import com.imyeyu.api.modules.user.bean.Gender;
import com.imyeyu.api.modules.user.entity.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.annotation.table.Column;
import com.imyeyu.spring.annotation.table.Transient;
@@ -62,15 +61,12 @@ public class GaoCustomer extends UUIDEntity {
private String name;
@Column("name_pinyin_full")
@JsonIgnore
private String namePinyinFull;
@Column("name_pinyin_initial")
@JsonIgnore
private String namePinyinInitial;
@Column("name_pinyin_mixed")
@JsonIgnore
private String namePinyinMixed;
/// 性别
@@ -1,6 +1,8 @@
package com.imyeyu.api.modules.gao.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.spring.annotation.table.Column;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
@@ -61,6 +63,18 @@ public class GaoProduct extends UUIDEntity {
/// 产品名称
private String name;
@Column("name_pinyin_full")
@JsonIgnore
private String namePinyinFull;
@Column("name_pinyin_initial")
@JsonIgnore
private String namePinyinInitial;
@Column("name_pinyin_mixed")
@JsonIgnore
private String namePinyinMixed;
/// 产品类型
private ProductType productType;
@@ -1,7 +1,10 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateCustomerView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseTextView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPage;
import com.imyeyu.spring.mapper.BaseMapper;
@@ -27,6 +30,14 @@ public interface GaoCustomerMapper extends BaseMapper<GaoCustomer, String> {
/// @return 每日新增客户数
List<GaoCustomerDailyStateView> stateDailyNewCustomer(@Param("storeId") String storeId, @Param("beginCreatedAt") Long beginCreatedAt, @Param("endCreatedAt") Long endCreatedAt);
/// 查询每日新增客户摘要
///
/// @param storeId 门店 ID
/// @param beginCreatedAt 开始创建时间
/// @param endCreatedAt 结束创建时间
/// @return 每日新增客户摘要
List<GaoCustomerDailyStateCustomerView> selectDailyNewCustomer(@Param("storeId") String storeId, @Param("beginCreatedAt") Long beginCreatedAt, @Param("endCreatedAt") Long endCreatedAt);
/// 统计开始时间前客户总数
///
/// @param beginCreatedAt 开始创建时间
@@ -38,6 +49,18 @@ public interface GaoCustomerMapper extends BaseMapper<GaoCustomer, String> {
/// @return 性别统计列表
List<GaoCustomerGenderStatView> stateGender(@Param("storeId") String storeId);
/// 统计客户年龄分布
///
/// @param storeId 门店 ID
/// @return 客户年龄列表
List<GaoCustomerAgeStatView> stateAge(@Param("storeId") String storeId);
/// 查询客户病症原始文本
///
/// @param storeId 门店 ID
/// @return 客户备注和病症
List<GaoCustomerDiseaseTextView> selectDiseaseTextByStoreId(@Param("storeId") String storeId);
/// 查询客户总数
///
/// @param page 查询条件
@@ -55,5 +78,5 @@ public interface GaoCustomerMapper extends BaseMapper<GaoCustomer, String> {
/// @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);
void updateInvitedCountDelta(String customerId, Long delta);
}
@@ -1,6 +1,8 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
@@ -17,4 +19,7 @@ public interface GaoCustomerChartService {
/// 查询客户登记日历记录
List<GaoEventRecordCalendarView> eventRecordCalendar(GaoCustomerChartReq req);
/// 查询客户病症词频
List<GaoCustomerDiseaseWordView> customerDiseaseWord(GaoCustomerDiseaseWordReq req);
}
@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPage;
@@ -87,4 +88,10 @@ public interface GaoCustomerService extends BaseService<GaoCustomer, String> {
/// @param storeId 门店 ID
/// @return 性别统计列表
List<GaoCustomerGenderStatView> stateGender(String storeId);
/// 统计客户年龄分布
///
/// @param storeId 门店 ID
/// @return 客户年龄列表
List<GaoCustomerAgeStatView> stateAge(String storeId);
}
@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.vo.GaoOrderStraightThroughRequest;
/// GAO 订单创建直通服务
///
/// @author Codex
/// @since 2026-09-13
public interface GaoOrderStraightThroughService {
/// 在订单创建事务内完成可选的全额收款和全部履约
///
/// @param order 销售订单
/// @param straightThrough 直通请求,空为不直通
void complete(GaoSalesOrder order, GaoOrderStraightThroughRequest straightThrough);
}
@@ -33,6 +33,14 @@ public interface GaoProductSkuService extends BaseService<GaoProductSku, String>
/// 查询并校验可销售 SKU 单位
GaoProductSkuUnitSelection getSellableSkuUnit(String storeId, String skuId, String skuUnitId);
/// 查询并校验可用于历史订单履约的 SKU 单位
///
/// @param storeId 门店 ID
/// @param skuId SKU ID
/// @param skuUnitId SKU 单位 ID
/// @return SKU 单位选择结果
GaoProductSkuUnitSelection getFulfillableSkuUnit(String storeId, String skuId, String skuUnitId);
/// 查询并校验 SKU 单位
GaoProductSkuUnitSelection getSkuUnit(String storeId, String skuId, String skuUnitId, boolean purchase);
@@ -46,6 +46,9 @@ public interface GaoProductStockService {
/// 为履约预占库存
boolean hasSufficientForFulfillment(String storeId, List<GaoStockChangeItem> items);
/// 获取履约库存缺口提示,无缺口时返回 null
String getFulfillmentStockShortageMessage(String storeId, List<GaoStockChangeItem> items);
/// 为履约预占库存
List<GaoProductStock> reserveForFulfillment(String storeId, String bizId, List<GaoStockChangeItem> items, String requestNo, String operatorUserId);
@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
@@ -21,6 +22,9 @@ public interface GaoStoreChartService {
/// 查询客户性别统计
List<GaoCustomerGenderStatView> customerGender(String storeId);
/// 查询客户年龄分布
List<GaoCustomerAgeStatView> customerAge(String storeId);
/// 查询登记事件每日统计
List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoStoreChartReq req);
@@ -1,15 +1,23 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.hankcs.hanlp.seg.common.Term;
import com.hankcs.hanlp.tokenizer.StandardTokenizer;
import com.imyeyu.api.modules.gao.mapper.GaoCustomerMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerChartService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseTextView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseWordView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/// 客户报表服务实现
///
@@ -20,6 +28,7 @@ import java.util.List;
public class GaoCustomerChartServiceImplement implements GaoCustomerChartService {
private final GaoEventRecordService eventRecordService;
private final GaoCustomerMapper customerMapper;
@Override
public List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoCustomerChartReq req) {
@@ -33,6 +42,42 @@ public class GaoCustomerChartServiceImplement implements GaoCustomerChartService
return eventRecordService.calendarByCustomerRange(req);
}
@Override
public List<GaoCustomerDiseaseWordView> customerDiseaseWord(GaoCustomerDiseaseWordReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getStoreId(), "not found req.storeId");
int topN = normalizeTopN(req.getTopN());
Map<String, Long> wordCountMap = new HashMap<>();
for (GaoCustomerDiseaseTextView customer : customerMapper.selectDiseaseTextByStoreId(req.getStoreId())) {
countNounWord(customer.getDisease(), wordCountMap);
countNounWord(customer.getRemark(), wordCountMap);
}
return wordCountMap.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed().thenComparing(Map.Entry.comparingByKey()))
.limit(topN)
.map(item -> new GaoCustomerDiseaseWordView(item.getKey(), item.getValue()))
.toList();
}
private void countNounWord(String text, Map<String, Long> wordCountMap) {
if (text == null || text.isBlank()) {
return;
}
for (Term term : StandardTokenizer.segment(text)) {
if (term.nature != null && term.nature.toString().startsWith("n")) {
wordCountMap.merge(term.word, 1L, Long::sum);
}
}
}
private int normalizeTopN(Integer topN) {
if (topN == null) {
return 60;
}
return Math.clamp(topN, 1, 100);
}
private void checkRangeReq(GaoCustomerChartReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getStoreId(), "not found req.storeId");
@@ -13,6 +13,7 @@ import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem;
import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageMapper;
import com.imyeyu.api.modules.gao.service.GaoSalesOrderService;
import com.imyeyu.api.modules.gao.service.GaoOrderStraightThroughService;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService;
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
@@ -67,6 +68,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
private final GaoProductSkuService skuService;
private final GaoCustomerService customerService;
private final GaoSalesOrderService salesOrderService;
private final GaoOrderStraightThroughService straightThroughService;
private final GaoStoreScopeService storeScopeService;
private final AttachmentService attachmentService;
private final UserLoginService userLoginService;
@@ -168,6 +170,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
if (existingOrder != null) {
GaoCustomerPackage existingPackage = getBySalesOrderId(existingOrder.getId());
verifyPurchaseIdempotency(existingOrder, existingPackage, request);
straightThroughService.complete(salesOrderService.detail(existingOrder.getId()), request.getStraightThrough());
return existingPackage;
}
Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now());
@@ -205,6 +208,8 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
result.setStandardAmountSnapshot(config.getStandardAmount());
result.setRemark(request.getRemark());
super.create(result);
straightThroughService.complete(salesOrder, request.getStraightThrough());
salesOrder = salesOrderService.detail(salesOrder.getId());
result.setSalesOrder(salesOrder);
result.setItemList(salesOrder.getItemList());
result.setStatus(salesOrder.getStatus());
@@ -14,7 +14,9 @@ import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
import com.imyeyu.api.modules.gao.util.CustomerXSSFBuilder;
import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateCustomerView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStateReq;
@@ -410,6 +412,10 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
for (GaoCustomerDailyStateView view : mapper.stateDailyNewCustomer(req.getStoreId(), beginCreatedAt, endCreatedAt)) {
newCustomerCountMap.put(view.getDateValue(), TimiJava.defaultIfNull(view.getNewCustomerCount(), 0L));
}
Map<Integer, List<GaoCustomerDailyStateCustomerView>> customerListMap = new HashMap<>();
for (GaoCustomerDailyStateCustomerView customer : mapper.selectDailyNewCustomer(req.getStoreId(), beginCreatedAt, endCreatedAt)) {
customerListMap.computeIfAbsent(customer.getDateValue(), key -> new ArrayList<>()).add(customer);
}
long totalCustomerCount = TimiJava.defaultIfNull(mapper.countCreatedBefore(req.getStoreId(), beginCreatedAt), 0L);
List<GaoCustomerDailyStateView> list = new ArrayList<>();
for (LocalDate date = beginDate; !date.isAfter(endDate); date = date.plusDays(1)) {
@@ -420,6 +426,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
view.setDateValue(dateValue);
view.setNewCustomerCount(newCustomerCount);
view.setTotalCustomerCount(totalCustomerCount);
view.setCustomerList(customerListMap.getOrDefault(dateValue, List.of()));
list.add(view);
}
return list;
@@ -431,6 +438,12 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
return mapper.stateGender(storeId);
}
@Override
public List<GaoCustomerAgeStatView> stateAge(String storeId) {
TimiException.required(storeId, "not found storeId");
return mapper.stateAge(storeId);
}
private void changeInvitedCount(String customerId, long delta) {
if (TimiJava.isEmpty(customerId) || delta == 0) {
return;
@@ -207,7 +207,8 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
applySkuSpecSnapshots(stockItems, fulfillmentItemList);
TimiException.requiredTrue(!stockItems.isEmpty(), "履约单没有需要占用的实物库存");
TimiException.requiredTrue(productStockService.hasSufficientForFulfillment(order.getStoreId(), stockItems), "库存仍然不足,不能准备履约");
String stockShortageMessage = productStockService.getFulfillmentStockShortageMessage(order.getStoreId(), stockItems);
TimiException.requiredTrue(stockShortageMessage == null, stockShortageMessage);
String operatorUserId = userLoginService.getRequireLoginUserId();
productStockService.reserveForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems,
stockRequestNo(fulfillment, "RESERVE"), operatorUserId);
@@ -450,12 +451,32 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoSalesOrderItem.ItemType expectedItemType = order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT;
TimiException.requiredTrue(source.getItemType() == expectedItemType, "订单项类型与订单类型不匹配");
GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(order.getStoreId(), source.getSkuId(), requestItem.getSkuUnitId());
if (requestItem.getSkuUnitId().equals(source.getSkuUnitId())) {
result.add(toSourceByOrderSnapshot(order, source, requestItem.getQuantity(), checkAvailable));
continue;
}
GaoProductSkuUnitSelection selection = skuService.getFulfillableSkuUnit(order.getStoreId(), source.getSkuId(), requestItem.getSkuUnitId());
result.add(toSource(order, source, selection.getProduct(), selection.getSku(), selection.getUnit(), requestItem.getQuantity(), checkAvailable));
}
return result;
}
private SourceEntry toSourceByOrderSnapshot(GaoSalesOrder order, GaoSalesOrderItem source, BigDecimal quantity, boolean checkAvailable) {
GaoProductSku sku = skuService.get(source.getSkuId());
TimiException.required(sku, "履约来源 SKU 不存在");
GaoProduct product = productService.get(sku.getProductId());
TimiException.required(product, "履约来源产品不存在");
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
quantity = requiredPositiveQuantity(quantity, "履约数量");
if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) {
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
}
BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(source.getBaseQuantityPerUnitSnapshot());
BigDecimal baseQuantity = toBaseQuantity(quantity, baseQuantityPerUnit, "履约基础数量最多保留三位小数");
return buildSource(order, source, source.getSkuUnitId(), source.getUnitSnapshot(), baseQuantityPerUnit, quantity,
baseQuantity, product, sku, checkAvailable);
}
private List<SourceEntry> loadPersistedSources(GaoSalesOrder order, List<GaoOrderFulfillmentItem> itemList) {
TimiException.requiredTrue(itemList != null && !itemList.isEmpty(), "履约单没有明细");
List<GaoOrderFulfillmentItem> sortedItemList = new ArrayList<>(itemList);
@@ -103,20 +103,17 @@ public class GaoOrderReminderServiceImplement implements GaoOrderReminderService
log.warn("GAO 订单提醒跳过不存在的店长用户: storeId[{}], userId[{}]", storeId, manager.getUserId());
continue;
}
String remindedId = "%s:%s:%s:%s".formatted(LocalDate.now(ZoneId.of(ZONE_ID)), storeId, manager.getUserId(), reminderType);
if (notifyService.existsByBizReminded(NOTIFY_BIZ_TYPE, reminderType, remindedId)) {
continue;
}
String remindedKey = "%s:%s:%s:%s".formatted(LocalDate.now(ZoneId.of(ZONE_ID)), storeId, manager.getUserId(), reminderType);
long sendAt = Time.now();
Notify notify = new Notify();
notify.setBizType(NOTIFY_BIZ_TYPE);
notify.setBizRemindedType(reminderType);
notify.setBizRemindedId(remindedId);
notify.setBizRemindedKey(remindedKey);
notify.setDetailList(List.of(
createInternalDetail(manager.getUserId(), content, sendAt),
createSmsDetail(user.getPhoneNo(), customerNames, action, content, sendAt)
));
notifyService.create(notify);
notifyService.createIfAbsent(notify);
} catch (Exception e) {
log.error("GAO 订单提醒发送失败: storeId[{}], userId[{}], reminderType[{}]", storeId, manager.getUserId(), reminderType, e);
}
@@ -0,0 +1,129 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem;
import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService;
import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService;
import com.imyeyu.api.modules.gao.service.GaoOrderStraightThroughService;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentCreateRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentActionRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentItemRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentReceiveRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderStraightThroughRequest;
import com.imyeyu.api.modules.user.service.PermissionChecker;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
/// GAO 订单创建直通服务实现
///
/// @author Codex
/// @since 2026-09-13
@Service
@RequiredArgsConstructor
public class GaoOrderStraightThroughServiceImplement implements GaoOrderStraightThroughService {
private final GaoOrderPaymentService paymentService;
private final GaoOrderFulfillmentService fulfillmentService;
private final PermissionChecker permissionChecker;
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
public void complete(GaoSalesOrder order, GaoOrderStraightThroughRequest straightThrough) {
if (straightThrough == null) {
return;
}
boolean receiveFullPayment = Boolean.TRUE.equals(straightThrough.getReceiveFullPayment());
boolean fulfillAll = Boolean.TRUE.equals(straightThrough.getFulfillAll());
if (!receiveFullPayment && !fulfillAll) {
return;
}
TimiException.required(order, "not found sales order");
TimiException.required(order.getId(), "not found salesOrderId");
TimiException.requiredTrue(!fulfillAll || receiveFullPayment || order.getSaleAmount() == 0,
"全部履约直通必须同时直接全额收款");
checkPermissions(order.getOrderType(), receiveFullPayment, fulfillAll);
if (receiveFullPayment) {
settle(order, straightThrough);
}
if (fulfillAll) {
fulfill(order, straightThrough);
}
}
private void checkPermissions(GaoSalesOrder.OrderType orderType, boolean receiveFullPayment, boolean fulfillAll) {
List<String> permissionCodeList = new ArrayList<>();
permissionCodeList.add(orderType == GaoSalesOrder.OrderType.PACKAGE
? GaoPermissionCode.CUSTOMER_PACKAGE_CREATE.getValue() : GaoPermissionCode.SALES_ORDER_CREATE.getValue());
if (receiveFullPayment) {
permissionCodeList.add(GaoPermissionCode.FUND_RECEIVE_CREATE.getValue());
permissionCodeList.add(GaoPermissionCode.FUND_RECEIVE_VERIFY.getValue());
}
if (fulfillAll) {
permissionCodeList.add(GaoPermissionCode.ORDER_FULFILLMENT_CREATE.getValue());
permissionCodeList.add(GaoPermissionCode.ORDER_FULFILLMENT_VERIFY.getValue());
}
permissionChecker.checkAll(ModuleCode.GAO, permissionCodeList.toArray(String[]::new));
}
private void settle(GaoSalesOrder order, GaoOrderStraightThroughRequest straightThrough) {
if (order.getSaleAmount() == null || order.getSaleAmount() == 0) {
return;
}
GaoOrderPaymentReceiveRequest paymentRequest = new GaoOrderPaymentReceiveRequest();
paymentRequest.setSalesOrderId(order.getId());
paymentRequest.setRequestNo("ORDER-%s-FULL-PAY".formatted(order.getId()));
paymentRequest.setAmount(order.getSaleAmount());
paymentRequest.setOccurredAt(TimiJava.defaultIfNull(straightThrough.getPaymentOccurredAt(), order.getPurchasedAt()));
paymentRequest.setAttachmentList(straightThrough.getPaymentAttachmentList());
paymentRequest.setRemark(TimiJava.defaultIfEmpty(straightThrough.getPaymentRemark(), "随订单创建直接全额收款"));
GaoFundRecord record = paymentService.receive(paymentRequest);
if (record.getStatus() == GaoFundRecord.Status.PENDING) {
paymentService.verify(record.getId());
return;
}
TimiException.requiredTrue(record.getStatus() == GaoFundRecord.Status.VERIFIED, "直通收款记录不是已核销状态");
}
private void fulfill(GaoSalesOrder order, GaoOrderStraightThroughRequest straightThrough) {
List<GaoSalesOrderItem> orderItemList = order.getItemList();
TimiException.requiredTrue(orderItemList != null && !orderItemList.isEmpty(), "销售订单没有可履约明细");
List<GaoOrderFulfillmentItemRequest> itemList = orderItemList.stream().map(item -> {
GaoOrderFulfillmentItemRequest result = new GaoOrderFulfillmentItemRequest();
result.setSourceOrderItemId(item.getId());
result.setSkuUnitId(item.getSkuUnitId());
result.setQuantity(item.getQuantity());
return result;
}).toList();
GaoOrderFulfillmentCreateRequest fulfillmentRequest = new GaoOrderFulfillmentCreateRequest();
fulfillmentRequest.setSalesOrderId(order.getId());
fulfillmentRequest.setRequestNo("ORDER-%s-FULL-FULFILL".formatted(order.getId()));
fulfillmentRequest.setDeliveredAt(TimiJava.defaultIfNull(straightThrough.getDeliveredAt(), order.getPurchasedAt()));
fulfillmentRequest.setItemList(itemList);
fulfillmentRequest.setAttachmentList(straightThrough.getFulfillmentAttachmentList());
fulfillmentRequest.setRemark(TimiJava.defaultIfEmpty(straightThrough.getFulfillmentRemark(), "随订单创建直接全部履约"));
GaoOrderFulfillment fulfillment = fulfillmentService.create(fulfillmentRequest);
if (fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING) {
fulfillmentService.verify(actionRequest(fulfillment.getId()));
return;
}
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.VERIFIED,
fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK ? "库存不足,不能全部履约直通" : "直通履约单不是已核销状态");
}
private GaoOrderFulfillmentActionRequest actionRequest(String fulfillmentId) {
GaoOrderFulfillmentActionRequest request = new GaoOrderFulfillmentActionRequest();
request.setFulfillmentId(fulfillmentId);
return request;
}
}
@@ -17,6 +17,7 @@ import com.imyeyu.api.modules.gao.vo.GaoProductPage;
import com.imyeyu.api.modules.system.audit.AuditOperation;
import com.imyeyu.api.modules.system.audit.OperationAudit;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.util.PinyinUtil;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
@@ -166,6 +167,7 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
TimiException.required(product.getStoreId(), "not found product.storeId");
product.setCode(requiredText(product.getCode(), "产品编码不能为空"));
product.setName(requiredText(product.getName(), "产品名称不能为空"));
fillPinyin(product);
product.setProductType(TimiJava.defaultIfNull(product.getProductType(), GaoProduct.ProductType.GOODS));
product.setStockEnabled(TimiJava.defaultIfNull(product.getStockEnabled(), product.getProductType() != GaoProduct.ProductType.SERVICE));
TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !product.getStockEnabled(), "SERVICE 产品不能启用库存");
@@ -261,4 +263,14 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
TimiException.requiredTrue(!result.isEmpty(), message);
return result;
}
/// 填充产品名称拼音搜索字段
///
/// @param product 产品
private void fillPinyin(GaoProduct product) {
String name = product.getName();
product.setNamePinyinFull(PinyinUtil.full(name));
product.setNamePinyinInitial(PinyinUtil.initial(name));
product.setNamePinyinMixed(PinyinUtil.mixed(name));
}
}
@@ -109,6 +109,22 @@ public class GaoProductSkuServiceImplement extends AbstractEntityService<GaoProd
return getSkuUnit(storeId, skuId, skuUnitId, false);
}
@Override
public GaoProductSkuUnitSelection getFulfillableSkuUnit(String storeId, String skuId, String skuUnitId) {
TimiException.required(storeId, "门店 ID 不能为空");
TimiException.required(skuId, "SKU ID 不能为空");
TimiException.required(skuUnitId, "SKU 单位 ID 不能为空");
GaoProductSku sku = mapper.selectForUpdate(skuId);
TimiException.required(sku, "SKU 不存在");
TimiException.requiredTrue(storeId.equals(sku.getStoreId()), "SKU 不属于当前门店");
GaoProduct product = productServiceProvider.getObject().get(sku.getProductId());
TimiException.required(product, "产品不存在");
GaoProductSkuUnit unit = unitMapper.selectForUpdate(skuUnitId);
TimiException.required(unit, "SKU 单位不存在");
TimiException.requiredTrue(skuId.equals(unit.getSkuId()), "SKU 单位不属于当前 SKU");
return new GaoProductSkuUnitSelection(product, sku, unit);
}
@Override
public GaoProductSkuUnitSelection getSkuUnit(String storeId, String skuId, String skuUnitId, boolean purchase) {
TimiException.required(storeId, "not found storeId");
@@ -354,11 +370,8 @@ public class GaoProductSkuServiceImplement extends AbstractEntityService<GaoProd
TimiException.requiredTrue(1 == unitMapper.updateUnit(unit), "SKU 单位更新失败");
}
}
for (GaoProductSkuUnit existing : existingUnitList) {
if (!retainedIdSet.contains(existing.getId())) {
unitMapper.delete(existing.getId());
}
}
TimiException.requiredTrue(existingUnitList.stream().allMatch(existing -> retainedIdSet.contains(existing.getId())),
"已保存的 SKU 单位不能删除,请关闭允许销售和允许采购");
}
private void saveSpecValues(String skuId, List<String> specValueIdList) {
@@ -156,19 +156,30 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
public boolean hasSufficientForFulfillment(String storeId, List<GaoStockChangeItem> items) {
return getFulfillmentStockShortageMessage(storeId, items) == null;
}
@Override
@Transactional(TimiServerDBConfig.ROLLBACKER)
public String getFulfillmentStockShortageMessage(String storeId, List<GaoStockChangeItem> items) {
TimiException.required(storeId, "not found storeId");
if (items == null || items.isEmpty()) {
return true;
return null;
}
List<GaoStockChangeItem> sortedItems = new ArrayList<>(items);
sortedItems.sort(Comparator.comparing(GaoStockChangeItem::getSkuId));
List<String> shortageList = new ArrayList<>();
for (GaoStockChangeItem item : sortedItems) {
GaoProductStock stock = stockMapper.selectForUpdate(storeId, item.getSkuId());
if (stock == null || decimal(stock.getOnHandQuantity()).subtract(decimal(stock.getReservedQuantity())).compareTo(item.getBaseQuantity()) < 0) {
return false;
BigDecimal availableQuantity = stock == null ? ZERO : decimal(stock.getOnHandQuantity()).subtract(decimal(stock.getReservedQuantity()));
BigDecimal shortageQuantity = item.getBaseQuantity().subtract(availableQuantity);
if (0 < shortageQuantity.signum()) {
shortageList.add("%s(%s)可用 %s %s,缺少 %s %s".formatted(
item.getProductNameSnapshot(), item.getSkuNameSnapshot(), availableQuantity.toPlainString(), item.getUnitSnapshot(),
shortageQuantity.toPlainString(), item.getUnitSnapshot()));
}
}
return true;
return shortageList.isEmpty() ? null : "库存不足:" + String.join(";", shortageList);
}
@Override
@@ -21,6 +21,7 @@ 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.GaoOrderFulfillmentService;
import com.imyeyu.api.modules.gao.service.GaoOrderStraightThroughService;
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductSkuService;
@@ -80,6 +81,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
private final GaoSalesOrderItemMapper itemMapper;
private final GaoFundRecordService fundRecordService;
private final ObjectProvider<GaoOrderFulfillmentService> fulfillmentServiceProvider;
private final ObjectProvider<GaoOrderStraightThroughService> straightThroughServiceProvider;
private final GaoCustomerService customerService;
private final GaoProductService productService;
private final GaoProductSkuService skuService;
@@ -143,7 +145,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoSalesOrder createDirectProductOrder(GaoDirectProductOrderCreateRequest request) {
return createDirectInternal(request);
GaoSalesOrder order = createDirectInternal(request);
straightThroughServiceProvider.getObject().complete(order, request.getStraightThrough());
return detail(order.getId());
}
@OperationAudit(AuditOperation.GAO_SALES_ORDER_UPDATE)
@@ -3,6 +3,7 @@ package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreChartService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStateReq;
@@ -43,6 +44,12 @@ public class GaoStoreChartServiceImplement implements GaoStoreChartService {
return customerService.stateGender(storeId);
}
@Override
public List<GaoCustomerAgeStatView> customerAge(String storeId) {
TimiException.required(storeId, "not found storeId");
return customerService.stateAge(storeId);
}
@Override
public List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoStoreChartReq req) {
checkRangeReq(req);
@@ -0,0 +1,18 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 客户年龄统计视图
///
/// @author Codex
/// @since 2026-09-11
@Data
public class GaoCustomerAgeStatView {
/// 客户年龄
private Long age;
/// 该年龄的客户数量
private Long count;
}
@@ -0,0 +1,20 @@
package com.imyeyu.api.modules.gao.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
///
/// 客户每日统计中的客户摘要
///
/// @author Codex
/// @since 2026-09-11
@Data
public class GaoCustomerDailyStateCustomerView {
/// 用于服务层按日期分组,不返回给前端
@JsonIgnore
private Integer dateValue;
/// 客户姓名
private String name;
}
@@ -2,6 +2,8 @@ package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
import java.util.List;
///
/// 客户每日统计视图
///
@@ -18,4 +20,7 @@ public class GaoCustomerDailyStateView {
/// 截止当日客户总数
private Long totalCustomerCount;
/// 当日新增客户列表
private List<GaoCustomerDailyStateCustomerView> customerList;
}
@@ -0,0 +1,19 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
/// 客户病症原始文本
///
/// 只用于后端分词,不对外返回。
///
/// @author Codex
/// @since 2026-09-11
@Data
public class GaoCustomerDiseaseTextView {
/// 备注
private String remark;
/// 病症
private String disease;
}
@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
/// 客户病症词频查询请求
///
/// @author Codex
/// @since 2026-09-11
@Data
public class GaoCustomerDiseaseWordReq {
/// 门店 ID
private String storeId;
/// 返回词语数量
private Integer topN;
}
@@ -0,0 +1,19 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
/// 客户病症词频
///
/// @author Codex
/// @since 2026-09-11
@Data
@AllArgsConstructor
public class GaoCustomerDiseaseWordView {
/// 词语
private String word;
/// 词语出现次数
private Long value;
}
@@ -33,6 +33,9 @@ public class GaoCustomerPackagePurchaseRequest {
/// 购买照片
private List<Attachment> attachmentList;
/// 创建订单后的直通处理
private GaoOrderStraightThroughRequest straightThrough;
/// 备注
private String remark;
}
@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.gao.vo;
import com.imyeyu.java.bean.BasePage;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.spring.bean.Page;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -12,7 +13,7 @@ import java.util.List;
/// @since 2026-08-13
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoCustomerPage extends BasePage {
public class GaoCustomerPage extends Page<GaoCustomer> {
/// 门店 ID
private String storeId;
@@ -22,4 +23,10 @@ public class GaoCustomerPage extends BasePage {
/// 标签 ID 列表,客户必须同时拥有列表内全部标签
private List<String> tagIdList;
/// 录入时间起始,包含,Unix 毫秒时间戳
private Long beginCreatedAt;
/// 录入时间结束,包含,Unix 毫秒时间戳
private Long endCreatedAt;
}
@@ -30,6 +30,9 @@ public class GaoDirectProductOrderCreateRequest {
/// 购买照片
private List<Attachment> attachmentList;
/// 创建订单后的直通处理
private GaoOrderStraightThroughRequest straightThrough;
/// 备注
private String remark;
}
@@ -0,0 +1,38 @@
package com.imyeyu.api.modules.gao.vo;
import com.imyeyu.api.modules.common.entity.Attachment;
import lombok.Data;
import java.util.List;
/// GAO 订单创建直通请求
///
/// @author Codex
/// @since 2026-09-13
@Data
public class GaoOrderStraightThroughRequest {
/// true 为直接全额收款并核销
private Boolean receiveFullPayment;
/// 实际收款时间,空为订单下单时间
private Long paymentOccurredAt;
/// 收款凭证照片
private List<Attachment> paymentAttachmentList;
/// 收款备注
private String paymentRemark;
/// true 为直接全部履约并核销
private Boolean fulfillAll;
/// 实际履约时间,空为订单下单时间
private Long deliveredAt;
/// 履约凭证照片
private List<Attachment> fulfillmentAttachmentList;
/// 履约备注
private String fulfillmentRemark;
}
@@ -30,6 +30,6 @@ public class GaoProductPage extends BasePage {
/// 产品类型
private GaoProduct.ProductType productType;
/// 名称或编码关键词
/// 名称、拼音或编码关键词
private String keyword;
}
@@ -18,6 +18,6 @@ public class GaoProductStockPage extends BasePage {
/// 产品 ID
private String productId;
/// 产品或 SKU 名称、编码、规格摘要关键词
/// 产品或 SKU 名称、拼音、编码、规格摘要关键词
private String keyword;
}
@@ -19,7 +19,7 @@ public class GaoProductStockRecordPage extends BasePage {
/// 产品 ID
private String productId;
/// 产品名称或编码关键词
/// 产品名称、拼音或编码关键词
private String keyword;
/// 来源业务类型
@@ -1,66 +1,51 @@
package com.imyeyu.api.modules.system.audit;
import com.imyeyu.api.modules.system.entity.Logger;
import lombok.AllArgsConstructor;
import lombok.Getter;
/// 操作审计编码
///
/// @author Codex
/// @since 2026-09-09
@Getter
@AllArgsConstructor
public enum AuditOperation {
GAO_CUSTOMER_PACKAGE_PURCHASE(Logger.Module.GAO, "GAO_CUSTOMER_PACKAGE_PURCHASE"),
GAO_CUSTOMER_PACKAGE_CANCEL(Logger.Module.GAO, "GAO_CUSTOMER_PACKAGE_CANCEL"),
GAO_SALES_ORDER_PAYMENT_RECEIVE(Logger.Module.GAO, "GAO_SALES_ORDER_PAYMENT_RECEIVE"),
GAO_SALES_ORDER_PAYMENT_VERIFY(Logger.Module.GAO, "GAO_SALES_ORDER_PAYMENT_VERIFY"),
GAO_SALES_ORDER_REFUND(Logger.Module.GAO, "GAO_SALES_ORDER_REFUND"),
GAO_SALES_ORDER_CONFIRM_NO_REFUND(Logger.Module.GAO, "GAO_SALES_ORDER_CONFIRM_NO_REFUND"),
GAO_SALES_ORDER_FUND_REVOKE(Logger.Module.GAO, "GAO_SALES_ORDER_FUND_REVOKE"),
GAO_ORDER_FULFILLMENT_CREATE(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_CREATE"),
GAO_ORDER_FULFILLMENT_PREPARE_STOCK(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_PREPARE_STOCK"),
GAO_ORDER_FULFILLMENT_VERIFY(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_VERIFY"),
GAO_ORDER_FULFILLMENT_REJECT(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_REJECT"),
GAO_ORDER_FULFILLMENT_CANCEL(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_CANCEL"),
GAO_ORDER_FULFILLMENT_REVOKE(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_REVOKE"),
GAO_PACKAGE_CONFIG_CREATE(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_CREATE"),
GAO_PACKAGE_CONFIG_UPDATE(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_UPDATE"),
GAO_PACKAGE_CONFIG_DELETE(Logger.Module.GAO, "GAO_PACKAGE_CONFIG_DELETE"),
GAO_PRODUCT_CATEGORY_SORT(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_SORT"),
GAO_PRODUCT_CATEGORY_CREATE(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_CREATE"),
GAO_PRODUCT_CATEGORY_UPDATE(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_UPDATE"),
GAO_PRODUCT_CATEGORY_DISABLE(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_DISABLE"),
GAO_PRODUCT_CREATE(Logger.Module.GAO, "GAO_PRODUCT_CREATE"),
GAO_PRODUCT_UPDATE(Logger.Module.GAO, "GAO_PRODUCT_UPDATE"),
GAO_PRODUCT_DISABLE(Logger.Module.GAO, "GAO_PRODUCT_DISABLE"),
GAO_PRODUCT_STOCK_INITIAL_IN(Logger.Module.GAO, "GAO_PRODUCT_STOCK_INITIAL_IN"),
GAO_PRODUCT_STOCK_MANUAL_IN(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_IN"),
GAO_PRODUCT_STOCK_MANUAL_OUT(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_OUT"),
GAO_PRODUCT_STOCK_MANUAL_REVOKE(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_REVOKE"),
GAO_PRODUCT_STOCK_STOCKTAKE_GAIN(Logger.Module.GAO, "GAO_PRODUCT_STOCK_STOCKTAKE_GAIN"),
GAO_PRODUCT_STOCK_STOCKTAKE_LOSS(Logger.Module.GAO, "GAO_PRODUCT_STOCK_STOCKTAKE_LOSS"),
GAO_SALES_ORDER_DIRECT_PRODUCT_CREATE(Logger.Module.GAO, "GAO_SALES_ORDER_DIRECT_PRODUCT_CREATE"),
GAO_SALES_ORDER_UPDATE(Logger.Module.GAO, "GAO_SALES_ORDER_UPDATE"),
GAO_SALES_ORDER_CANCEL(Logger.Module.GAO, "GAO_SALES_ORDER_CANCEL");
COMMON_NOTIFY_CREATE(Logger.Module.COMMON),
COMMON_NOTIFY_DELETE(Logger.Module.COMMON),
GAO_CUSTOMER_PACKAGE_PURCHASE(Logger.Module.GAO),
GAO_CUSTOMER_PACKAGE_CANCEL(Logger.Module.GAO),
GAO_SALES_ORDER_PAYMENT_RECEIVE(Logger.Module.GAO),
GAO_SALES_ORDER_PAYMENT_VERIFY(Logger.Module.GAO),
GAO_SALES_ORDER_REFUND(Logger.Module.GAO),
GAO_SALES_ORDER_CONFIRM_NO_REFUND(Logger.Module.GAO),
GAO_SALES_ORDER_FUND_REVOKE(Logger.Module.GAO),
GAO_ORDER_FULFILLMENT_CREATE(Logger.Module.GAO),
GAO_ORDER_FULFILLMENT_PREPARE_STOCK(Logger.Module.GAO),
GAO_ORDER_FULFILLMENT_VERIFY(Logger.Module.GAO),
GAO_ORDER_FULFILLMENT_REJECT(Logger.Module.GAO),
GAO_ORDER_FULFILLMENT_CANCEL(Logger.Module.GAO),
GAO_ORDER_FULFILLMENT_REVOKE(Logger.Module.GAO),
GAO_PACKAGE_CONFIG_CREATE(Logger.Module.GAO),
GAO_PACKAGE_CONFIG_UPDATE(Logger.Module.GAO),
GAO_PACKAGE_CONFIG_DELETE(Logger.Module.GAO),
GAO_PRODUCT_CATEGORY_SORT(Logger.Module.GAO),
GAO_PRODUCT_CATEGORY_CREATE(Logger.Module.GAO),
GAO_PRODUCT_CATEGORY_UPDATE(Logger.Module.GAO),
GAO_PRODUCT_CATEGORY_DISABLE(Logger.Module.GAO),
GAO_PRODUCT_CREATE(Logger.Module.GAO),
GAO_PRODUCT_UPDATE(Logger.Module.GAO),
GAO_PRODUCT_DISABLE(Logger.Module.GAO),
GAO_PRODUCT_STOCK_INITIAL_IN(Logger.Module.GAO),
GAO_PRODUCT_STOCK_MANUAL_IN(Logger.Module.GAO),
GAO_PRODUCT_STOCK_MANUAL_OUT(Logger.Module.GAO),
GAO_PRODUCT_STOCK_MANUAL_REVOKE(Logger.Module.GAO),
GAO_PRODUCT_STOCK_STOCKTAKE_GAIN(Logger.Module.GAO),
GAO_PRODUCT_STOCK_STOCKTAKE_LOSS(Logger.Module.GAO),
GAO_SALES_ORDER_DIRECT_PRODUCT_CREATE(Logger.Module.GAO),
GAO_SALES_ORDER_UPDATE(Logger.Module.GAO),
GAO_SALES_ORDER_CANCEL(Logger.Module.GAO);
private final Logger.Module module;
private final String code;
AuditOperation(Logger.Module module, String code) {
this.module = module;
this.code = code;
}
/// 获取日志模块
///
/// @return 日志模块
public Logger.Module getModule() {
return module;
}
/// 获取操作编码
///
/// @return 操作编码
public String getCode() {
return code;
}
final Logger.Module module;
}
@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.system.audit;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -30,7 +31,7 @@ public class OperationAuditAspect {
/// 执行并记录操作审计
///
/// @param joinPoint 连接点
/// @param joinPoint 连接点
/// @param operationAudit 审计配置
/// @return 业务返回值
/// @throws Throwable 业务异常
@@ -38,7 +39,7 @@ public class OperationAuditAspect {
public Object around(ProceedingJoinPoint joinPoint, OperationAudit operationAudit) throws Throwable {
AuditOperation operation = operationAudit.value();
Object[] arguments = joinPoint.getArgs();
Logger logger = new Logger(operation.getModule(), operation.getCode());
Logger logger = new Logger(operation.getModule(), operation.name());
logger.setContent(payloadSerializer.serializeArguments(arguments));
long startNanos = System.nanoTime();
try {
@@ -56,9 +57,9 @@ public class OperationAuditAspect {
throw e;
} catch (Throwable e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(serializeThrowable(e));
logger.setException(TimiJava.serializeThrowable(e));
logger.setDuration(duration(startNanos));
log.error("业务操作失败: {}", operation.getCode(), e);
log.error("业务操作失败: {}", operation.name(), e);
record(logger);
throw e;
}
@@ -79,12 +80,4 @@ public class OperationAuditAspect {
}
return (int) Math.max(0L, millis);
}
private String serializeThrowable(Throwable throwable) {
java.io.StringWriter stringWriter = new java.io.StringWriter();
try (java.io.PrintWriter printWriter = new java.io.PrintWriter(stringWriter)) {
throwable.printStackTrace(printWriter);
}
return stringWriter.toString();
}
}
@@ -1,19 +0,0 @@
package com.imyeyu.api.modules.system.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
/**
*
*
* @author 夜雨
* @since 2026-04-23 16:31
*/
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/system/setting")
public class SettingController {
}
@@ -0,0 +1,55 @@
package db.migration.timiserver;
import com.imyeyu.api.util.PinyinUtil;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
/// 回填 GAO 产品名称拼音搜索字段
///
/// @author Codex
/// @since 2026-09-10
public class V78__backfill_gao_product_pinyin_search extends BaseJavaMigration {
private static final int BATCH_SIZE = 500;
@Override
public void migrate(Context context) throws Exception {
Connection connection = context.getConnection();
try (
PreparedStatement select = connection.prepareStatement("""
SELECT `id`, `name`
FROM `gao_product`
WHERE `name` IS NOT NULL
AND (`name_pinyin_full` IS NULL OR `name_pinyin_initial` IS NULL OR `name_pinyin_mixed` IS NULL)
""");
PreparedStatement update = connection.prepareStatement("""
UPDATE `gao_product`
SET `name_pinyin_full` = ?, `name_pinyin_initial` = ?, `name_pinyin_mixed` = ?
WHERE `id` = ?
""")) {
try (ResultSet resultSet = select.executeQuery()) {
int batchSize = 0;
while (resultSet.next()) {
String name = resultSet.getString("name");
update.setString(1, PinyinUtil.full(name));
update.setString(2, PinyinUtil.initial(name));
update.setString(3, PinyinUtil.mixed(name));
update.setString(4, resultSet.getString("id"));
update.addBatch();
batchSize++;
if (batchSize >= BATCH_SIZE) {
update.executeBatch();
batchSize = 0;
}
}
if (0 < batchSize) {
update.executeBatch();
}
}
}
}
}
@@ -0,0 +1,25 @@
ALTER TABLE `notify`
CHANGE COLUMN `biz_reminded_id` `biz_reminded_key` VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '业务提醒查重键',
ADD COLUMN `biz_reminded_active_key` TINYINT(1) GENERATED ALWAYS AS (IF(`deleted_at` IS NULL, 1, NULL)) STORED COMMENT '未删除查重键' INVISIBLE AFTER `deleted_at`,
DROP INDEX `idx_biz_reminded_created`,
ADD INDEX `idx_biz_reminded_key_created`(`biz_type` ASC, `biz_reminded_type` ASC, `biz_reminded_key` ASC, `created_at` ASC) USING BTREE;
UPDATE `notify` target
INNER JOIN (
SELECT
id,
ROW_NUMBER() OVER (
PARTITION BY biz_type, biz_reminded_type, biz_reminded_key
ORDER BY created_at DESC, id DESC
) AS rn
FROM `notify`
WHERE deleted_at IS NULL
AND biz_type IS NOT NULL
AND biz_reminded_type IS NOT NULL
AND biz_reminded_key IS NOT NULL
) duplicate ON duplicate.id = target.id
SET target.deleted_at = UNIX_TIMESTAMP() * 1000
WHERE duplicate.rn > 1;
ALTER TABLE `notify`
ADD UNIQUE INDEX `uk_biz_reminded_key_active`(`biz_type` ASC, `biz_reminded_type` ASC, `biz_reminded_key` ASC, `biz_reminded_active_key` ASC) USING BTREE;
@@ -0,0 +1,7 @@
ALTER TABLE `gao_product`
ADD COLUMN `name_pinyin_full` VARCHAR(768) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '产品名称全拼' AFTER `name`,
ADD COLUMN `name_pinyin_initial` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '产品名称拼音首字母' AFTER `name_pinyin_full`,
ADD COLUMN `name_pinyin_mixed` VARCHAR(768) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '产品名称混合拼音' AFTER `name_pinyin_initial`,
ADD KEY `idx_gao_product_store_name_pinyin_full` (`store_id`, `deleted_at`, `name_pinyin_full`(128)),
ADD KEY `idx_gao_product_store_name_pinyin_initial` (`store_id`, `deleted_at`, `name_pinyin_initial`),
ADD KEY `idx_gao_product_store_name_pinyin_mixed` (`store_id`, `deleted_at`, `name_pinyin_mixed`(128));
@@ -1,6 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.common.mapper.NotifyMapper">
<insert id="insertIfAbsent">
INSERT INTO `notify` (
`id`,
`biz_type`,
`biz_reminded_type`,
`biz_reminded_key`,
`args`,
`created_by`,
`created_at`,
`updated_at`,
`deleted_at`
) VALUES (
#{id},
#{bizType},
#{bizRemindedType},
#{bizRemindedKey},
#{args},
#{createdBy},
#{createdAt},
#{updatedAt},
#{deletedAt}
)
ON DUPLICATE KEY UPDATE `id` = `id`
</insert>
<select id="selectManagementPage" resultType="com.imyeyu.api.modules.common.vo.NotifyManagementItem">
SELECT
n.`id`,
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.common.mapper.SettingMapper">
<select id="selectPublicByQuery" resultType="com.imyeyu.api.modules.common.entity.Setting">
SELECT *
FROM `setting`
WHERE `biz_type` = #{bizType}
AND `is_private` = 0
<if test="bizId != null and bizId != ''">
AND `biz_id` = #{bizId}
</if>
<if test="codeList != null and !codeList.isEmpty()">
AND `code` IN
<foreach collection="codeList" item="code" separator="," open="(" close=")">
#{code}
</foreach>
</if>
AND (`deleted_at` IS NULL OR FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) &lt; `deleted_at`)
</select>
</mapper>
@@ -18,7 +18,6 @@
</if>
AND `deleted_at` IS NULL
</select>
<sql id="queryWhere">
`c`.`deleted_at` IS NULL
<if test="storeId != null and storeId != ''">
@@ -50,6 +49,12 @@
HAVING COUNT(DISTINCT `ta`.`tag_id`) = #{tagCount}
)
</if>
<if test="beginCreatedAt != null">
AND `c`.`created_at` &gt;= #{beginCreatedAt}
</if>
<if test="endCreatedAt != null">
AND `c`.`created_at` &lt;= #{endCreatedAt}
</if>
</sql>
<select id="countByQuery" resultType="long">
@@ -64,7 +69,18 @@
FROM `gao_customer` `c`
WHERE
<include refid="queryWhere"/>
ORDER BY `c`.`created_at` DESC, `c`.`id` DESC
<choose>
<when test="orderMap != null and !orderMap.isEmpty()">
ORDER BY
<foreach collection="orderMap" index="key" item="value" separator=",">
<bind name="underscoreKey" value="@com.imyeyu.utils.Text@camelCase2underscore(key)"/>
${underscoreKey} ${value}
</foreach>
</when>
<otherwise>
ORDER BY `c`.`created_at` DESC, `c`.`id` DESC
</otherwise>
</choose>
LIMIT #{offset}, #{limit}
</select>
@@ -82,6 +98,19 @@
ORDER BY `dateValue` ASC
</select>
<select id="selectDailyNewCustomer" resultType="com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateCustomerView">
SELECT
`name`,
CAST(DATE_FORMAT(FROM_UNIXTIME(`created_at` / 1000), '%Y%m%d') AS UNSIGNED) AS `dateValue`
FROM `gao_customer`
WHERE
`store_id` = #{storeId}
AND `created_at` &gt;= #{beginCreatedAt}
AND `created_at` &lt;= #{endCreatedAt}
AND `deleted_at` IS NULL
ORDER BY `dateValue` ASC, `created_at` ASC, `id` ASC
</select>
<select id="countCreatedBefore" resultType="java.lang.Long">
SELECT
COUNT(1)
@@ -102,4 +131,31 @@
AND `deleted_at` IS NULL
GROUP BY IFNULL(CAST(`gender` AS CHAR), 'UNKNOWN')
</select>
<select id="stateAge" resultType="com.imyeyu.api.modules.gao.vo.GaoCustomerAgeStatView">
SELECT
COALESCE(`age`, TIMESTAMPDIFF(YEAR, `birthdate`, CURDATE())) AS `age`,
COUNT(1) AS `count`
FROM `gao_customer`
WHERE
`store_id` = #{storeId}
AND `deleted_at` IS NULL
AND (`age` IS NOT NULL OR `birthdate` IS NOT NULL)
GROUP BY COALESCE(`age`, TIMESTAMPDIFF(YEAR, `birthdate`, CURDATE()))
ORDER BY COALESCE(`age`, TIMESTAMPDIFF(YEAR, `birthdate`, CURDATE())) ASC
</select>
<select id="selectDiseaseTextByStoreId" resultType="com.imyeyu.api.modules.gao.vo.GaoCustomerDiseaseTextView">
SELECT
`remark`,
`disease`
FROM `gao_customer`
WHERE
`store_id` = #{storeId}
AND `deleted_at` IS NULL
AND (
(`remark` IS NOT NULL AND `remark` != '')
OR (`disease` IS NOT NULL AND `disease` != '')
)
</select>
</mapper>
@@ -348,10 +348,23 @@
FROM `daily_stat` d
JOIN `open_calendar` o ON d.`registeredDateValue` = o.`dateValue`
),
`customer_stat` AS (
SELECT
d.`customerId`,
MAX(CASE WHEN d.`registeredDateValue` = #{todayDateValue} THEN 1 ELSE 0 END) AS `hasToday`
FROM `daily_stat` d
GROUP BY d.`customerId`
),
`anchor_date` AS (
SELECT
MAX(`openIndex`) AS `anchorOpenIndex`
FROM `open_calendar`
s.`customerId`,
CASE
WHEN s.`hasToday` = 1 THEN MAX(CASE WHEN c.`dateValue` = #{todayDateValue} THEN c.`openIndex` END)
ELSE MAX(CASE WHEN c.`dateValue` &lt; #{todayDateValue} THEN c.`openIndex` END)
END AS `anchorOpenIndex`
FROM `customer_stat` s
CROSS JOIN `open_calendar` c
GROUP BY s.`customerId`, s.`hasToday`
),
`continuous_stat` AS (
SELECT
@@ -361,7 +374,7 @@
ELSE COUNT(1)
END AS `recentContinuousEventDays`
FROM `continuous_ordered` o
JOIN `anchor_date` a
JOIN `anchor_date` a ON o.`customerId` = a.`customerId`
WHERE
o.`openIndex` + o.`dayIndex` = a.`anchorOpenIndex` + 1
<if test="eventIdList == null or eventIdList.size() != 1">
@@ -53,7 +53,7 @@
INNER JOIN `gao_customer` `c` ON `c`.`id` = `a`.`customer_id` AND `c`.`store_id` = `a`.`store_id`
WHERE
<include refid="queryWhere"/>
ORDER BY `a`.`created_at` DESC, `a`.`id` DESC
ORDER BY `a`.`total_earned` DESC, `a`.`created_at` DESC, `a`.`id` DESC
LIMIT #{offset}, #{limit}
</select>
@@ -13,7 +13,7 @@
</choose>
<if test="status != null">AND `status` = #{status}</if>
<if test="productType != null">AND `product_type` = #{productType}</if>
<if test="keyword != null and keyword.trim() != ''">AND (`code` LIKE CONCAT('%', #{keyword}, '%') OR `name` LIKE CONCAT('%', #{keyword}, '%'))</if>
<if test="keyword != null and keyword.trim() != ''">AND (`code` LIKE CONCAT('%', #{keyword}, '%') OR `name` LIKE CONCAT('%', #{keyword}, '%') OR `name_pinyin_full` LIKE CONCAT(#{keyword}, '%') OR `name_pinyin_initial` LIKE CONCAT(#{keyword}, '%') OR `name_pinyin_mixed` LIKE CONCAT(#{keyword}, '%'))</if>
</select>
<select id="selectByQuery" resultType="com.imyeyu.api.modules.gao.entity.GaoProduct">
SELECT * FROM `gao_product`
@@ -27,7 +27,7 @@
</choose>
<if test="status != null">AND `status` = #{status}</if>
<if test="productType != null">AND `product_type` = #{productType}</if>
<if test="keyword != null and keyword.trim() != ''">AND (`code` LIKE CONCAT('%', #{keyword}, '%') OR `name` LIKE CONCAT('%', #{keyword}, '%'))</if>
<if test="keyword != null and keyword.trim() != ''">AND (`code` LIKE CONCAT('%', #{keyword}, '%') OR `name` LIKE CONCAT('%', #{keyword}, '%') OR `name_pinyin_full` LIKE CONCAT(#{keyword}, '%') OR `name_pinyin_initial` LIKE CONCAT(#{keyword}, '%') OR `name_pinyin_mixed` LIKE CONCAT(#{keyword}, '%'))</if>
ORDER BY `sort`, `name`, `code`, `created_at` DESC
LIMIT #{offset}, #{limit}
</select>
@@ -23,7 +23,7 @@
AND k.`stock_enabled` = 1 AND k.`status` = 'ACTIVE'
<if test="storeId != null and storeId != ''">AND p.`store_id` = #{storeId}</if>
<if test="productId != null and productId != ''">AND k.`product_id` = #{productId}</if>
<if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR k.`code` LIKE CONCAT('%', #{keyword}, '%') OR k.`name` LIKE CONCAT('%', #{keyword}, '%') OR k.`spec_summary` LIKE CONCAT('%', #{keyword}, '%') OR k.`barcode` LIKE CONCAT('%', #{keyword}, '%'))</if>
<if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR p.`name_pinyin_full` LIKE CONCAT(#{keyword}, '%') OR p.`name_pinyin_initial` LIKE CONCAT(#{keyword}, '%') OR p.`name_pinyin_mixed` LIKE CONCAT(#{keyword}, '%') OR k.`code` LIKE CONCAT('%', #{keyword}, '%') OR k.`name` LIKE CONCAT('%', #{keyword}, '%') OR k.`spec_summary` LIKE CONCAT('%', #{keyword}, '%') OR k.`barcode` LIKE CONCAT('%', #{keyword}, '%'))</if>
</select>
<select id="selectByQuery" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStock">
SELECT s.`id`,
@@ -45,7 +45,7 @@
AND k.`stock_enabled` = 1 AND k.`status` = 'ACTIVE'
<if test="storeId != null and storeId != ''">AND p.`store_id` = #{storeId}</if>
<if test="productId != null and productId != ''">AND k.`product_id` = #{productId}</if>
<if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR k.`code` LIKE CONCAT('%', #{keyword}, '%') OR k.`name` LIKE CONCAT('%', #{keyword}, '%') OR k.`spec_summary` LIKE CONCAT('%', #{keyword}, '%') OR k.`barcode` LIKE CONCAT('%', #{keyword}, '%'))</if>
<if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR p.`name_pinyin_full` LIKE CONCAT(#{keyword}, '%') OR p.`name_pinyin_initial` LIKE CONCAT(#{keyword}, '%') OR p.`name_pinyin_mixed` LIKE CONCAT(#{keyword}, '%') OR k.`code` LIKE CONCAT('%', #{keyword}, '%') OR k.`name` LIKE CONCAT('%', #{keyword}, '%') OR k.`spec_summary` LIKE CONCAT('%', #{keyword}, '%') OR k.`barcode` LIKE CONCAT('%', #{keyword}, '%'))</if>
ORDER BY p.`sort`, p.`name`, p.`code`, k.`sort`, k.`name`, k.`code`
LIMIT #{offset}, #{limit}
</select>
@@ -57,7 +57,7 @@
AND p.`stock_enabled` = 1
AND k.`stock_enabled` = 1 AND k.`status` = 'ACTIVE'
<if test="storeId != null and storeId != ''">AND p.`store_id` = #{storeId}</if>
<if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%'))</if>
<if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR p.`name_pinyin_full` LIKE CONCAT(#{keyword}, '%') OR p.`name_pinyin_initial` LIKE CONCAT(#{keyword}, '%') OR p.`name_pinyin_mixed` LIKE CONCAT(#{keyword}, '%'))</if>
</select>
<select id="selectProductByQuery" resultType="com.imyeyu.api.modules.gao.vo.GaoProductStockProductView">
SELECT p.`id` AS `product_id`,
@@ -79,7 +79,7 @@
AND p.`stock_enabled` = 1
AND k.`stock_enabled` = 1 AND k.`status` = 'ACTIVE'
<if test="storeId != null and storeId != ''">AND p.`store_id` = #{storeId}</if>
<if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%'))</if>
<if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR p.`name_pinyin_full` LIKE CONCAT(#{keyword}, '%') OR p.`name_pinyin_initial` LIKE CONCAT(#{keyword}, '%') OR p.`name_pinyin_mixed` LIKE CONCAT(#{keyword}, '%'))</if>
GROUP BY p.`id`, p.`code`, p.`name`, p.`product_type`, p.`sort`
ORDER BY p.`sort`, p.`name`, p.`code`
LIMIT #{offset}, #{limit}
@@ -31,7 +31,7 @@
1 = 1
<if test="storeId != null and storeId != ''">AND r.`store_id` = #{storeId}</if>
<if test="productId != null and productId != ''">AND k.`product_id` = #{productId}</if>
<if test="keyword != null and keyword.trim() != ''">AND (r.`product_name_snapshot` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR p.`code` LIKE CONCAT('%', #{keyword}, '%'))</if>
<if test="keyword != null and keyword.trim() != ''">AND (r.`product_name_snapshot` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR p.`name_pinyin_full` LIKE CONCAT(#{keyword}, '%') OR p.`name_pinyin_initial` LIKE CONCAT(#{keyword}, '%') OR p.`name_pinyin_mixed` LIKE CONCAT(#{keyword}, '%') OR p.`code` LIKE CONCAT('%', #{keyword}, '%'))</if>
<if test="bizType != null">AND r.`biz_type` = #{bizType}</if>
<if test="bizId != null and bizId != ''">AND r.`biz_id` = #{bizId}</if>
<if test="operationType != null">AND r.`operation_type` = #{operationType}</if>