25 Commits

Author SHA1 Message Date
cd767dce6f BUGFIX:新用户不能获取历史系统通知 2025-12-24 11:47:11 +08:00
bf95b85841 to dev 2025-12-23 16:28:07 +08:00
9e58bd9e7d Merge branch 'release/3.1' into dev/3.1_release_merge
# Conflicts:
#	src/main/java/com/ai/da/common/utils/SendRequestUtil.java
2025-12-23 16:16:30 +08:00
d0ec5c5c26 BUGFIX:邮件发送失败 2025-12-23 16:03:48 +08:00
ab8aa5ea5c Merge branch 'dev/dev_xp' into dev/3.1_release_merge 2025-12-23 14:07:53 +08:00
litianxiang
34da437a26 Merge remote-tracking branch 'origin/dev-ltx' into dev/3.1_release_merge 2025-12-22 11:36:23 +08:00
litianxiang
f84935d0bd 登录token存入redis 2025-12-22 11:35:38 +08:00
35edaa0f27 CONFIG: 拦截器配置 2025-12-19 21:43:02 +08:00
f43099e19e CONFIG: redis 配置修改 2025-12-19 21:21:21 +08:00
8079877734 CONFIG: TO DEV 2025-12-19 17:40:37 +08:00
ef686e38ac CONFIG: TO PROD 2025-12-19 17:33:51 +08:00
100019d2a4 Merge remote-tracking branch 'origin/dev/3.1_release_merge' into dev/3.1_release_merge 2025-12-19 16:00:55 +08:00
litianxiang
12af237d76 Merge remote-tracking branch 'origin/dev/3.1_release_merge' into dev/3.1_release_merge 2025-12-19 15:47:36 +08:00
litianxiang
44dbbb2a4b 更换Moodboard和Printboard提示词 2025-12-19 15:47:07 +08:00
9f42e153a4 Merge branch 'release/3.1' into dev/3.1_release_merge
# Conflicts:
#	.gitea/workflows/develop_build_manual.yaml
2025-12-19 15:01:32 +08:00
6cd42b799a 删除 .gitea/workflows/prod_build_schedule.yaml 2025-12-01 10:22:29 +08:00
6e1ed7f9b8 删除 .gitea/workflows/prod_build_manual.yaml 2025-12-01 10:22:25 +08:00
b7be16738b 删除 .gitea/workflows/develop_build_manual.yaml 2025-12-01 10:22:21 +08:00
6da5e91ec1 删除 .gitea/workflows/develop_build_commit.yaml 2025-12-01 10:22:17 +08:00
a710fdd432 删除 docker-compose.yml 2025-11-30 11:01:12 +08:00
d598f53d3c Merge branch 'dev/3.1_release_merge' into release/3.1
# Conflicts:
#	.gitea/workflows/prod_build_schedule.yaml
2025-11-28 17:16:55 +08:00
96170a9956 更新 .gitea/workflows/prod_build_manual.yaml 2025-11-28 15:25:50 +08:00
8205fb5290 上传文件至「.gitea/workflows」 2025-11-28 15:25:41 +08:00
fcbe4762b3 TO prod 2025-11-27 17:38:24 +08:00
e750adcc94 TO prod 2025-11-27 17:35:47 +08:00
10 changed files with 146 additions and 57 deletions

View File

