Compare commits

..
7 Commits
Author SHA1 Message Date
timi 76c2f3a5fd Merge pull request 'v1.0.49' (#61) from dev into master
Reviewed-on: #61
2026-09-16 17:34:23 +00:00
Timi b5a7c0d46c v1.0.49
CI / build-deploy (pull_request) Successful in 25s
CI / notify-on-failure (pull_request) Skipped
2026-09-17 01:31:21 +08:00
Timi 088afbe5c1 update customer in list 2026-09-17 01:31:02 +08:00
Timi 0d155ec5b2 update AIAgent 2026-09-16 15:15:36 +08:00
Timi 7d53d31c3f add GaoProductStockBatchRequest 2026-09-16 15:15:19 +08:00
Timi cd8bcae5be auto verify for order fulfillment and payment 2026-09-16 13:06:39 +08:00
Timi a2b6253d1d add customerCount for list event record 2026-09-16 12:54:39 +08:00
37 changed files with 743 additions and 87 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
<groupId>com.imyeyu.timiserverapi</groupId> <groupId>com.imyeyu.timiserverapi</groupId>
<artifactId>TimiServerAPI</artifactId> <artifactId>TimiServerAPI</artifactId>
<version>1.0.48</version> <version>1.0.49</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>TimiServerAPI</name> <name>TimiServerAPI</name>
<description>imyeyu.com API</description> <description>imyeyu.com API</description>
@@ -14,10 +14,6 @@ import org.springframework.stereotype.Component;
@Component @Component
public class GaoAiProperties { public class GaoAiProperties {
/// MCP 服务间密钥
@Value("${gao.ai.mcp-service-key:}")
private String mcpServiceKey;
/// AI 会话有效期,单位秒 /// AI 会话有效期,单位秒
@Value("${gao.ai.session-ttl-seconds:300}") @Value("${gao.ai.session-ttl-seconds:300}")
private long sessionTtlSeconds; private long sessionTtlSeconds;
@@ -33,16 +29,4 @@ public class GaoAiProperties {
/// Dify 流式读取空闲超时,单位秒 /// Dify 流式读取空闲超时,单位秒
@Value("${gao.ai.read-timeout-seconds:600}") @Value("${gao.ai.read-timeout-seconds:600}")
private long readTimeoutSeconds; private long readTimeoutSeconds;
/// true 为允许缺少 AI 会话时使用固定测试会话,仅用于本地调试
@Value("${gao.ai.allow-missing-session:false}")
private boolean allowMissingSession;
/// 缺少 AI 会话时使用的测试用户 ID
@Value("${gao.ai.debug-user-id:dify-agent-debug}")
private String debugUserId;
/// 缺少 AI 会话时使用的测试门店 ID
@Value("${gao.ai.debug-store-id:00000000-0000-0000-0000-000000000001}")
private String debugStoreId;
} }
@@ -1,8 +1,11 @@
package com.imyeyu.api.modules.gao.ai.controller; package com.imyeyu.api.modules.gao.ai.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.modules.gao.ai.service.GaoAiAccessException; import com.imyeyu.api.modules.gao.ai.service.GaoAiAccessException;
import com.imyeyu.api.modules.gao.ai.service.GaoAiChatService; import com.imyeyu.api.modules.gao.ai.service.GaoAiChatService;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiChatRequest; import com.imyeyu.api.modules.gao.ai.vo.GaoAiChatRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiConversationListRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiConversationReadRequest;
import com.imyeyu.spring.annotation.IgnoreGlobalReturn; import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import com.imyeyu.spring.annotation.RequestRateLimit; import com.imyeyu.spring.annotation.RequestRateLimit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -10,6 +13,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@@ -38,7 +42,57 @@ public class GaoAiChatController {
try { try {
return chatService.chat(request); return chatService.chat(request);
} catch (GaoAiAccessException e) { } catch (GaoAiAccessException e) {
throw new ResponseStatusException(HttpStatus.valueOf(e.getStatus()), e.getMessage(), e); throw accessException(e);
} }
} }
/// 列出当前登录用户的历史会话
///
/// @param request 历史会话分页请求
/// @return 历史会话分页结果
@RequestRateLimit
@PostMapping("/conversation/list")
public JsonNode listConversation(@RequestBody(required = false) GaoAiConversationListRequest request) {
try {
return chatService.listConversation(request);
} catch (GaoAiAccessException e) {
throw accessException(e);
}
}
/// 读取指定历史会话的消息
///
/// @param request 历史会话读取请求
/// @return 会话消息分页结果
@RequestRateLimit
@PostMapping("/conversation/read")
public JsonNode readConversation(@RequestBody GaoAiConversationReadRequest request) {
try {
return chatService.readConversation(request);
} catch (GaoAiAccessException e) {
throw accessException(e);
}
}
/// 删除当前登录用户的历史会话
///
/// @param conversationId Dify 会话 ID
/// @return 无
@RequestRateLimit
@PostMapping("/conversation/delete")
public void deleteConversation(@RequestParam String conversationId) {
try {
chatService.deleteConversation(conversationId);
} catch (GaoAiAccessException e) {
throw accessException(e);
}
}
/// 转换智能体访问异常
///
/// @param exception 智能体访问异常
/// @return HTTP 响应状态异常
private ResponseStatusException accessException(GaoAiAccessException exception) {
return new ResponseStatusException(HttpStatus.valueOf(exception.getStatus()), exception.getMessage(), exception);
}
} }
@@ -1,9 +1,10 @@
package com.imyeyu.api.modules.gao.ai.mcp; package com.imyeyu.api.modules.gao.ai.mcp;
import com.imyeyu.api.modules.gao.ai.config.GaoAiProperties; import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.gao.ai.entity.GaoAiSession; import com.imyeyu.api.modules.gao.ai.entity.GaoAiSession;
import com.imyeyu.api.modules.gao.ai.service.GaoAiSessionService; import com.imyeyu.api.modules.gao.ai.service.GaoAiSessionService;
import com.imyeyu.api.modules.gao.ai.service.implement.GaoAiSessionServiceImplement.GaoAiSessionException; import com.imyeyu.api.modules.gao.ai.service.implement.GaoAiSessionServiceImplement.GaoAiSessionException;
import com.imyeyu.api.modules.gao.bean.GaoSystemSetting;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -20,23 +21,21 @@ import java.security.MessageDigest;
@RequiredArgsConstructor @RequiredArgsConstructor
public class GaoAiMcpAuthentication { public class GaoAiMcpAuthentication {
private final GaoAiProperties properties; private final SettingService settingService;
private final GaoAiSessionService sessionService; private final GaoAiSessionService sessionService;
/// 校验 Dify 服务密钥和 AI 会话 /// 校验 Dify 服务密钥和 AI 会话
/// ///
/// @param authorization Authorization 请求头 /// @param authorization Authorization 请求头
/// @param sessionId X-Timi-AI-Session 请求头 /// @param headerSessionId X-Timi-AI-Session 请求头
/// @param argumentSessionId MCP 工具参数中的 AI 会话 ID
/// @param toolName 工具名称,可为空 /// @param toolName 工具名称,可为空
/// @return 已认证会话 /// @return 已认证会话
public GaoAiSession authenticate(String authorization, String sessionId, String toolName) { public GaoAiSession authenticate(String authorization, String headerSessionId, String argumentSessionId, String toolName) {
authenticateService(authorization); authenticateService(authorization);
if (isBlank(sessionId) && properties.isAllowMissingSession()) { String sessionId = headerSessionId;
GaoAiSession session = debugSession(); if (isBlank(sessionId) && !isBlank(argumentSessionId)) {
if (toolName != null && !session.getAllowedTools().contains(toolName)) { sessionId = argumentSessionId;
throw new GaoAiSessionException("AI 会话无权调用该工具", 403);
}
return session;
} }
GaoAiSession session = sessionService.require(sessionId); GaoAiSession session = sessionService.require(sessionId);
if (toolName != null && (session.getAllowedTools() == null || !session.getAllowedTools().contains(toolName))) { if (toolName != null && (session.getAllowedTools() == null || !session.getAllowedTools().contains(toolName))) {
@@ -51,9 +50,9 @@ public class GaoAiMcpAuthentication {
/// ///
/// @param authorization Authorization 请求头 /// @param authorization Authorization 请求头
public void authenticateService(String authorization) { public void authenticateService(String authorization) {
String expectedKey = trim(properties.getMcpServiceKey()); String key = settingService.getSystem(GaoSystemSetting.DIFY_MCP_SERVICE_KEY).asString();
String actualKey = bearerValue(authorization); String actualKey = bearerValue(authorization);
if (expectedKey.isEmpty() || actualKey.isEmpty() || !constantTimeEquals(expectedKey, actualKey)) { if (key.isEmpty() || actualKey.isEmpty() || !constantTimeEquals(key, actualKey)) {
throw new GaoAiSessionException("MCP 服务认证失败", 401); throw new GaoAiSessionException("MCP 服务认证失败", 401);
} }
} }
@@ -77,24 +76,6 @@ public class GaoAiMcpAuthentication {
return value == null ? "" : value.trim(); return value == null ? "" : value.trim();
} }
/// 创建固定的开发测试会话
///
/// @return 开发测试会话
private GaoAiSession debugSession() {
String userId = trim(properties.getDebugUserId());
String storeId = trim(properties.getDebugStoreId());
if (userId.isEmpty() || storeId.isEmpty()) {
throw new GaoAiSessionException("开发测试会话缺少用户或门店配置", 401);
}
GaoAiSession session = new GaoAiSession();
session.setSessionId("dify-agent-debug-bypass");
session.setUserId(userId);
session.setStoreId(storeId);
session.setAllowedTools(GaoAiToolNames.orderedReadOnlyTools());
session.setExpiresAt(Long.MAX_VALUE);
return session;
}
/// 判断字符串是否为空 /// 判断字符串是否为空
/// ///
/// @param value 待判断字符串 /// @param value 待判断字符串
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.ai.mcp;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.modules.gao.ai.entity.GaoAiAuditLog; import com.imyeyu.api.modules.gao.ai.entity.GaoAiAuditLog;
import com.imyeyu.api.modules.gao.ai.entity.GaoAiSession; import com.imyeyu.api.modules.gao.ai.entity.GaoAiSession;
import com.imyeyu.api.modules.gao.ai.mcp.tool.GaoAiTool; import com.imyeyu.api.modules.gao.ai.mcp.tool.GaoAiTool;
@@ -90,12 +91,13 @@ public class GaoAiMcpController {
yield response(200, success(id, toolsResult())); yield response(200, success(id, toolsResult()));
} }
case "tools/call" -> { case "tools/call" -> {
GaoAiSession session = authentication.authenticate(authorization, aiSessionId, text(request.path("params"), "name")); JsonNode params = request.path("params");
if (!hasText(aiSessionId)) { String argumentSessionId = text(params.path("arguments"), "aiSessionId");
log.warn("GAO MCP 使用开发测试会话:toolName={}, userId={}, storeId={}", GaoAiSession session = authentication.authenticate(authorization, aiSessionId, argumentSessionId, text(params, "name"));
toolName, session.getUserId(), session.getStoreId()); if (hasText(argumentSessionId) && !hasText(aiSessionId)) {
log.info("GAO MCP 使用工具参数中的 AI 会话:toolName={}, aiSessionLength={}", toolName, length(argumentSessionId));
} }
yield response(200, success(id, executeTool(session, request.path("params"), aiSessionId))); yield response(200, success(id, executeTool(session, params, hasText(aiSessionId) ? aiSessionId : argumentSessionId)));
} }
default -> response(400, error(id, -32601, "MCP 方法不支持")); default -> response(400, error(id, -32601, "MCP 方法不支持"));
}; };
@@ -126,12 +128,14 @@ public class GaoAiMcpController {
if (arguments == null || !arguments.isObject()) { if (arguments == null || !arguments.isObject()) {
throw new GaoAiToolException("工具参数格式无效", 400); throw new GaoAiToolException("工具参数格式无效", 400);
} }
if (containsField(arguments, "storeId")) { ObjectNode businessArguments = ((ObjectNode) arguments).deepCopy();
businessArguments.remove("aiSessionId");
if (containsField(businessArguments, "storeId")) {
throw new GaoAiToolException("工具请求不允许携带 storeId", 403); throw new GaoAiToolException("工具请求不允许携带 storeId", 403);
} }
GaoAiToolRequest request; GaoAiToolRequest request;
try { try {
request = jackson.treeToValue(arguments, GaoAiToolRequest.class); request = jackson.treeToValue(businessArguments, GaoAiToolRequest.class);
} catch (Exception e) { } catch (Exception e) {
throw new GaoAiToolException("工具参数格式无效", 400); throw new GaoAiToolException("工具参数格式无效", 400);
} }
@@ -4,6 +4,7 @@ import com.imyeyu.api.modules.gao.ai.entity.GaoAiSession;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiToolRequest; import com.imyeyu.api.modules.gao.ai.vo.GaoAiToolRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiToolResult; import com.imyeyu.api.modules.gao.ai.vo.GaoAiToolResult;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
/// GAO 智能体只读工具 /// GAO 智能体只读工具
@@ -36,15 +37,19 @@ public interface GaoAiTool {
), ),
"additionalProperties", false "additionalProperties", false
); );
Map<String, Object> properties = new LinkedHashMap<>();
properties.put("query", Map.of("type", "string", "description", "自然语言关键词"));
properties.put("timeRange", timeRange);
properties.put("filters", Map.of("type", "object"));
properties.put("pageSize", Map.of("type", "integer", "minimum", 1, "maximum", 20));
properties.put("cursor", Map.of("type", "string"));
properties.put("aiSessionId", Map.of(
"type", "string",
"description", "由工作流变量注入的 AI 会话 ID,不允许由模型生成"
));
return Map.of( return Map.of(
"type", "object", "type", "object",
"properties", Map.of( "properties", properties,
"query", Map.of("type", "string", "description", "自然语言关键词"),
"timeRange", timeRange,
"filters", Map.of("type", "object"),
"pageSize", Map.of("type", "integer", "minimum", 1, "maximum", 20),
"cursor", Map.of("type", "string")
),
"additionalProperties", false "additionalProperties", false
); );
} }
@@ -1,6 +1,9 @@
package com.imyeyu.api.modules.gao.ai.service; package com.imyeyu.api.modules.gao.ai.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiChatRequest; import com.imyeyu.api.modules.gao.ai.vo.GaoAiChatRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiConversationListRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiConversationReadRequest;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/// GAO 智能体聊天服务 /// GAO 智能体聊天服务
@@ -14,4 +17,21 @@ public interface GaoAiChatService {
/// @param request 聊天请求 /// @param request 聊天请求
/// @return SSE 发射器 /// @return SSE 发射器
SseEmitter chat(GaoAiChatRequest request); SseEmitter chat(GaoAiChatRequest request);
/// 列出当前登录用户的历史会话
///
/// @param request 历史会话分页请求
/// @return 历史会话分页结果
JsonNode listConversation(GaoAiConversationListRequest request);
/// 读取当前登录用户的历史会话消息
///
/// @param request 历史会话读取请求
/// @return 会话消息分页结果
JsonNode readConversation(GaoAiConversationReadRequest request);
/// 删除当前登录用户的历史会话
///
/// @param conversationId Dify 会话 ID
void deleteConversation(String conversationId);
} }
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.ai.service.implement;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.common.service.SettingService; import com.imyeyu.api.modules.common.service.SettingService;
@@ -13,6 +14,8 @@ import com.imyeyu.api.modules.gao.ai.service.GaoAiAuditService;
import com.imyeyu.api.modules.gao.ai.service.GaoAiChatService; import com.imyeyu.api.modules.gao.ai.service.GaoAiChatService;
import com.imyeyu.api.modules.gao.ai.service.GaoAiSessionService; import com.imyeyu.api.modules.gao.ai.service.GaoAiSessionService;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiChatRequest; import com.imyeyu.api.modules.gao.ai.vo.GaoAiChatRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiConversationListRequest;
import com.imyeyu.api.modules.gao.ai.vo.GaoAiConversationReadRequest;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode; import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.bean.GaoSystemSetting; import com.imyeyu.api.modules.gao.bean.GaoSystemSetting;
import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.api.modules.gao.entity.GaoStore;
@@ -33,6 +36,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.URI; import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
@@ -101,13 +105,7 @@ public class GaoAiChatServiceImplement implements GaoAiChatService {
if (request.getConversationId() != null && 128 < request.getConversationId().length()) { if (request.getConversationId() != null && 128 < request.getConversationId().length()) {
throw new GaoAiAccessException("会话 ID 无效", 400); throw new GaoAiAccessException("会话 ID 无效", 400);
} }
User user = userLoginService.getLoginUser(); User user = requireAiUser();
if (user == null) {
throw new GaoAiAccessException("登录已过期,请重新登录", 401);
}
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name())) {
throw new GaoAiAccessException("当前账号无权使用门店智能助手", 403);
}
String storeId; String storeId;
try { try {
storeId = storeScopeService.resolveStoreId(request.getStoreId()); storeId = storeScopeService.resolveStoreId(request.getStoreId());
@@ -122,6 +120,50 @@ public class GaoAiChatServiceImplement implements GaoAiChatService {
return stream(session, message); return stream(session, message);
} }
@Override
public JsonNode listConversation(GaoAiConversationListRequest request) {
User user = requireAiUser();
String lastId = optionalValue(request == null ? null : request.getLastId(), "上一页游标", 128);
int limit = normalizeLimit(request == null ? null : request.getLimit());
String sortBy = normalizeSortBy(request == null ? null : request.getSortBy());
StringBuilder url = new StringBuilder("/conversations");
appendQueryParameter(url, "user", difyUser(user));
appendQueryParameter(url, "last_id", lastId);
appendQueryParameter(url, "limit", String.valueOf(limit));
appendQueryParameter(url, "sort_by", sortBy);
return sanitizeConversationList(parseJson(sendDify(newDifyRequest(url.toString()).GET().build(), "查询历史会话")));
}
@Override
public JsonNode readConversation(GaoAiConversationReadRequest request) {
User user = requireAiUser();
if (request == null) {
throw new GaoAiAccessException("会话读取参数不能为空", 400);
}
String conversationId = requiredValue(request.getConversationId(), "会话 ID", 128);
String firstId = optionalValue(request.getFirstId(), "上一页游标", 128);
int limit = normalizeLimit(request.getLimit());
StringBuilder url = new StringBuilder("/messages");
appendQueryParameter(url, "user", difyUser(user));
appendQueryParameter(url, "conversation_id", conversationId);
appendQueryParameter(url, "first_id", firstId);
appendQueryParameter(url, "limit", String.valueOf(limit));
return sanitizeConversationMessages(parseJson(sendDify(newDifyRequest(url.toString()).GET().build(), "读取历史会话")));
}
@Override
public void deleteConversation(String conversationId) {
User user = requireAiUser();
String value = requiredValue(conversationId, "会话 ID", 128);
ObjectNode body = jackson.createObjectNode();
body.put("user", difyUser(user));
HttpRequest request = newDifyRequest("/conversations/" + encodePathSegment(value))
.method("DELETE", HttpRequest.BodyPublishers.ofString(writeJson(body), StandardCharsets.UTF_8))
.header("Content-Type", "application/json")
.build();
sendDify(request, "删除历史会话");
}
private SseEmitter stream(GaoAiSession session, String message) { private SseEmitter stream(GaoAiSession session, String message) {
long readTimeoutSeconds = positive(properties.getReadTimeoutSeconds(), 600L); long readTimeoutSeconds = positive(properties.getReadTimeoutSeconds(), 600L);
long emitterTimeout = Math.min(Long.MAX_VALUE - 1L, readTimeoutSeconds * 1000L + 30_000L); long emitterTimeout = Math.min(Long.MAX_VALUE - 1L, readTimeoutSeconds * 1000L + 30_000L);
@@ -190,19 +232,250 @@ public class GaoAiChatServiceImplement implements GaoAiChatService {
GaoStore store = storeService.get(session.getStoreId()); GaoStore store = storeService.get(session.getStoreId());
inputs.put("storeName", store == null ? "当前门店" : store.getName()); inputs.put("storeName", store == null ? "当前门店" : store.getName());
inputs.put("today", LocalDate.now(ZONE).toString()); inputs.put("today", LocalDate.now(ZONE).toString());
String url = settingService.getSystem(DifySetting.BASE_URL).asString(); return newDifyRequest("/chat-messages")
String appApiKey = settingService.getSystem(GaoSystemSetting.DIFY_APP_API_KEY).asString();
return HttpRequest.newBuilder()
.uri(URI.create(url + "/chat-messages"))
.timeout(Duration.ofSeconds(readTimeoutSeconds)) .timeout(Duration.ofSeconds(readTimeoutSeconds))
.header("Authorization", "Bearer " + appApiKey)
.header("X-Timi-AI-Session", session.getSessionId()) .header("X-Timi-AI-Session", session.getSessionId())
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("Accept", "text/event-stream") .setHeader("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(writeJson(body), StandardCharsets.UTF_8)) .POST(HttpRequest.BodyPublishers.ofString(writeJson(body), StandardCharsets.UTF_8))
.build(); .build();
} }
/// 获取当前登录且有权使用智能体的用户
///
/// @return 当前登录用户
private User requireAiUser() {
User user = userLoginService.getLoginUser();
if (user == null) {
throw new GaoAiAccessException("登录已过期,请重新登录", 401);
}
if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name())) {
throw new GaoAiAccessException("当前账号无权使用门店智能助手", 403);
}
return user;
}
/// 创建 Dify API 请求
///
/// @param path Dify API 路径,可包含已编码的查询参数
/// @return Dify API 请求构造器
private HttpRequest.Builder newDifyRequest(String path) {
String appApiKey = settingService.getSystem(GaoSystemSetting.DIFY_APP_API_KEY).asString();
if (isBlank(appApiKey)) {
throw new GaoAiAccessException("智能体服务未配置", 503);
}
return HttpRequest.newBuilder(difyUri(path))
.timeout(Duration.ofSeconds(Math.min(60L, positive(properties.getReadTimeoutSeconds(), 600L))))
.header("Authorization", "Bearer " + appApiKey)
.header("Accept", "application/json");
}
/// 调用 Dify API
///
/// @param request Dify API 请求
/// @param operation 操作名称
/// @return Dify 响应正文
private String sendDify(HttpRequest request, String operation) {
HttpResponse<String> response;
try {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new GaoAiAccessException("智能体服务请求被中断", 503);
} catch (IOException | RuntimeException e) {
log.warn("{}失败", operation, e);
throw new GaoAiAccessException("智能体服务暂时不可用,请稍后重试", 503);
}
if (response.statusCode() < 200 || 299 < response.statusCode()) {
log.warn("{}失败:statusCode={}", operation, response.statusCode());
int status = response.statusCode() == 404 ? 404 : 503;
String message = status == 404 ? "历史会话不存在" : "智能体服务暂时不可用,请稍后重试";
throw new GaoAiAccessException(message, status);
}
return response.body();
}
/// 解析 Dify JSON 响应
///
/// @param responseBody 响应正文
/// @return JSON 节点
private JsonNode parseJson(String responseBody) {
try {
return jackson.readTree(responseBody);
} catch (IOException | RuntimeException e) {
log.warn("解析智能体历史会话响应失败", e);
throw new GaoAiAccessException("智能体服务返回数据无效", 503);
}
}
/// 过滤会话列表中的 Dify 内部字段
///
/// @param response Dify 响应
/// @return 脱敏后的响应
private JsonNode sanitizeConversationList(JsonNode response) {
if (response == null || !response.isObject()) {
return response;
}
ObjectNode result = jackson.createObjectNode();
copyFields(response, result, "has_more", "limit");
ArrayNode data = result.putArray("data");
for (JsonNode item : response.path("data")) {
if (!item.isObject()) {
continue;
}
ObjectNode conversation = data.addObject();
copyFields(item, conversation, "id", "name", "status", "introduction", "created_at", "updated_at");
}
return result;
}
/// 过滤会话消息中的 Dify 内部字段
///
/// @param response Dify 响应
/// @return 脱敏后的响应
private JsonNode sanitizeConversationMessages(JsonNode response) {
if (response == null || !response.isObject()) {
return response;
}
ObjectNode result = jackson.createObjectNode();
copyFields(response, result, "has_more", "limit");
ArrayNode data = result.putArray("data");
for (JsonNode item : response.path("data")) {
if (!item.isObject()) {
continue;
}
ObjectNode message = data.addObject();
copyFields(item, message, "id", "conversation_id", "query", "answer", "created_at", "status", "feedback");
}
return result;
}
/// 复制指定的 JSON 字段
///
/// @param source 源 JSON 对象
/// @param target 目标 JSON 对象
/// @param fieldNames 字段名称
/// @return 无
private void copyFields(JsonNode source, ObjectNode target, String... fieldNames) {
for (String fieldName : fieldNames) {
if (source.has(fieldName)) {
target.set(fieldName, source.get(fieldName));
}
}
}
/// 生成 Dify 用户标识
///
/// @param user 登录用户
/// @return Dify 用户标识
private String difyUser(User user) {
return "gao-store-manager:" + user.getId();
}
/// 添加 URL 查询参数
///
/// @param url URL 构造器
/// @param name 参数名称
/// @param value 参数值
/// @return 无
private void appendQueryParameter(StringBuilder url, String name, String value) {
if (isBlank(value)) {
return;
}
url.append(url.indexOf("?") < 0 ? '?' : '&')
.append(name)
.append('=')
.append(URLEncoder.encode(value, StandardCharsets.UTF_8));
}
/// 生成 Dify API URI
///
/// @param path Dify API 路径
/// @return Dify API URI
private URI difyUri(String path) {
String baseUrl = settingService.getSystem(DifySetting.BASE_URL).asString();
if (isBlank(baseUrl)) {
throw new GaoAiAccessException("智能体服务未配置", 503);
}
String normalizedBaseUrl = baseUrl.trim();
while (normalizedBaseUrl.endsWith("/")) {
normalizedBaseUrl = normalizedBaseUrl.substring(0, normalizedBaseUrl.length() - 1);
}
try {
URI uri = URI.create(normalizedBaseUrl + (path.startsWith("/") ? path : "/" + path));
if (!uri.isAbsolute() || isBlank(uri.getScheme()) || isBlank(uri.getHost())) {
throw new IllegalArgumentException("Dify 地址必须是绝对 HTTP 地址");
}
return uri;
} catch (IllegalArgumentException e) {
throw new GaoAiAccessException("智能体服务配置无效", 503);
}
}
/// 编码 URL 路径片段
///
/// @param value 路径片段
/// @return 编码后的路径片段
private String encodePathSegment(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20");
}
/// 标准化分页条数
///
/// @param limit 请求的分页条数
/// @return 标准化后的分页条数
private int normalizeLimit(Integer limit) {
int value = limit == null ? 20 : limit;
if (!(0 < value) || 100 < value) {
throw new GaoAiAccessException("分页条数必须在 1 至 100 之间", 400);
}
return value;
}
/// 标准化会话排序方式
///
/// @param sortBy 请求的排序方式
/// @return 标准化后的排序方式
private String normalizeSortBy(String sortBy) {
String value = isBlank(sortBy) ? "-updated_at" : sortBy.trim();
if (!"created_at".equals(value) && !"-created_at".equals(value)
&& !"updated_at".equals(value) && !"-updated_at".equals(value)) {
throw new GaoAiAccessException("会话排序方式无效", 400);
}
return value;
}
/// 校验必填文本参数
///
/// @param value 参数值
/// @param name 参数名称
/// @param maxLength 最大长度
/// @return 去除首尾空白后的参数值
private String requiredValue(String value, String name, int maxLength) {
String result = optionalValue(value, name, maxLength);
if (result == null) {
throw new GaoAiAccessException(name + "不能为空", 400);
}
return result;
}
/// 校验可选文本参数
///
/// @param value 参数值
/// @param name 参数名称
/// @param maxLength 最大长度
/// @return 去除首尾空白后的参数值,无值时返回 null
private String optionalValue(String value, String name, int maxLength) {
if (isBlank(value)) {
return null;
}
String result = value.trim();
if (maxLength < result.length()) {
throw new GaoAiAccessException(name + "无效", 400);
}
return result;
}
private void readResponse(HttpResponse<InputStream> response, SseEmitter emitter, AtomicBoolean finished, private void readResponse(HttpResponse<InputStream> response, SseEmitter emitter, AtomicBoolean finished,
AtomicReference<CompletableFuture<HttpResponse<InputStream>>> upstreamRef, AtomicReference<ScheduledFuture<?>> timeoutRef, AtomicReference<CompletableFuture<HttpResponse<InputStream>>> upstreamRef, AtomicReference<ScheduledFuture<?>> timeoutRef,
AtomicReference<String> status, AtomicBoolean auditRecorded, GaoAiSession session, String message, long startedAt, AtomicReference<String> status, AtomicBoolean auditRecorded, GaoAiSession session, String message, long startedAt,
@@ -228,7 +501,11 @@ public class GaoAiChatServiceImplement implements GaoAiChatService {
if (line.startsWith("event:")) { if (line.startsWith("event:")) {
sseEvent.set(line.substring(6).trim()); sseEvent.set(line.substring(6).trim());
} else if (line.startsWith("data:")) { } else if (line.startsWith("data:")) {
data.append(line.substring(5).trim()); if (!data.isEmpty()) {
data.append('\n');
}
String dataLine = line.substring(5);
data.append(dataLine.startsWith(" ") ? dataLine.substring(1) : dataLine);
} else if (line.isEmpty() && !data.isEmpty()) { } else if (line.isEmpty() && !data.isEmpty()) {
processData(data.toString(), sseEvent.get(), emitter, status, finished, timeoutRef, auditRecorded, processData(data.toString(), sseEvent.get(), emitter, status, finished, timeoutRef, auditRecorded,
session, message, startedAt); session, message, startedAt);
@@ -292,9 +569,13 @@ public class GaoAiChatServiceImplement implements GaoAiChatService {
} }
switch (event) { switch (event) {
case "message", "agent_message", "text_chunk" -> { case "message", "agent_message", "text_chunk" -> {
String answer = firstText(text(payload, "answer", null), text(payload, "text", null)); String answer = firstRawText(payload, "answer");
if (answer != null) { if (answer == null) {
safeSend(emitter, SseEmitter.event().name("message").data(answer), finished); answer = firstRawText(payload, "text");
}
if (answer != null && !answer.isEmpty()) {
Map<String, String> messageData = Map.of("answer", answer);
safeSend(emitter, SseEmitter.event().name("message").data(messageData), finished);
} }
} }
case "message_end" -> emitDone(emitter, session, startedAt, finished, timeoutRef, auditRecorded, message, status); case "message_end" -> emitDone(emitter, session, startedAt, finished, timeoutRef, auditRecorded, message, status);
@@ -446,6 +727,13 @@ public class GaoAiChatServiceImplement implements GaoAiChatService {
return fallback; return fallback;
} }
private String firstRawText(JsonNode node, String field) {
if (node != null && node.hasNonNull(field)) {
return node.get(field).asText();
}
return null;
}
private String firstText(String... values) { private String firstText(String... values) {
for (String value : values) { for (String value : values) {
if (!isBlank(value)) { if (!isBlank(value)) {
@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.gao.ai.vo;
import lombok.Data;
/// GAO 智能体历史会话列表请求
///
/// 字段与 Dify 会话列表分页参数对应,但用户标识由后端注入
///
/// @author Codex
/// @since 2026-09-16
@Data
public class GaoAiConversationListRequest {
/// 上一页最后一条会话 ID,可为空
private String lastId;
/// 返回条数,可为空
private Integer limit;
/// 排序方式,可选 `created_at`、`-created_at`、`updated_at`、`-updated_at`
private String sortBy;
}
@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.gao.ai.vo;
import lombok.Data;
/// GAO 智能体历史会话消息读取请求
///
/// 用户标识由后端注入,前端只能指定会话和分页游标
///
/// @author Codex
/// @since 2026-09-16
@Data
public class GaoAiConversationReadRequest {
/// Dify 会话 ID
private String conversationId;
/// 上一页第一条消息 ID,可为空
private String firstId;
/// 返回条数,可为空
private Integer limit;
}
@@ -17,11 +17,11 @@ public enum GaoSystemSetting implements SettingEnum {
DIFY_APP_API_KEY(null, Setting.ValueType.STRING), DIFY_APP_API_KEY(null, Setting.ValueType.STRING),
SERVICE_LICENSE(null, Setting.ValueType.STRING), DIFY_MCP_SERVICE_KEY(null, Setting.ValueType.STRING),
PRIVACY_LICENSE(null, Setting.ValueType.STRING), AI_AGENT_EXAMPLE(null, Setting.ValueType.JSON_LIST),
OPEN_SOURCE_LICENSE(null, Setting.ValueType.STRING); AGREEMENT(null, Setting.ValueType.JSON_OBJECT);
final String defaultValue; final String defaultValue;
@@ -69,6 +69,7 @@ public class GaoEventRecordController {
result.setTotal(pageResult.getTotal()); result.setTotal(pageResult.getTotal());
result.setList(pageResult.getList()); result.setList(pageResult.getList());
result.setNumberValueTotal(resolveNumberValueTotal(page)); result.setNumberValueTotal(resolveNumberValueTotal(page));
result.setCustomerCount(service.countCustomerByRange(page));
{ {
List<String> customerIdList = result.getList().stream().map(GaoEventRecord::getCustomerId).toList(); List<String> customerIdList = result.getList().stream().map(GaoEventRecord::getCustomerId).toList();
Map<String, GaoEvent> eventMap = eventService.mapByIdList(result.getList().stream().map(GaoEventRecord::getEventId).toList()); Map<String, GaoEvent> eventMap = eventService.mapByIdList(result.getList().stream().map(GaoEventRecord::getEventId).toList());
@@ -4,7 +4,9 @@ import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord; import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoFundRecordService; import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService; import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
@@ -39,6 +41,7 @@ import java.util.stream.Stream;
public class GaoFundRecordController { public class GaoFundRecordController {
private final GaoFundRecordService service; private final GaoFundRecordService service;
private final GaoCustomerService customerService;
private final GaoOrderPaymentService orderPaymentService; private final GaoOrderPaymentService orderPaymentService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
@@ -124,10 +127,23 @@ public class GaoFundRecordController {
.distinct() .distinct()
.toList(); .toList();
Map<String, User> userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); Map<String, User> userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
List<String> customerIdList = recordList.stream()
.map(GaoFundRecord::getCustomerId)
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, GaoCustomer> customerMap = customerIdList.isEmpty() ? Map.of() : customerService.mapByIdList(customerIdList);
Map<String, List<Attachment>> customerPhotoMap = customerIdList.isEmpty()
? Map.of() : attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList, GaoCustomer.AttachType.PHOTO.name());
for (GaoFundRecord record : recordList) { for (GaoFundRecord record : recordList) {
record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of())); record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of()));
record.setOperator(userMap.get(record.getOperatorUserId())); record.setOperator(userMap.get(record.getOperatorUserId()));
record.setVerifier(userMap.get(record.getVerifierUserId())); record.setVerifier(userMap.get(record.getVerifierUserId()));
GaoCustomer customer = customerMap.get(record.getCustomerId());
if (customer != null) {
customer.setAttachmentList(customerPhotoMap.getOrDefault(customer.getId(), List.of()));
}
record.setCustomer(customer);
} }
} }
} }
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.bean.PermissionMatchMode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoCustomer;
@@ -78,6 +79,16 @@ public class GaoOrderFulfillmentController {
return result; return result;
} }
@AOPLog
@RequestRateLimit
@RequireGaoPermission(value = { GaoPermissionCode.ORDER_FULFILLMENT_CREATE, GaoPermissionCode.ORDER_FULFILLMENT_VERIFY }, mode = PermissionMatchMode.ALL)
@PostMapping("/create-and-verify")
public GaoOrderFulfillment createAndVerify(@RequestBody GaoOrderFulfillmentCreateRequest request) {
GaoOrderFulfillment result = service.createAndVerify(request);
fillTransient(List.of(result));
return result;
}
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_VERIFY) @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_VERIFY)
@@ -101,8 +101,8 @@ public class GaoPackageConfigController {
if (skuIdList.isEmpty()) { if (skuIdList.isEmpty()) {
return; return;
} }
Map<String, GaoProductSku> skuMap = skuIdList.stream() Map<String, GaoProductSku> skuMap = skuService.listByIdList(skuIdList).stream()
.collect(java.util.stream.Collectors.toMap(item -> item, skuService::detail)); .collect(java.util.stream.Collectors.toMap(GaoProductSku::getId, item -> item));
for (GaoPackageConfig config : configList) { for (GaoPackageConfig config : configList) {
for (GaoPackageConfigItem item : config.getItemList()) { for (GaoPackageConfigItem item : config.getItemList()) {
item.setSku(skuMap.get(item.getSkuId())); item.setSku(skuMap.get(item.getSkuId()));
@@ -7,6 +7,7 @@ import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord;
import com.imyeyu.api.modules.gao.service.GaoProductStockService; import com.imyeyu.api.modules.gao.service.GaoProductStockService;
import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService; import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoProductStockBatchRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest; import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest; import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
@@ -23,6 +24,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/// GAO 产品库存接口 /// GAO 产品库存接口
/// ///
/// @author Codex /// @author Codex
@@ -64,6 +67,16 @@ public class GaoProductStockController {
return stockService.detail(storeScopeService.resolveStoreId(storeId), skuId); return stockService.detail(storeScopeService.resolveStoreId(storeId), skuId);
} }
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ)
@PostMapping("/batch-detail")
public List<GaoProductStock> batchDetail(@RequestBody GaoProductStockBatchRequest request) {
TimiException.required(request, "not found stock batch request");
request.setStoreId(storeScopeService.resolveStoreId(request.getStoreId()));
return stockService.batchDetail(request.getStoreId(), request.getSkuIdList());
}
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE) @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE)
@@ -102,6 +102,14 @@ public class GaoSalesOrderController {
return paymentService.receive(request); return paymentService.receive(request);
} }
@AOPLog
@RequestRateLimit
@RequireGaoPermission(value = { GaoPermissionCode.FUND_RECEIVE_CREATE, GaoPermissionCode.FUND_RECEIVE_VERIFY }, mode = PermissionMatchMode.ALL)
@PostMapping("/payment/receive-and-verify")
public GaoFundRecord receiveAndVerify(@RequestBody GaoOrderPaymentReceiveRequest request) {
return paymentService.receiveAndVerify(request);
}
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY) @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY)
@@ -120,6 +120,10 @@ public class GaoFundRecord extends UUIDEntity {
@Transient @Transient
private User verifier; private User verifier;
/// 所属客户
@Transient
private GaoCustomer customer;
/// 附件 /// 附件
@Transient @Transient
private List<Attachment> attachmentList; private List<Attachment> attachmentList;
@@ -168,6 +168,10 @@ public class GaoProductStockRecord implements IDEntity<String>, Creatable {
@Transient @Transient
private User operator; private User operator;
/// 所属客户
@Transient
private GaoCustomer customer;
/// 附件 /// 附件
@Transient @Transient
private List<Attachment> attachmentList; private List<Attachment> attachmentList;
@@ -87,6 +87,12 @@ public interface GaoEventRecordMapper extends BaseMapper<GaoEventRecord, String>
/// @return 登记事件记录列表 /// @return 登记事件记录列表
List<GaoEventRecord> selectByRange(GaoEventRecordPage page); List<GaoEventRecord> selectByRange(GaoEventRecordPage page);
/// 按登记记录列表条件统计去重客户数量
///
/// @param page 登记记录范围查询请求
/// @return 去重后的客户数量
long countCustomerByRange(GaoEventRecordPage page);
/// 按登记记录列表条件汇总数值 /// 按登记记录列表条件汇总数值
/// ///
/// @param page 登记记录范围查询请求 /// @param page 登记记录范围查询请求
@@ -5,6 +5,7 @@ import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List; import java.util.List;
/// GAO 统一履约 Mapper /// GAO 统一履约 Mapper
@@ -19,6 +20,9 @@ public interface GaoOrderFulfillmentMapper extends BaseMapper<GaoOrderFulfillmen
/// 按 ID 查询履约单 /// 按 ID 查询履约单
GaoOrderFulfillment selectById(@Param("id") String id); GaoOrderFulfillment selectById(@Param("id") String id);
/// 按 ID 批量查询履约单
List<GaoOrderFulfillment> selectByIdList(@Param("idList") Collection<String> idList);
/// 锁定履约单 /// 锁定履约单
GaoOrderFulfillment selectForUpdate(@Param("id") String id); GaoOrderFulfillment selectForUpdate(@Param("id") String id);
@@ -32,6 +32,9 @@ public interface GaoProductStockMapper extends BaseMapper<GaoProductStock, Strin
/// 按门店和产品查询库存余额 /// 按门店和产品查询库存余额
GaoProductStock selectByStoreIdAndSkuId(@Param("storeId") String storeId, @Param("skuId") String skuId); GaoProductStock selectByStoreIdAndSkuId(@Param("storeId") String storeId, @Param("skuId") String skuId);
/// 按门店和 SKU 批量查询库存余额
List<GaoProductStock> selectByStoreIdAndSkuIdList(@Param("storeId") String storeId, @Param("skuIdList") List<String> skuIdList);
/// 条件更新库存余额 /// 条件更新库存余额
int updateQuantity(@Param("id") String id, @Param("onHandDelta") java.math.BigDecimal onHandDelta, @Param("reservedDelta") java.math.BigDecimal reservedDelta, @Param("updatedBy") String updatedBy); int updateQuantity(@Param("id") String id, @Param("onHandDelta") java.math.BigDecimal onHandDelta, @Param("reservedDelta") java.math.BigDecimal reservedDelta, @Param("updatedBy") String updatedBy);
@@ -117,6 +117,12 @@ public interface GaoEventRecordService extends BaseService<GaoEventRecord, Strin
/// @return 登记事件记录列表 /// @return 登记事件记录列表
PageResult<GaoEventRecord> pageByRange(GaoEventRecordPage req); PageResult<GaoEventRecord> pageByRange(GaoEventRecordPage req);
/// 按登记记录列表条件统计去重客户数量
///
/// @param req 登记记录范围查询请求
/// @return 去重后的客户数量
long countCustomerByRange(GaoEventRecordPage req);
/// 按时间范围统计门店登记数 /// 按时间范围统计门店登记数
/// ///
/// @param req 门店报表查询请求 /// @param req 门店报表查询请求
@@ -32,6 +32,9 @@ public interface GaoOrderFulfillmentService extends BaseService<GaoOrderFulfillm
/// 创建履约单 /// 创建履约单
GaoOrderFulfillment create(GaoOrderFulfillmentCreateRequest request); GaoOrderFulfillment create(GaoOrderFulfillmentCreateRequest request);
/// 创建并直接核销履约单
GaoOrderFulfillment createAndVerify(GaoOrderFulfillmentCreateRequest request);
/// 为库存不足履约单准备库存 /// 为库存不足履约单准备库存
GaoOrderFulfillment prepareStock(GaoOrderFulfillmentPrepareStockRequest request); GaoOrderFulfillment prepareStock(GaoOrderFulfillmentPrepareStockRequest request);
@@ -26,6 +26,9 @@ public interface GaoProductStockService {
/// 查询库存余额详情 /// 查询库存余额详情
GaoProductStock detail(String storeId, String skuId); GaoProductStock detail(String storeId, String skuId);
/// 批量查询指定 SKU 库存余额
List<GaoProductStock> batchDetail(String storeId, List<String> skuIdList);
/// 期初入库 /// 期初入库
GaoProductStock initialIn(GaoProductStockChangeRequest request); GaoProductStock initialIn(GaoProductStockChangeRequest request);
@@ -159,6 +159,17 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
logger.setContent(jackson.writeValueAsString(customer)); logger.setContent(jackson.writeValueAsString(customer));
GaoCustomer dbCustomer = get(customer.getId()); GaoCustomer dbCustomer = get(customer.getId());
TimiException.requiredTrue(dbCustomer.getStoreId().equals(customer.getStoreId()), "customer.storeId invalid"); TimiException.requiredTrue(dbCustomer.getStoreId().equals(customer.getStoreId()), "customer.storeId invalid");
String oldIntroducerCustomerId = dbCustomer.getIntroducerCustomerId();
String requestIntroducerCustomerId = customer.getIntroducerCustomerId();
String introducerCustomerId = requestIntroducerCustomerId == null
? oldIntroducerCustomerId
: TimiJava.defaultIfEmpty(requestIntroducerCustomerId, null);
if (TimiJava.isNotEmpty(introducerCustomerId)) {
TimiException.requiredTrue(!introducerCustomerId.equals(customer.getId()), "customer.introducerCustomerId invalid");
GaoCustomer introducerCustomer = get(introducerCustomerId);
TimiException.required(introducerCustomer, "not found introducer customer");
TimiException.requiredTrue(customer.getStoreId().equals(introducerCustomer.getStoreId()), "介绍人和客户不属于同一门店");
}
String code = TimiJava.defaultIfEmpty(customer.getCode(), null); String code = TimiJava.defaultIfEmpty(customer.getCode(), null);
{ {
if (!Objects.equals(dbCustomer.getCode(), code) && TimiJava.isNotEmpty(code)) { if (!Objects.equals(dbCustomer.getCode(), code) && TimiJava.isNotEmpty(code)) {
@@ -184,7 +195,12 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
dbCustomer.setRemark(customer.getRemark()); dbCustomer.setRemark(customer.getRemark());
dbCustomer.setDisease(customer.getDisease()); dbCustomer.setDisease(customer.getDisease());
dbCustomer.setConditioningPart(customer.getConditioningPart()); dbCustomer.setConditioningPart(customer.getConditioningPart());
dbCustomer.setIntroducerCustomerId(introducerCustomerId);
mapper.update(dbCustomer); mapper.update(dbCustomer);
if (!Objects.equals(oldIntroducerCustomerId, introducerCustomerId)) {
changeInvitedCount(oldIntroducerCustomerId, -1);
changeInvitedCount(introducerCustomerId, 1);
}
// 标签 // 标签
if (customer.getTagApply() != null) { if (customer.getTagApply() != null) {
@@ -467,6 +467,12 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
return result; return result;
} }
@Override
public long countCustomerByRange(GaoEventRecordPage page) {
TimiException.required(page.getStoreId(), "not found page.storeId");
return mapper.countCustomerByRange(page);
}
@Override @Override
public List<GaoEventRecordDailyStatView> stateDailyByRange(GaoStoreChartReq req) { public List<GaoEventRecordDailyStatView> stateDailyByRange(GaoStoreChartReq req) {
return mapper.stateDailyByRange(req); return mapper.stateDailyByRange(req);
@@ -153,6 +153,19 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
return createInternal(request); return createInternal(request);
} }
@OperationAudit(AuditOperation.GAO_ORDER_FULFILLMENT_CREATE)
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoOrderFulfillment createAndVerify(GaoOrderFulfillmentCreateRequest request) {
GaoOrderFulfillment fulfillment = createInternal(request);
if (fulfillment.getStatus() == GaoOrderFulfillment.Status.VERIFIED) {
return fulfillment;
}
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING,
"库存不足,不能直接核销履约");
return verifyInternal(actionRequest(fulfillment.getId()));
}
private GaoOrderFulfillment createInternal(GaoOrderFulfillmentCreateRequest request) { private GaoOrderFulfillment createInternal(GaoOrderFulfillmentCreateRequest request) {
TimiException.required(request, "not found fulfillment request"); TimiException.required(request, "not found fulfillment request");
String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); String requestNo = requiredText(request.getRequestNo(), "not found requestNo");
@@ -588,6 +601,12 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
} }
} }
private GaoOrderFulfillmentActionRequest actionRequest(String fulfillmentId) {
GaoOrderFulfillmentActionRequest request = new GaoOrderFulfillmentActionRequest();
request.setFulfillmentId(fulfillmentId);
return request;
}
private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) { private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) {
GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem(); GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem();
result.setSkuId(source.getSkuId()); result.setSkuId(source.getSkuId());
@@ -8,7 +8,9 @@ import com.imyeyu.api.modules.gao.entity.GaoProductCategory;
import com.imyeyu.api.modules.gao.entity.GaoProductSpec; import com.imyeyu.api.modules.gao.entity.GaoProductSpec;
import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue; import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue;
import com.imyeyu.api.modules.gao.entity.GaoProductSku; import com.imyeyu.api.modules.gao.entity.GaoProductSku;
import com.imyeyu.api.modules.gao.entity.GaoProductStock;
import com.imyeyu.api.modules.gao.mapper.GaoProductMapper; import com.imyeyu.api.modules.gao.mapper.GaoProductMapper;
import com.imyeyu.api.modules.gao.mapper.GaoProductStockMapper;
import com.imyeyu.api.modules.gao.service.GaoProductCategoryService; import com.imyeyu.api.modules.gao.service.GaoProductCategoryService;
import com.imyeyu.api.modules.gao.service.GaoProductService; import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductSkuService; import com.imyeyu.api.modules.gao.service.GaoProductSkuService;
@@ -48,6 +50,7 @@ import java.util.stream.Collectors;
public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct, String> implements GaoProductService { public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct, String> implements GaoProductService {
private final GaoProductMapper mapper; private final GaoProductMapper mapper;
private final GaoProductStockMapper stockMapper;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final ObjectProvider<GaoProductCategoryService> categoryServiceProvider; private final ObjectProvider<GaoProductCategoryService> categoryServiceProvider;
private final ObjectProvider<GaoProductSkuService> skuServiceProvider; private final ObjectProvider<GaoProductSkuService> skuServiceProvider;
@@ -120,6 +123,9 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
TimiException.required(request.getId(), "not found product.id"); TimiException.required(request.getId(), "not found product.id");
GaoProduct product = get(request.getId()); GaoProduct product = get(request.getId());
TimiException.required(product, "not found product"); TimiException.required(product, "not found product");
if (request.getProductType() == GaoProduct.ProductType.SERVICE && product.getProductType() != GaoProduct.ProductType.SERVICE) {
verifyNoReservedStock(product);
}
product.setCategoryId(request.getCategoryId()); product.setCategoryId(request.getCategoryId());
product.setCode(TimiJava.defaultIfNull(request.getCode(), product.getCode())); product.setCode(TimiJava.defaultIfNull(request.getCode(), product.getCode()));
product.setName(TimiJava.defaultIfNull(request.getName(), product.getName())); product.setName(TimiJava.defaultIfNull(request.getName(), product.getName()));
@@ -139,6 +145,19 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
} }
} }
private void verifyNoReservedStock(GaoProduct product) {
List<String> skuIdList = skuServiceProvider.getObject().listByProductId(product.getId()).stream()
.map(GaoProductSku::getId)
.filter(id -> id != null && !id.isBlank())
.toList();
if (skuIdList.isEmpty()) {
return;
}
boolean hasReservedStock = stockMapper.selectByStoreIdAndSkuIdList(product.getStoreId(), skuIdList).stream()
.anyMatch(stock -> stock.getReservedQuantity() != null && 0 < stock.getReservedQuantity().signum());
TimiException.requiredTrue(!hasReservedStock, "当前产品存在预占库存,不能切换为服务产品,请先完成或取消相关订单");
}
@OperationAudit(AuditOperation.GAO_PRODUCT_DISABLE) @OperationAudit(AuditOperation.GAO_PRODUCT_DISABLE)
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
@@ -2,8 +2,12 @@ package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment;
import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord;
import com.imyeyu.api.modules.gao.mapper.GaoOrderFulfillmentMapper;
import com.imyeyu.api.modules.gao.mapper.GaoProductStockRecordMapper; import com.imyeyu.api.modules.gao.mapper.GaoProductStockRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService; import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService;
import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage; import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage;
import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.entity.User;
@@ -18,6 +22,7 @@ import org.springframework.stereotype.Service;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
/// GAO 库存流水服务实现 /// GAO 库存流水服务实现
/// ///
@@ -28,7 +33,9 @@ import java.util.Map;
public class GaoProductStockRecordServiceImplement extends AbstractEntityService<GaoProductStockRecord, String> implements GaoProductStockRecordService { public class GaoProductStockRecordServiceImplement extends AbstractEntityService<GaoProductStockRecord, String> implements GaoProductStockRecordService {
private final GaoProductStockRecordMapper mapper; private final GaoProductStockRecordMapper mapper;
private final GaoOrderFulfillmentMapper orderFulfillmentMapper;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final GaoCustomerService customerService;
private final UserService userService; private final UserService userService;
@Override @Override
@@ -133,10 +140,46 @@ public class GaoProductStockRecordServiceImplement extends AbstractEntityService
List<String> userIdList = recordList.stream().map(GaoProductStockRecord::getOperatorUserId) List<String> userIdList = recordList.stream().map(GaoProductStockRecord::getOperatorUserId)
.filter(value -> value != null && !value.isBlank()).distinct().toList(); .filter(value -> value != null && !value.isBlank()).distinct().toList();
Map<String, User> userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); Map<String, User> userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList);
Map<String, List<Attachment>> userAvatarMap = userIdList.isEmpty()
? Map.of() : attachmentService.mapByBizIdList(Attachment.BizType.USER, userIdList, User.AttachType.AVATAR.name());
List<String> fulfillmentIdList = recordList.stream()
.filter(record -> record.getBizType() == GaoProductStockRecord.BizType.ORDER_FULFILLMENT)
.map(GaoProductStockRecord::getBizId)
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, GaoOrderFulfillment> fulfillmentMap = fulfillmentIdList.isEmpty()
? Map.of() : orderFulfillmentMapper.selectByIdList(fulfillmentIdList).stream()
.collect(Collectors.toMap(GaoOrderFulfillment::getId, item -> item));
List<String> customerIdList = recordList.stream()
.map(record -> getCustomerId(record, fulfillmentMap))
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
Map<String, GaoCustomer> customerMap = customerIdList.isEmpty() ? Map.of() : customerService.mapByIdList(customerIdList);
Map<String, List<Attachment>> customerPhotoMap = customerIdList.isEmpty()
? Map.of() : attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList, GaoCustomer.AttachType.PHOTO.name());
for (GaoProductStockRecord record : recordList) { for (GaoProductStockRecord record : recordList) {
record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of())); record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of()));
record.setOperator(userMap.get(record.getOperatorUserId())); User operator = userMap.get(record.getOperatorUserId());
if (operator != null) {
operator.setAttachmentList(userAvatarMap.getOrDefault(operator.getId(), List.of()));
}
record.setOperator(operator);
GaoCustomer customer = customerMap.get(getCustomerId(record, fulfillmentMap));
if (customer != null) {
customer.setAttachmentList(customerPhotoMap.getOrDefault(customer.getId(), List.of()));
}
record.setCustomer(customer);
} }
} }
private String getCustomerId(GaoProductStockRecord record, Map<String, GaoOrderFulfillment> fulfillmentMap) {
if (record.getBizType() != GaoProductStockRecord.BizType.ORDER_FULFILLMENT) {
return null;
}
GaoOrderFulfillment fulfillment = fulfillmentMap.get(record.getBizId());
return fulfillment == null ? null : fulfillment.getCustomerId();
}
} }
@@ -68,7 +68,8 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
List<GaoProductStock> list = stockMapper.selectByQuery(page); List<GaoProductStock> list = stockMapper.selectByQuery(page);
Map<String, Attachment> photoMap = attachmentService.mapFirstImageByIdList( Map<String, Attachment> photoMap = attachmentService.mapFirstImageByIdList(
Attachment.BizType.GAO_PRODUCT_SKU, Attachment.BizType.GAO_PRODUCT_SKU,
list.stream().map(GaoProductStock::getSkuId).filter(id -> id != null && !id.isBlank()).toList() list.stream().map(GaoProductStock::getSkuId).filter(id -> id != null && !id.isBlank()).toList(),
"PHOTO"
); );
for (GaoProductStock stock : list) { for (GaoProductStock stock : list) {
fillBalance(stock); fillBalance(stock);
@@ -86,7 +87,17 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
TimiException.required(page, "not found stock product page"); TimiException.required(page, "not found stock product page");
PageResult<GaoProductStockProductView> result = new PageResult<>(); PageResult<GaoProductStockProductView> result = new PageResult<>();
result.setTotal(stockMapper.countProductByQuery(page)); result.setTotal(stockMapper.countProductByQuery(page));
result.setList(stockMapper.selectProductByQuery(page)); List<GaoProductStockProductView> list = stockMapper.selectProductByQuery(page);
Map<String, Attachment> photoMap = attachmentService.mapFirstImageByIdList(
Attachment.BizType.GAO_PRODUCT,
list.stream().map(GaoProductStockProductView::getProductId).filter(id -> id != null && !id.isBlank()).toList(),
"PHOTO"
);
for (GaoProductStockProductView product : list) {
Attachment photo = photoMap.get(product.getProductId());
product.setAttachmentList(photo == null ? List.of() : List.of(photo));
}
result.setList(list);
return result; return result;
} }
@@ -111,6 +122,27 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
return stock; return stock;
} }
@Override
public List<GaoProductStock> batchDetail(String storeId, List<String> skuIdList) {
TimiException.required(storeId, "not found storeId");
storeScopeService.checkStoreId(storeId);
if (skuIdList == null || skuIdList.isEmpty()) {
return List.of();
}
List<String> normalizedSkuIdList = skuIdList.stream()
.filter(value -> value != null && !value.isBlank())
.distinct()
.toList();
if (normalizedSkuIdList.isEmpty()) {
return List.of();
}
List<GaoProductStock> result = stockMapper.selectByStoreIdAndSkuIdList(storeId, normalizedSkuIdList);
for (GaoProductStock stock : result) {
fillBalance(stock);
}
return result;
}
@OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_INITIAL_IN) @OperationAudit(AuditOperation.GAO_PRODUCT_STOCK_INITIAL_IN)
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
@@ -19,4 +19,8 @@ public class GaoEventRecordPageResult extends PageResult<GaoEventRecord> {
/// 指定单个数值事件的登记数值合计,未指定单个 NUMBER 事件时为空 /// 指定单个数值事件的登记数值合计,未指定单个 NUMBER 事件时为空
@JsonView(ResponseView.Public.class) @JsonView(ResponseView.Public.class)
private Long numberValueTotal; private Long numberValueTotal;
/// 查询条件下去重后的客户数量
@JsonView(ResponseView.Public.class)
private Long customerCount;
} }
@@ -0,0 +1,19 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
import java.util.List;
/// GAO 产品库存批量查询请求
///
/// @author Codex
/// @since 2026-09-16
@Data
public class GaoProductStockBatchRequest {
/// 门店 ID
private String storeId;
/// SKU ID 列表
private List<String> skuIdList;
}
@@ -1,9 +1,11 @@
package com.imyeyu.api.modules.gao.vo; package com.imyeyu.api.modules.gao.vo;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.gao.entity.GaoProduct; import com.imyeyu.api.modules.gao.entity.GaoProduct;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
/// 产品库存第一层汇总视图 /// 产品库存第一层汇总视图
/// ///
@@ -21,6 +23,9 @@ public class GaoProductStockProductView {
/// 产品名称 /// 产品名称
private String productName; private String productName;
/// 产品图片
private List<Attachment> attachmentList;
/// 产品类型 /// 产品类型
private GaoProduct.ProductType productType; private GaoProduct.ProductType productType;
@@ -167,6 +167,13 @@
ORDER BY r.`registered_at` DESC, r.`created_at` DESC ORDER BY r.`registered_at` DESC, r.`created_at` DESC
LIMIT #{offset}, #{limit} LIMIT #{offset}, #{limit}
</select> </select>
<select id="countCustomerByRange" resultType="long">
SELECT
COUNT(DISTINCT r.`customer_id`)
<include refid="rangeFrom"/>
WHERE
<include refid="rangeWhere"/>
</select>
<select id="sumNumberValueByRange" resultType="java.lang.Long"> <select id="sumNumberValueByRange" resultType="java.lang.Long">
SELECT SELECT
COALESCE(SUM(r.`number_value`), 0) COALESCE(SUM(r.`number_value`), 0)
@@ -10,6 +10,11 @@
SELECT * FROM `gao_order_fulfillment` SELECT * FROM `gao_order_fulfillment`
WHERE `id` = #{id} WHERE `id` = #{id}
</select> </select>
<select id="selectByIdList" resultType="com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment">
SELECT * FROM `gao_order_fulfillment`
WHERE `id` IN
<foreach collection="idList" item="id" open="(" separator="," close=")">#{id}</foreach>
</select>
<select id="selectForUpdate" resultType="com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment"> <select id="selectForUpdate" resultType="com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment">
SELECT * FROM `gao_order_fulfillment` SELECT * FROM `gao_order_fulfillment`
WHERE `id` = #{id} WHERE `id` = #{id}
@@ -10,6 +10,25 @@
SELECT * FROM `gao_product_stock` SELECT * FROM `gao_product_stock`
WHERE `store_id` = #{storeId} AND `sku_id` = #{skuId} WHERE `store_id` = #{storeId} AND `sku_id` = #{skuId}
</select> </select>
<select id="selectByStoreIdAndSkuIdList" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStock">
SELECT s.`id`,
k.`store_id`,
k.`id` AS `sku_id`,
COALESCE(s.`on_hand_quantity`, 0) AS `on_hand_quantity`,
COALESCE(s.`reserved_quantity`, 0) AS `reserved_quantity`,
s.`updated_by`,
s.`created_at`,
s.`updated_at`
FROM `gao_product_sku` k
JOIN `gao_product` p ON p.`id` = k.`product_id`
LEFT JOIN `gao_product_stock` s ON s.`sku_id` = k.`id` AND s.`store_id` = #{storeId}
WHERE k.`store_id` = #{storeId}
AND p.`deleted_at` IS NULL AND k.`deleted_at` IS NULL
AND k.`id` IN
<foreach collection="skuIdList" item="skuId" open="(" separator="," close=")">
#{skuId}
</foreach>
</select>
<select id="countByQuery" resultType="long"> <select id="countByQuery" resultType="long">
SELECT COUNT(1) SELECT COUNT(1)
FROM `gao_product_sku` k FROM `gao_product_sku` k