@@ -6,6 +6,7 @@ import com.ai.da.common.context.UserContext;
import com.ai.da.common.security.config.SecurityProperties;
import com.ai.da.common.security.jwt.JWTTokenHelper;
import com.ai.da.common.utils.LocalCacheUtils;
import com.ai.da.common.utils.RedisUtil;
import com.ai.da.common.utils.MultiReadHttpServletRequest;
import com.ai.da.common.utils.MultiReadHttpServletResponse;
import com.ai.da.common.utils.RequestInfoUtil;
@@ -40,6 +41,8 @@ public class AuthenticationFilter extends OncePerRequestFilter {
private JWTTokenHelper jwtTokenHelper;
@Resource
private SecurityProperties properties;
@Resource
private RedisUtil redisUtil;
private static final List<String> FILTER_URL =
Arrays.asList("/favicon.ico", "/doc.html", "/swagger-ui.html",
@@ -132,12 +135,19 @@ public class AuthenticationFilter extends OncePerRequestFilter {
UserContext.delete();
//存取用户信息到缓存
UserContext.setUserHolder(principal);
//校验token
String cacheToken = LocalCacheUtils.getTokenCache(String.valueOf(principal.getId()));
// 校验 token:先查本地缓存,再查 Redis保证服务重启后仍然有效
String userIdStr = String.valueOf(principal.getId());
String cacheToken = LocalCacheUtils.getTokenCache(userIdStr);
if(StringUtils.isEmpty(cacheToken)){
// throw new RuntimeException("TOKEN已过期请重新登录");
throw new TokenMissingOrExpiredException("TOKEN已过期请重新登录(local cache empty)");
if (StringUtils.isEmpty(cacheToken)) {
// 本地缓存为空时,尝试从 Redis 读取
cacheToken = redisUtil.getLoginToken(principal.getId());
if (StringUtils.isEmpty(cacheToken)) {
// throw new RuntimeException("TOKEN已过期请重新登录");
throw new TokenMissingOrExpiredException("TOKEN已过期请重新登录(cache & redis empty)");
}
// 将 Redis 中的 token 回填到本地缓存,减少后续 Redis 访问
LocalCacheUtils.setTokenCache(userIdStr, cacheToken);
}
if(!cacheToken.equals(jwtToken) ){
// throw new RuntimeException("TOKEN已过期请重新登录");

View File

@@ -40,7 +40,7 @@ public class SubscriptionReminderTask {
REMINDER_DAYS_CONFIG.put("year", 14);
}
@Scheduled(cron = "0 0 9 * * ?")
// @Scheduled(cron = "0 0 9 * * ?")
public void subscriptionReminder() {
// 获取所有需要通知的订阅
List<SubscriptionInfo> subscriptionInfos = getDueSubscriptions();
@@ -97,7 +97,7 @@ public class SubscriptionReminderTask {
return subscriptionInfoMapper.selectList(qw);
}
@Scheduled(cron = "0 0 9 * * ?")
// @Scheduled(cron = "0 0 9 * * ?")
public void trialReminder() {
// 今天的 00:00:00 和 23:59:59
LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();

View File

@@ -34,6 +34,11 @@ public class RedisUtil {
private RedisTemplate<String, String> redisTemplate;
public final static String FLUX_POLLING_URL = "Flux:";
/**
* 登录 token 在 Redis 中的前缀:
* 最终 key 结构为 login:token:{userId}
*/
public final static String LOGIN_TOKEN_KEY = "login:token:";
public Boolean hasKey(String key){
return redisTemplate.hasKey(key);
@@ -186,6 +191,40 @@ public class RedisUtil {
redisTemplate.delete(key);
}
/**
* 保存登录 token
*
* @param userId 用户 ID
* @param token token 字符串
* @param expireMillis 过期时间(毫秒,通常与 JWT 保持一致)
*/
public void setLoginToken(Long userId, String token, long expireMillis) {
if (expireMillis <= 0) {
// 不设置过期时间,直到手动删除(不推荐)
addToString(LOGIN_TOKEN_KEY + userId, token);
return;
}
long expireSeconds = expireMillis / 1000;
if (expireSeconds <= 0) {
expireSeconds = 1;
}
addToString(LOGIN_TOKEN_KEY + userId, token, expireSeconds);
}
/**
* 获取登录 token
*/
public String getLoginToken(Long userId) {
return getFromString(LOGIN_TOKEN_KEY + userId);
}
/**
* 删除登录 token
*/
public void deleteLoginToken(Long userId) {
removeFromString(LOGIN_TOKEN_KEY + userId);
}
public final static String PORTFOLIO_LIKE_KEY = "portfolio:like:";
public void likePost(Long portfolioId, Long userId) {

View File

@@ -114,8 +114,8 @@ public class SendRequestUtil {
public String sendFluxPost(String url, String requestBodyStr) {
// 尝试两个API key
String[] apiKeys = {"d447a0ac-2291-4f1c-9a36-f7614c385989",
"84e8f5d5-b0b3-49aa-b244-ab7ba27e7ae7"};
String[] apiKeys = {"84e8f5d5-b0b3-49aa-b244-ab7ba27e7ae7",
"d447a0ac-2291-4f1c-9a36-f7614c385989"};
boolean[] notified = {false, false}; // 记录是否已发送过不足提醒
for (int i = 0; i < apiKeys.length; i++) {
@@ -140,22 +140,9 @@ public class SendRequestUtil {
if (status == 402 || status == 403) {
if (!notified[i]) {
SendEmailUtil.commonExceptionReminder(
"Flux账户积分不足flux生成任务失败",
new String[]{"xupei3360@163.com"}
"Flux账户积分不足flux生成任务失败key:" + apiKeys[i],
new String[]{"xupei3360@163.com", "fangjianliao@aidlab.hk", "investigation@aidlab.hk"}
);
log.info("发送给xupei3360@163.com");
SendEmailUtil.commonExceptionReminder(
"Flux账户积分不足flux生成任务失败",
new String[]{"fangjianliao@aidlab.hk"}
);
log.info("发送给fangjianliao@aidlab.hk");
SendEmailUtil.commonExceptionReminder(
"Flux账户积分不足flux生成任务失败",
new String[]{"investigation@aidlab.hk"}
);
log.info("发送给investigation@aidlab.hk");
notified[i] = true;
}
continue; // 尝试下一个key

View File

@@ -4,6 +4,7 @@ import com.ai.da.common.config.mybatis.plus.CommonMapper;
import com.ai.da.mapper.primary.entity.Notification;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -20,5 +21,5 @@ public interface NotificationMapper extends CommonMapper<Notification> {
void setPersonalNotificationAllRead(String type, Long receiverId, LocalDateTime time);
List<Long> getUnreadSysNotification(Long accountId);
List<Long> getUnreadSysNotification(Long accountId, Date createTime);
}

View File

@@ -132,6 +132,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
@Resource
private RedisUtil redisUtil;
@Resource
private com.ai.da.common.security.config.SecurityProperties securityProperties;
@Resource
private UserFollowService userFollowService;
@@ -344,7 +347,11 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
principal.setLanguage(account.getLanguage());
principal.setCountry(account.getCountry());
String token2 = jwtTokenHelper.createToken(principal);
// 本地 JVM 缓存(适配旧逻辑)
LocalCacheUtils.setTokenCache(String.valueOf(account.getId()), token2);
// 同步写入 Redis重启后仍然可用
long jwtExpiration = securityProperties.getJwtExpiration();
redisUtil.setLoginToken(account.getId(), token2, jwtExpiration);
return token2;
}
@@ -600,21 +607,25 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
@Override
public Boolean logout(AccountLogoutDTO accountLogoutDTO) {
//jwt本身失效比较难做 统一用缓存实现 删除缓存失效
String token = LocalCacheUtils.getTokenCache(String.valueOf(accountLogoutDTO.getUserId()));
if (StringUtils.isNotBlank(token)) {
LocalCacheUtils.delTokenCache(String.valueOf(accountLogoutDTO.getUserId()));
}
// jwt 本身失效比较难做统一用缓存实现删除缓存失效
String userIdStr = String.valueOf(accountLogoutDTO.getUserId());
LocalCacheUtils.delTokenCache(userIdStr);
// 同时删除 Redis 中的 token防止服务重启后仍然有效
redisUtil.deleteLoginToken(accountLogoutDTO.getUserId());
return Boolean.TRUE;
}
@Override
public Boolean isLogin(AccountLogoutDTO accountLogoutDTO) {
String token = LocalCacheUtils.getTokenCache(String.valueOf(accountLogoutDTO.getUserId()));
String userIdStr = String.valueOf(accountLogoutDTO.getUserId());
// 先查本地缓存
String token = LocalCacheUtils.getTokenCache(userIdStr);
if (StringUtils.isNotBlank(token)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
// 本地没有时,再查 Redis保证服务重启后也能判断登录状态
String redisToken = redisUtil.getLoginToken(accountLogoutDTO.getUserId());
return StringUtils.isNotBlank(redisToken);
}
@Override
@@ -812,6 +823,8 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
if (StringUtils.isNotBlank(token)) {
LocalCacheUtils.delTokenCache(String.valueOf(accountDelete.getId()));
}
// 删除 Redis 中对应的登录 token
redisUtil.deleteLoginToken(accountDelete.getId());
if (!userName.equals(userToBeUpdate.getUserName())) {
// accountMapper.deleteById(accountDelete);
log.info("排查用户被删除原因deleteNoLoginRequiredtrue, 删除用户(改为降为游客)");
@@ -1065,6 +1078,8 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
if (StringUtils.isNotBlank(token)) {
LocalCacheUtils.delTokenCache(String.valueOf(account.getId()));
}
// 删除 Redis 中对应的登录 token
redisUtil.deleteLoginToken(account.getId());
// accountMapper.deleteById(account.getId());
log.info("排查用户被删除原因deleteNoLoginRequiredNew删除用户改为降为游客");
accountMapper.toVisitor(account.getId());

View File

@@ -1203,6 +1203,9 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
* @param modelName advanced high normal
*/
private HashMap<String, String> chooseModelAndPrompt(GenerateThroughImageTextDTO generateDTO, String modelName) {
if (StringUtil.isNullOrEmpty(modelName)){
throw new BusinessException("system error");
}
HashMap<String, String> modelAndPromptMap = new HashMap<>();
boolean isUseImage;
if (Objects.isNull(generateDTO.getCollectionElementId())
@@ -1218,7 +1221,7 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
String style = generateDTO.getText().substring(0, firstCommaIndex).trim();
String prompt = generateDTO.getText().substring(firstCommaIndex + 1).trim();
prompt = getPrintboardPrompt(style, prompt);
prompt = getPrintboardPrompt(style, prompt,modelName);
modelAndPromptMap.put(ModelConstants.PROMPT, prompt);
@@ -1239,7 +1242,14 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
}
} else if (ModelConstants.MOODBOARD.equals(generateDTO.getLevel1Type())) {
String prompt = generateDTO.getText() + "high-resolution, ultra-detailed, realistic textures, perfect anatomy, cinematic lighting, 8k render, editorial photography style";
String userInput = generateDTO.getText();
String systemPrompt = "high-resolution, ultra-detailed, realistic textures, cinematic lighting, 8k render, editorial photography style";
String prompt;
if (userInput == null || userInput.trim().isEmpty()) {
throw new BusinessException("prompt null");
} else {
prompt = "Theme: " + userInput.trim() + "\nRequirement: " + systemPrompt;
}
modelAndPromptMap.put(ModelConstants.PROMPT, prompt);
if (ModelConstants.ADVANCED.equals(modelName)) {
@@ -1560,19 +1570,40 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
}
private String getPrintboardPrompt(String style, String prompt) {
private String getPrintboardPrompt(String style, String userInput, String modelName) {
String systemPrompt = null;
String prompt;
if ("Painting Style".equals(style)) {
prompt = "1.Requirements: Create a seamless, tiling fashion printboard pattern for apparel. The output must be stylish, contemporary, and suitable for real garment printing. Design pattern, seamless, highly detailed, elegant composition, visually balanced, professional textile print\n" +
"2.Core Theme: " + prompt + "\n" +
"3.Style: painting_style-The painting style refers to the overall approach, techniques, and artistic philosophy used in the artwork. For fashion designs that will be applied to printboards, it is important to define the unique stylistic elements that would translate well into wearable patterns.";
if (ModelConstants.ADVANCED.equals(modelName)) {
systemPrompt = "Tileable seamless pattern, elegant composition, visually balanced, Light watercolor, Giplie Studio (style) pattern with even color field background, high-quality digital print, zero perspective depth, harmonious visual balance, consistent color tone.";
} else if (ModelConstants.HIGH.equals(modelName)) {
systemPrompt = "Design pattern, seamless, highly detailed, elegant composition, visually balanced. \n" +
"Painting style: traditional painting, hand-painted, brush strokes.";
}
} else if ("Illustration Style".equals(style)) {
prompt = "1.Requirements: Create a seamless, tiling fashion printboard pattern for apparel. The output must be stylish, contemporary, and suitable for real garment printing. Design pattern, seamless, highly detailed, elegant composition, visually balanced, professional textile print\n" +
"2.Core Theme: " + prompt + "\n" +
"3.Style: illustration_style-Illustration style focuses on the visual storytellingaspect, often used to depict narratives, characters, or thematic concepts. Forfashion, this style can introduce vivid and artistic interpretations, often aligned with specific themes.";
if (ModelConstants.ADVANCED.equals(modelName)) {
systemPrompt = "Tileable seamless pattern, elegant composition, visually balanced, flat graphic, clean lines pattern with even color field background, high-quality digital print, zero perspective depth, harmonious visual balance, consistent color tone. ";
} else if (ModelConstants.HIGH.equals(modelName)) {
systemPrompt = "Design pattern, seamless, highly detailed, elegant composition, visually balanced. \n" +
"Illustration Style: flat graphic, clean lines.";
}
} else if ("Real Style".equals(style)) {
prompt = "1.Requirements: Create a seamless, tiling fashion printboard pattern for apparel. The output must be stylish, contemporary, and suitable for real garment printing. Design pattern, seamless, highly detailed, elegant composition, visually balanced, professional textile print\n" +
"2.Core Theme: " + prompt + "\n" +
"3.Style: real_style-Real style in fashion is all about authenticity. It featuresnatural fabrics, simple cuts that mirror real life silhouettes, and colors inspired bythe everyday world, exuding a down-to-earth and genuine charm.";
if (ModelConstants.ADVANCED.equals(modelName)) {
systemPrompt = "Tileable seamless pattern, even color field background, photorealistic style pattern, high-quality digital print, zero perspective depth, harmonious visual balance, consistent color tone. ";
} else if (ModelConstants.HIGH.equals(modelName)) {
systemPrompt = "Design pattern, seamless, highly detailed, elegant composition, visually balanced. \n" +
"Flat textile pattern printed directly on fabric surface, no three-dimensional objects, no items placed on cloth. \n" +
"Real style: fabric print, realistic woven/printed pattern, detailed surface pattern only";
}
}else {
throw new BusinessException("style error:"+ style);
}
if (userInput == null || userInput.trim().isEmpty()) {
throw new BusinessException("prompt null");
} else {
prompt = "Theme: " + userInput.trim() + "\nRequirement: " + systemPrompt;
}
return prompt;
}

View File

@@ -79,6 +79,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
throw new BusinessException("type.cannot.be.empty");
}
Long accountId = UserContext.getUserHolder().getId();
Account account = accountService.getById(accountId);
// 查动态
if (!StringUtils.isNullOrEmpty(getNotificationDTO.getType()) && getNotificationDTO.getType().equals("newPosted")) {
return getNewPosted(accountId, getNotificationDTO.getPage(), getNotificationDTO.getSize());
@@ -92,6 +93,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
if (getNotificationDTO.getType().equals("system")) {
queryWrapper.lambda().eq(Notification::getType, "system")
.gt(Notification::getCreateTime, account.getCreateDate())
.and(wrapper -> wrapper
.isNull(Notification::getReceiverId)
.or()
@@ -103,7 +105,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
Page<Notification> notificationPage = baseMapper.selectPage(new Page<>(getNotificationDTO.getPage(), getNotificationDTO.getSize()), queryWrapper);
List<Long> unreadSysNotificationIds = baseMapper.getUnreadSysNotification(accountId);
List<Long> unreadSysNotificationIds = baseMapper.getUnreadSysNotification(accountId, account.getCreateDate());
IPage<NotificationVO> convert = notificationPage.convert(o -> {
NotificationVO notificationVO = CopyUtil.copyObject(o, NotificationVO.class);
Account senderAccount = accountService.getById(notificationVO.getSenderId());
@@ -247,9 +249,11 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
private Long getUnreadSystemNotification(Long receiverId) {
// Long accountId = UserContext.getUserHolder().getId();
Account account = accountService.getById(receiverId);
// 计算总的系统通知数量
QueryWrapper<Notification> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Notification::getType, "system")
.gt(Notification::getCreateTime, account.getCreateDate())
.and(wrapper -> wrapper
.isNull(Notification::getReceiverId)
.or()
@@ -302,6 +306,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
// 一键已读
public void setReadAll(String type) {
Long accountId = UserContext.getUserHolder().getId();
Account account = accountService.getById(accountId);
// 指定某个用户的某种类型的数据,将未读数据全部已读
if (!type.equals("system")) {
// 个人消息已读
@@ -309,7 +314,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
} else {
// 系统消息已读
// 1、先确定当前用户未读的系统消息有哪些
List<Long> unreadSysNotificationIds = baseMapper.getUnreadSysNotification(accountId);
List<Long> unreadSysNotificationIds = baseMapper.getUnreadSysNotification(accountId, account.getCreateDate());
// 2、将未读的设为已读
if (!unreadSysNotificationIds.isEmpty()) setReadStatusSystem(unreadSysNotificationIds);
}

View File

@@ -22,7 +22,7 @@ spring.security.jwtExpiration=604800000
spring.security.ignorePaths=/,/favicon.ico,/doc.html,/webjars/**,/swagger-resources,/v2/api-docs,\
/api/account/**,/api/element/**,/api/python/**,/api/design/**,/api/history/**,/api/library/**,/api/third/party/**,/api/generate/**,/api/workspace/**,/api/classification/**,\
/api/product/**,/api/ali-pay/**,/api/order-info/**,/api/paypal/**,/api/credits/**,/api/inquiry/**,/api/tasks/**,/api/python/prepareForSR,/api/alipay-hk/**,/api/portfolio/**,\
/api/stripe/**,/api/message/**,/api/tags/**,/notification/**,/api/affiliate/**,/api/project/**,/api/llm/**
/api/stripe/**,/api/message/**,/api/tags/**,/notification/**,/api/affiliate/**,/api/project/**,/api/llm/**, /api/subscription_plan/**
spring.security.authApi=/auth/login
@@ -71,15 +71,15 @@ spring.rabbitmq.username=rabbit
spring.rabbitmq.password=123456
spring.rabbitmq.virtual-host=/
spring.redis.host=172.31.11.32
#spring.redis.host=18.167.251.121
spring.redis.port=6379
spring.redis.database=2
spring.redis.password=Aidlab
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-wait=5
spring.data.redis.host=172.31.11.32
#spring.data.redis.host=18.167.251.121
spring.data.redis.port=6379
spring.data.redis.database=2
spring.data.redis.password=Aidlab
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0
spring.data.redis.lettuce.pool.max-wait=5
redis.key.orderForGenerate=OrderForGenerate
redis.key.generateCancelSet=GenerateCancelSet

View File

@@ -60,7 +60,8 @@
SELECT system_notification_id
FROM `t_sys_notification_read_status`
WHERE account_id = #{accountId}
)
)
AND create_time > #{createTime}
</select>