hwmom-htk
yinq 4 months ago
commit 6eb1b471b4

@ -6,6 +6,9 @@ import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.dromara.ai.domain.bo.AiChatMessageDetailBo;
import org.dromara.ai.domain.vo.AiChatMessageDetailVo;
import org.dromara.ai.service.IAiChatMessageDetailService;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
@ -37,6 +40,8 @@ public class AiChatMessageController extends BaseController {
private final IAiChatMessageService aiChatMessageService;
private final IAiChatMessageDetailService aiChatMessageDetailService;
/**
*
*/
@ -114,4 +119,16 @@ public class AiChatMessageController extends BaseController {
List<AiChatMessageVo> list = aiChatMessageService.queryList(bo);
return R.ok(list);
}
/**
* 使
*/
// @SaCheckPermission("ai:aiChatMessage:list")
@GetMapping("/listDetail")
public TableDataInfo<AiChatMessageDetailVo> listDetail(AiChatMessageDetailBo bo, PageQuery pageQuery) {
return aiChatMessageDetailService.queryPageJoinList(bo, pageQuery);
}
}

@ -0,0 +1,117 @@
package org.dromara.ai.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.ai.domain.vo.AiTokenUsageVo;
import org.dromara.ai.domain.bo.AiTokenUsageBo;
import org.dromara.ai.service.IAiTokenUsageService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
/**
* token使
* 访:/ai/tokenUsage
*
* @author xins
* @date 2025-09-30
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/tokenUsage")
public class AiTokenUsageController extends BaseController {
private final IAiTokenUsageService aiTokenUsageService;
/**
* token使
*/
@SaCheckPermission("ai:tokenUsage:list")
@GetMapping("/list")
public TableDataInfo<AiTokenUsageVo> list(AiTokenUsageBo bo, PageQuery pageQuery) {
return aiTokenUsageService.queryPageJoinList(bo, pageQuery);
}
/**
* token使
*/
@SaCheckPermission("ai:tokenUsage:export")
@Log(title = "用户token使用详情", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(AiTokenUsageBo bo, HttpServletResponse response) {
List<AiTokenUsageVo> list = aiTokenUsageService.queryList(bo);
ExcelUtil.exportExcel(list, "用户token使用详情", AiTokenUsageVo.class, response);
}
/**
* token使
*
* @param tokenUsageId
*/
@SaCheckPermission("ai:tokenUsage:query")
@GetMapping("/{tokenUsageId}")
public R<AiTokenUsageVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long tokenUsageId) {
return R.ok(aiTokenUsageService.queryById(tokenUsageId));
}
/**
* token使
*/
@SaCheckPermission("ai:tokenUsage:add")
@Log(title = "用户token使用详情", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody AiTokenUsageBo bo) {
return toAjax(aiTokenUsageService.insertByBo(bo));
}
/**
* token使
*/
@SaCheckPermission("ai:tokenUsage:edit")
@Log(title = "用户token使用详情", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody AiTokenUsageBo bo) {
return toAjax(aiTokenUsageService.updateByBo(bo));
}
/**
* token使
*
* @param tokenUsageIds
*/
@SaCheckPermission("ai:tokenUsage:remove")
@Log(title = "用户token使用详情", businessType = BusinessType.DELETE)
@DeleteMapping("/{tokenUsageIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] tokenUsageIds) {
return toAjax(aiTokenUsageService.deleteWithValidByIds(List.of(tokenUsageIds), true));
}
/**
* token使
*/
@GetMapping("/getAiTokenUsageList")
public R<List<AiTokenUsageVo>> getAiTokenUsageList(AiTokenUsageBo bo) {
List<AiTokenUsageVo> list = aiTokenUsageService.queryList(bo);
return R.ok(list);
}
}

@ -37,6 +37,12 @@ public class AiChatMessageDetail extends TenantEntity {
*/
private String sessionId;
/**
* 1AI23SQL4AI5
*/
private String messageDetailType;
/**
*
*/
@ -72,6 +78,11 @@ public class AiChatMessageDetail extends TenantEntity {
*/
private Long knowledgeBaseId;
/**
* ID,使ai_knowledge_content
*/
private Long knowledgeContentId;
/**
* 10
*/
@ -82,5 +93,4 @@ public class AiChatMessageDetail extends TenantEntity {
*/
private String completeFlag;
}

@ -0,0 +1,61 @@
package org.dromara.ai.domain;
import org.dromara.common.tenant.core.TenantEntity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
/**
* token使 ai_token_usage
*
* @author xins
* @date 2025-09-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("ai_token_usage")
public class AiTokenUsage extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
*
*/
@TableId(value = "token_usage_id", type = IdType.AUTO)
private Long tokenUsageId;
/**
*
*/
private Long userId;
/**
* token
*/
private Long token;
/**
* IDai_model
*/
private Long modelId;
/**
* token
*/
private Long promptToken;
/**
* token
*/
private Long completionToken;
/**
* 使token
*/
private Long totalToken;
}

@ -38,6 +38,12 @@ public class AiChatMessageDetailBo extends BaseEntity {
@NotNull(message = "会话ID不能为空", groups = { AddGroup.class, EditGroup.class })
private String sessionId;
/**
* 1AI23SQL4AI5
*/
@NotNull(message = "会话类型不能为空", groups = { AddGroup.class, EditGroup.class })
private String messageDetailType;
/**
*
*/
@ -80,6 +86,11 @@ public class AiChatMessageDetailBo extends BaseEntity {
@NotNull(message = "知识库ID关联ai_knowledge_base不能为空", groups = { AddGroup.class, EditGroup.class })
private Long knowledgeBaseId;
/**
* ID,使ai_knowledge_content
*/
private Long knowledgeContentId;
/**
* 10
*/

@ -0,0 +1,73 @@
package org.dromara.ai.domain.bo;
import org.dromara.ai.domain.AiTokenUsage;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
/**
* token使 ai_token_usage
*
* @author xins
* @date 2025-09-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = AiTokenUsage.class, reverseConvertGenerate = false)
public class AiTokenUsageBo extends BaseEntity {
/**
*
*/
@NotNull(message = "主键不能为空", groups = { AddGroup.class, EditGroup.class })
private Long tokenUsageId;
/**
*
*/
@NotNull(message = "用户不能为空", groups = { AddGroup.class, EditGroup.class })
private Long userId;
/**
* token
*/
@NotNull(message = "待结算token不能为空", groups = { AddGroup.class, EditGroup.class })
private Long token;
/**
* IDai_model
*/
@NotNull(message = "模型ID关联ai_model不能为空", groups = { AddGroup.class, EditGroup.class })
private Long modelId;
/**
* token
*/
@NotNull(message = "提问token数量不能为空", groups = { AddGroup.class, EditGroup.class })
private Long promptToken;
/**
* token
*/
@NotNull(message = "回复token数量不能为空", groups = { AddGroup.class, EditGroup.class })
private Long completionToken;
/**
* 使token
*/
@NotNull(message = "累计使用token不能为空", groups = { AddGroup.class, EditGroup.class })
private Long totalToken;
/**
*
*/
private String nickName;
}

@ -1,8 +1,11 @@
package org.dromara.ai.domain.vo;
import jakarta.validation.constraints.NotNull;
import org.dromara.ai.domain.AiChatMessageDetail;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import io.github.linpeilie.annotations.AutoMapper;
@ -40,6 +43,11 @@ public class AiChatMessageDetailVo implements Serializable {
@ExcelProperty(value = "聊天信息ID关联ai_chat_message")
private Long chatMessageId;
/**
* 1AI23SQL4AI5
*/
private String messageDetailType;
/**
* ID
*/
@ -88,6 +96,11 @@ public class AiChatMessageDetailVo implements Serializable {
@ExcelProperty(value = "知识库ID关联ai_knowledge_base")
private Long knowledgeBaseId;
/**
* ID,使ai_knowledge_content
*/
private Long knowledgeContentId;
/**
* 10
*/
@ -101,5 +114,14 @@ public class AiChatMessageDetailVo implements Serializable {
@ExcelProperty(value = "完整标识(1是0否),代表回复信息是否完整回复,中间可以暂停继续。")
private String completeFlag;
/**
*
*/
private String nickName;
/**
* AI
*/
private String modelName;
}

@ -0,0 +1,83 @@
package org.dromara.ai.domain.vo;
import org.dromara.ai.domain.AiTokenUsage;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* token使 ai_token_usage
*
* @author xins
* @date 2025-09-30
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = AiTokenUsage.class)
public class AiTokenUsageVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
*
*/
@ExcelProperty(value = "主键")
private Long tokenUsageId;
/**
*
*/
@ExcelProperty(value = "用户")
private Long userId;
/**
* token
*/
@ExcelProperty(value = "待结算token")
private Long token;
/**
* IDai_model
*/
@ExcelProperty(value = "模型ID关联ai_model")
private Long modelId;
/**
* token
*/
@ExcelProperty(value = "提问token数量")
private Long promptToken;
/**
* token
*/
@ExcelProperty(value = "回复token数量")
private Long completionToken;
/**
* 使token
*/
@ExcelProperty(value = "累计使用token")
private Long totalToken;
/**
*
*/
private String nickName;
/**
* AI
*/
private String modelName;
}

@ -1,7 +1,13 @@
package org.dromara.ai.mapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.dromara.ai.domain.AiChatMessageDetail;
import org.dromara.ai.domain.vo.AiChatMessageDetailVo;
import org.dromara.common.mybatis.annotation.DataColumn;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
@ -12,4 +18,11 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
*/
public interface AiChatMessageDetailMapper extends BaseMapperPlus<AiChatMessageDetail, AiChatMessageDetailVo> {
@DataPermission({
@DataColumn(key = "deptName", value = "acmd.create_dept"),
@DataColumn(key = "userName", value = "acmd.user_id")
})
Page<AiChatMessageDetailVo> selectAiChatMessageDetailJoinList(@Param("page") Page<AiChatMessageDetailVo> page,
@Param(Constants.WRAPPER) Wrapper<AiChatMessageDetail> queryWrapper);
}

@ -0,0 +1,37 @@
package org.dromara.ai.mapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.dromara.ai.domain.AiTokenUsage;
import org.dromara.ai.domain.bo.AiTokenUsageBo;
import org.dromara.ai.domain.vo.AiTokenUsageVo;
import org.dromara.common.mybatis.annotation.DataColumn;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import java.util.List;
/**
* token使Mapper
*
* @author xins
* @date 2025-09-30
*/
public interface AiTokenUsageMapper extends BaseMapperPlus<AiTokenUsage, AiTokenUsageVo> {
/**
* token使join sys_user and ai_model
* @param page
* @param queryWrapper
* @return List<AiTokenUsageVo>
*/
@DataPermission({
@DataColumn(key = "deptName", value = "atu.create_dept"),
@DataColumn(key = "userName", value = "atu.user_id")
})
Page<AiTokenUsageVo> selectAiTokenUsageJoinList(@Param("page") Page<AiTokenUsageVo> page,
@Param(Constants.WRAPPER) Wrapper<AiTokenUsage> queryWrapper);
}

@ -37,18 +37,18 @@ public class AIResponse {
/**
* 使token
*/
private Usage usage;
private TokenUsage tokenUsage;
public AIResponse(boolean success, String errorMessage, String content, Usage usage) {
public AIResponse(boolean success, String errorMessage, String content, TokenUsage tokenUsage) {
this.success = success;
this.errorMessage = errorMessage;
this.content = content;
this.usage = usage;
this.tokenUsage = tokenUsage;
}
// 成功响应的便捷构造函数
public AIResponse(String content, Usage usage) {
this(true, null, content, usage);
public AIResponse(String content, TokenUsage tokenUsage) {
this(true, null, content, tokenUsage);
}
// 错误响应的便捷构造函数
@ -59,16 +59,16 @@ public class AIResponse {
/**
* Token使
*/
@Data
public static class Usage {
private int promptTokens;
private int completionTokens;
private int totalTokens;
public Usage(int promptTokens, int completionTokens, int totalTokens) {
this.promptTokens = promptTokens;
this.completionTokens = completionTokens;
this.totalTokens = totalTokens;
}
}
// @Data
// public static class Usage {
// private int promptTokens;
// private int completionTokens;
// private int totalTokens;
//
// public Usage(int promptTokens, int completionTokens, int totalTokens) {
// this.promptTokens = promptTokens;
// this.completionTokens = completionTokens;
// this.totalTokens = totalTokens;
// }
// }
}

@ -0,0 +1,46 @@
package org.dromara.ai.process.dto;
/**
* @Author xins
* @Date 2025/9/25 16:28
* @Description: AI使Tokendto
*/
// 如果还没有这个类,需要创建
public class TokenUsage {
private Long promptToken;
private Long completionToken;
private Long totalToken;
// 构造方法、getter、setter
public TokenUsage() {}
public TokenUsage(Long promptToken, Long completionToken, Long totalToken) {
this.promptToken = promptToken;
this.completionToken = completionToken;
this.totalToken = totalToken;
}
public Long getPromptToken() {
return promptToken;
}
public void setPromptToken(Long promptToken) {
this.promptToken = promptToken;
}
public Long getCompletionToken() {
return completionToken;
}
public void setCompletionToken(Long completionTokens) {
this.completionToken = completionToken;
}
public Long getTotalToken() {
return totalToken;
}
public void setTotalToken(Long totalToken) {
this.totalToken = totalToken;
}
}

@ -1,8 +1,10 @@
package org.dromara.ai.process.provider.processor;
import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse;
import org.dromara.ai.process.dto.AIRequest;
import org.dromara.ai.process.dto.AIResponse;
import org.dromara.ai.process.dto.TokenUsage;
import org.dromara.ai.process.enums.AIProviderEnum;
import org.dromara.system.api.model.LoginUser;
import reactor.core.publisher.Flux;
@ -24,6 +26,13 @@ public interface IUnifiedAIProviderProcessor {
*/
Mono<AIResponse> chat(AIRequest request);
/**
*
* @param request
* @return Flux
*/
Flux<String> chatStreamContent(AIRequest request, LoginUser loginUser);
/**
*
* @param request
@ -42,19 +51,53 @@ public interface IUnifiedAIProviderProcessor {
/**
*
* @param text
*
* @param aiRequest
* @return Double
* @throws RuntimeException
*/
public List<Double> getEmbedding(AIRequest aiRequest);
/**
*
* @param texts
*
* @param embeddingResponse
* @return
*/
public List<Double> getEmbedding(GetEmbeddingResponse embeddingResponse);
/**
* Usage
* @param aiRequest
* @return Double
* @throws RuntimeException
*/
public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest);
/**
*
* @param aiRequest
* @return Double
* @throws RuntimeException
*/
public List<List<Double>> getEmbeddings(AIRequest aiRequest);
/**
* token使
* @param messageDetailType
* @param questionContent
* @param answerContent
* @param tokenUsage
* @param modelId
* @param knowledgeBaseId
* @param knowledgeContentId
* @param chatMessageId
* @param sessionId
* @param takeFlag
* @param completeFlag
* @param userId
*/
public void saveTokenUsage(String messageDetailType, String questionContent, String answerContent, TokenUsage tokenUsage,
Long modelId, Long knowledgeBaseId, Long knowledgeContentId,
Long chatMessageId,String sessionId,String takeFlag,String completeFlag,Long userId);
}

@ -1,11 +1,28 @@
package org.dromara.ai.process.provider.processor.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.yulichang.toolkit.JoinWrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.dromara.ai.domain.AiChatMessage;
import org.dromara.ai.domain.AiChatMessageDetail;
import org.dromara.ai.domain.AiTokenUsage;
import org.dromara.ai.mapper.AiChatMessageDetailMapper;
import org.dromara.ai.mapper.AiChatMessageMapper;
import org.dromara.ai.mapper.AiTokenUsageMapper;
import org.dromara.ai.process.dto.AIMessage;
import org.dromara.ai.process.dto.AIRequest;
import org.dromara.ai.process.dto.AIResponse;
import org.dromara.ai.process.dto.TokenUsage;
import org.dromara.ai.process.enums.AIChatMessageTypeEnum;
import org.dromara.ai.process.provider.processor.IUnifiedAIProviderProcessor;
import org.dromara.ai.test.ChatRequest;
import org.dromara.common.constant.HwMomAiConstants;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.api.model.LoginUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
@ -14,6 +31,7 @@ import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import java.time.Duration;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -26,6 +44,12 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
protected final ObjectMapper objectMapper;
protected final WebClient webClient;
@Autowired
private AiChatMessageMapper aiChatMessageMapper;
@Autowired
private AiChatMessageDetailMapper aiChatMessageDetailMapper;
@Autowired
private AiTokenUsageMapper aiTokenUsageMapper;
// 用于解析流式JSON块的模式
private static final Pattern JSON_PATTERN = Pattern.compile("\\{(?:[^{}]|\\{(?:[^{}]|\\{[^{}]*\\})*\\})*\\}");
@ -50,7 +74,7 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
/**
* JSON
*/
protected String parseStreamChunk(String jsonChunk) {
protected String parseStreamChunkContent(String jsonChunk) {
try {
return extractContentFromStreamJson(jsonChunk);
} catch (Exception e) {
@ -116,7 +140,7 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
/**
* HTTP
*/
protected Flux<String> executeStreamRequest(String url, String requestBody, String apiKey) {
protected Flux<String> executeStreamRequestContent(String url, String requestBody, String apiKey) {
// String prompt = "你好";
// requestBody = String.format(
// "{\"model\":\"deepseek-chat\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}]}",
@ -135,12 +159,216 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
.filter(chunk -> chunk != null && !chunk.isBlank())
.filter(chunk -> !chunk.equals("[DONE]"))
.filter(chunk -> chunk.startsWith("{") && chunk.endsWith("}"))
.map(this::parseStreamChunk)
.map(this::parseStreamChunkContent)
.filter(content -> content != null && !content.isEmpty())
.onErrorResume(e -> Flux.error(new RuntimeException("流式请求失败: " + e.getMessage())));
}
/**
* JSONtoken
*/
protected StreamChunkResult parseStreamChunk(String jsonChunk) {
try {
return extractContentAndTokensFromStreamJson(jsonChunk);
} catch (Exception e) {
return new StreamChunkResult(null, null);
}
}
/**
* JSONtoken
*/
protected abstract StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception;
/**
* HTTPtokenFlux
*/
protected Flux<StreamChunkResult> executeStreamRequest(String url, String requestBody, String apiKey) {
return webClient.post()
.uri(url)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM_VALUE)
.bodyValue(requestBody)
.retrieve()
.bodyToFlux(String.class)
.timeout(Duration.ofSeconds(60))
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
.flatMap(this::extractJsonChunks)
.filter(chunk -> chunk != null && !chunk.isBlank())
.filter(chunk -> !chunk.equals("[DONE]"))
.filter(chunk -> chunk.startsWith("{") && chunk.endsWith("}"))
.map(this::parseStreamChunk)
.filter(result -> result.hasContent() || result.hasTokenUsage())
.onErrorResume(e -> Flux.error(new RuntimeException("流式请求失败: " + e.getMessage())));
}
protected void saveChatMessage(AIRequest request, String fullResponse, TokenUsage tokenUsage, LoginUser loginUser) {
try {
String sessionId = request.getSessionId();
AiChatMessage aiChatMessage = aiChatMessageMapper
.selectOne(new LambdaQueryWrapper<AiChatMessage>()
.eq(AiChatMessage::getSessionId, sessionId));
List<AIMessage> messages = request.getMessages();
if (aiChatMessage == null) {
aiChatMessage = new AiChatMessage();
aiChatMessage.setSessionId(request.getSessionId());
aiChatMessage.setMessageTopic(objectMapper.writeValueAsString(request.getMessageTopic()));
aiChatMessage.setMessageType(AIChatMessageTypeEnum.AI_CHAT.getCode());
aiChatMessage.setModelId(request.getModelId());
aiChatMessage.setKnowledgeBaseId(request.getKnowledgeBaseId());
// aiChatMessage.setTotalToken();
aiChatMessage.setTenantId(loginUser.getTenantId());
aiChatMessage.setCreateBy(loginUser.getUserId());
aiChatMessage.setCreateDept(loginUser.getDeptId());
aiChatMessageMapper.insert(aiChatMessage);
} else {
}
saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION, objectMapper.writeValueAsString(request.getQuestionContent()),
objectMapper.writeValueAsString(fullResponse), tokenUsage, request.getModelId(), request.getKnowledgeBaseId(), null,
aiChatMessage.getChatMessageId(),request.getSessionId(),request.getCarryHistoryFlag(),"1", loginUser.getUserId());
// AiChatMessageDetail aiChatMessageDetail = new AiChatMessageDetail();
// aiChatMessageDetail.setChatMessageId(aiChatMessage.getChatMessageId());
// aiChatMessageDetail.setSessionId(request.getSessionId());
// aiChatMessageDetail.setQuestionContent(objectMapper.writeValueAsString(request.getQuestionContent()));
// aiChatMessageDetail.setAnswerContent(objectMapper.writeValueAsString(fullResponse));
// // 设置token使用信息
// if (tokenUsage != null) {
// aiChatMessageDetail.setPromptToken((long) tokenUsage.getPromptTokens());
// aiChatMessageDetail.setCompletionToken((long) tokenUsage.getCompletionTokens());
// aiChatMessageDetail.setTotalToken((long) tokenUsage.getTotalTokens());
// }
// aiChatMessageDetail.setModelId(request.getModelId());
// aiChatMessageDetail.setKnowledgeBaseId(request.getKnowledgeBaseId());
// aiChatMessageDetail.setTakeFlag(request.getCarryHistoryFlag());
// aiChatMessageDetail.setCompleteFlag("1");
// aiChatMessageDetail.setTenantId(loginUser.getTenantId());
// aiChatMessageDetail.setCreateBy(loginUser.getUserId());
// aiChatMessageDetail.setCreateDept(loginUser.getDeptId());
//
// aiChatMessageDetailMapper.insert(aiChatMessageDetail);
} catch (Exception e) {
throw new RuntimeException("保存聊天记录失败", e);
}
// log.info("聊天记录已保存ID: {}", record.getId());
}
/**
* token使
* @param messageDetailType
* @param questionContent
* @param answerContent
* @param tokenUsage
* @param modelId
* @param knowledgeBaseId
* @param knowledgeContentId
* @param chatMessageId
* @param sessionId
* @param takeFlag
* @param completeFlag
* @param userId
*/
@Override
public void saveTokenUsage(String messageDetailType, String questionContent, String answerContent, TokenUsage tokenUsage,
Long modelId, Long knowledgeBaseId, Long knowledgeContentId,
Long chatMessageId,String sessionId,String takeFlag,String completeFlag,Long userId) {
Long promptToken = tokenUsage!=null ? tokenUsage.getPromptToken():null;
Long completionToken = tokenUsage!=null ? tokenUsage.getCompletionToken():null;
Long totalToken = tokenUsage!=null ? tokenUsage.getTotalToken():null;
AiChatMessageDetail aiChatMessageDetail = new AiChatMessageDetail();
aiChatMessageDetail.setMessageDetailType(messageDetailType);
aiChatMessageDetail.setQuestionContent(questionContent);
aiChatMessageDetail.setAnswerContent(answerContent);
aiChatMessageDetail.setPromptToken(promptToken);
aiChatMessageDetail.setCompletionToken(completionToken);
aiChatMessageDetail.setTotalToken(totalToken);
aiChatMessageDetail.setModelId(modelId);
aiChatMessageDetail.setKnowledgeBaseId(knowledgeBaseId);
aiChatMessageDetail.setKnowledgeContentId(knowledgeContentId);
aiChatMessageDetail.setTakeFlag(takeFlag);
aiChatMessageDetail.setCompleteFlag(completeFlag);
aiChatMessageDetail.setChatMessageId(chatMessageId);
aiChatMessageDetail.setSessionId(sessionId);
aiChatMessageDetailMapper.insert(aiChatMessageDetail);
MPJLambdaWrapper<AiTokenUsage> lqw = JoinWrappers.lambda(AiTokenUsage.class)
.selectAll(AiTokenUsage.class)
.eq(userId != null, AiTokenUsage::getUserId, userId)
.eq(modelId != null, AiTokenUsage::getModelId, modelId);
AiTokenUsage aiTokenUsage = aiTokenUsageMapper.selectOne(lqw);
if (aiTokenUsage == null) {
aiTokenUsage = new AiTokenUsage();
aiTokenUsage.setPromptToken(promptToken);
aiTokenUsage.setCompletionToken(completionToken);
aiTokenUsage.setTotalToken(totalToken);
aiTokenUsage.setModelId(modelId);
aiTokenUsage.setUserId(userId);
aiTokenUsageMapper.insert(aiTokenUsage);
} else {
if (promptToken != null) {
Long currentPromptToken = aiTokenUsage.getPromptToken() == null ? 0L : aiTokenUsage.getPromptToken();
aiTokenUsage.setPromptToken(currentPromptToken + promptToken);
}
if (completionToken != null) {
Long currentCompletionToken = aiTokenUsage.getCompletionToken() == null ? 0L : aiTokenUsage.getCompletionToken();
aiTokenUsage.setCompletionToken(currentCompletionToken + completionToken);
}
aiTokenUsage.setTotalToken(aiTokenUsage.getTotalToken() + totalToken);
aiTokenUsageMapper.updateById(aiTokenUsage);
}
}
/**
* chunk
*/
protected static class StreamChunkResult {
private final String content;
private final TokenUsage tokenUsage;
public StreamChunkResult(String content, TokenUsage tokenUsage) {
this.content = content;
this.tokenUsage = tokenUsage;
}
// getters
public String getContent() { return content; }
public TokenUsage getTokenUsage() { return tokenUsage; }
public boolean hasContent() {
return content != null && !content.isEmpty();
}
public boolean hasTokenUsage() {
return tokenUsage != null;
}
}
// /**
// * 流式回复
// * @param request

@ -8,6 +8,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse;
import org.dromara.ai.domain.AiChatMessage;
import org.dromara.ai.domain.AiChatMessageDetail;
import org.dromara.ai.domain.vo.AiModelVo;
@ -17,6 +18,7 @@ import org.dromara.ai.mapper.AiModelMapper;
import org.dromara.ai.process.dto.AIMessage;
import org.dromara.ai.process.dto.AIRequest;
import org.dromara.ai.process.dto.AIResponse;
import org.dromara.ai.process.dto.TokenUsage;
import org.dromara.ai.process.enums.AIChatMessageTypeEnum;
import org.dromara.ai.process.enums.AIProviderEnum;
import org.dromara.ai.service.IAiChatMessageService;
@ -54,11 +56,6 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
@Autowired
private AiModelMapper aiModelMapper;
@Autowired
private AiChatMessageMapper aiChatMessageMapper;
@Autowired
private AiChatMessageDetailMapper aiChatMessageDetailMapper;
public Mono<AIResponse> chatTest(AIRequest request) {
AIMessage aiMessage = new AIMessage();
@ -101,7 +98,7 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
}
@Override
public Flux<String> chatStream(AIRequest request, LoginUser loginUser) {
public Flux<String> chatStreamContent(AIRequest request, LoginUser loginUser) {
try {
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("model", deepSeekChatModel);
@ -119,13 +116,13 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
configureApiKey(request);
return executeStreamRequest(API_URL, requestBody, request.getApiKey()).doOnNext(chunk -> {
return executeStreamRequestContent(API_URL, requestBody, request.getApiKey()).doOnNext(chunk -> {
// 收集每个chunk
fullResponseBuilder.append(chunk);
})
.doOnComplete(() -> {
// 流完成后保存到数据库
saveChatMessage(request, fullResponseBuilder.toString(), loginUser);
saveChatMessage(request, fullResponseBuilder.toString(), null,loginUser);
})
.doOnError(error -> {
// 错误处理
@ -171,6 +168,106 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
}
}
@Override
public Flux<String> chatStream(AIRequest request, LoginUser loginUser) {
try {
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("model", deepSeekChatModel);
rootNode.set("messages", objectMapper.valueToTree(request.getMessages()));
rootNode.put("stream", true);
if (request.getTemperature() != null) {
rootNode.put("temperature", request.getTemperature());
}
String requestBody = objectMapper.writeValueAsString(rootNode);
configureApiKey(request);
// 用于收集完整响应和token信息
StringBuilder fullResponseBuilder = new StringBuilder();
TokenUsage finalTokenUsage = new TokenUsage(0L, 0L, 0L);
return executeStreamRequest(API_URL, requestBody, request.getApiKey())
.doOnNext(chunkResult -> {
// 收集内容
if (chunkResult.hasContent()) {
fullResponseBuilder.append(chunkResult.getContent());
}
// 更新token使用信息最后一个包含usage的chunk会覆盖之前的
if (chunkResult.hasTokenUsage()) {
TokenUsage usage = chunkResult.getTokenUsage();
finalTokenUsage.setPromptToken(usage.getPromptToken());
finalTokenUsage.setCompletionToken(usage.getCompletionToken());
finalTokenUsage.setTotalToken(usage.getTotalToken());
}
})
.map(chunkResult -> chunkResult.hasContent() ? chunkResult.getContent() : "")
.filter(content -> !content.isEmpty())
.doOnComplete(() -> {
// 流完成后保存到数据库包含token信息
saveChatMessage(request, fullResponseBuilder.toString(), finalTokenUsage, loginUser);
})
.doOnError(error -> {
// 即使出错也尝试保存已收集的内容
saveChatMessage(request, fullResponseBuilder.toString(), finalTokenUsage, loginUser);
});
} catch (IOException e) {
return Flux.error(new RuntimeException("构建请求失败: " + e.getMessage()));
}
}
@Override
protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception {
try {
JsonNode node = new ObjectMapper()
.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true)
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
.readTree(jsonChunk);
// 提取内容
String content = extractContent(node);
// 提取token使用信息
TokenUsage tokenUsage = extractTokenUsage(node);
return new StreamChunkResult(content, tokenUsage);
} catch (Exception e) {
// 尝试提取content的原始文本最后手段
String content = extractContentRaw(jsonChunk);
return new StreamChunkResult(content, null);
}
}
private String extractContent(JsonNode node) {
String content = node.path("delta").path("content").asText();
if (content.isEmpty()) {
JsonNode choices = node.path("choices");
if (choices.isArray() && choices.size() > 0) {
content = choices.get(0).path("delta").path("content").asText();
}
}
return content;
}
private TokenUsage extractTokenUsage(JsonNode node) {
JsonNode usage = node.path("usage");
if (!usage.isMissingNode() && !usage.isEmpty()) {
long promptTokens = usage.path("prompt_tokens").asLong();
long completionTokens = usage.path("completion_tokens").asLong();
long totalTokens = usage.path("total_tokens").asLong();
// 只有在有实际值时才返回TokenUsage
if (promptTokens > 0 || completionTokens > 0 || totalTokens > 0) {
return new TokenUsage(promptTokens, completionTokens, totalTokens);
}
}
return null;
}
@Override
protected AIResponse extractAIResponse(String json) throws Exception {
JsonNode node = objectMapper.readTree(json);
@ -181,13 +278,17 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
}
JsonNode usageNode = node.path("usage");
AIResponse.Usage usage = new AIResponse.Usage(
usageNode.path("prompt_tokens").asInt(),
usageNode.path("completion_tokens").asInt(),
usageNode.path("total_tokens").asInt()
);
// AIResponse.Usage usage = new AIResponse.Usage(
// usageNode.path("prompt_tokens").asInt(),
// usageNode.path("completion_tokens").asInt(),
// usageNode.path("total_tokens").asInt()
// );
return new AIResponse(content, usage);
TokenUsage tokenUsage = new TokenUsage( usageNode.path("prompt_tokens").asLong(),
usageNode.path("completion_tokens").asLong(),
usageNode.path("total_tokens").asLong());
return new AIResponse(content, tokenUsage);
}
@Override
@ -205,6 +306,16 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
return List.of();
}
@Override
public List<Double> getEmbedding(GetEmbeddingResponse embeddingResponse){
return List.of();
}
@Override
public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest) {
return null;
}
@Override
public List<List<Double>> getEmbeddings(AIRequest aiRequest) {
return List.of();
@ -235,53 +346,7 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
}
private void saveChatMessage(AIRequest request, String fullResponse, LoginUser loginUser) {
try {
String sessionId = request.getSessionId();
AiChatMessage aiChatMessage = aiChatMessageMapper
.selectOne(new LambdaQueryWrapper<AiChatMessage>()
.eq(AiChatMessage::getSessionId, sessionId));
List<AIMessage> messages = request.getMessages();
if (aiChatMessage == null) {
aiChatMessage = new AiChatMessage();
aiChatMessage.setSessionId(request.getSessionId());
aiChatMessage.setMessageTopic(objectMapper.writeValueAsString(request.getMessageTopic()));
aiChatMessage.setMessageType(AIChatMessageTypeEnum.AI_CHAT.getCode());
aiChatMessage.setModelId(request.getModelId());
aiChatMessage.setKnowledgeBaseId(request.getKnowledgeBaseId());
// aiChatMessage.setTotalToken();
aiChatMessage.setTenantId(loginUser.getTenantId());
aiChatMessage.setCreateBy(loginUser.getUserId());
aiChatMessage.setCreateDept(loginUser.getDeptId());
aiChatMessageMapper.insert(aiChatMessage);
} else {
}
AiChatMessageDetail aiChatMessageDetail = new AiChatMessageDetail();
aiChatMessageDetail.setChatMessageId(aiChatMessage.getChatMessageId());
aiChatMessageDetail.setSessionId(request.getSessionId());
aiChatMessageDetail.setQuestionContent(objectMapper.writeValueAsString(request.getQuestionContent()));
aiChatMessageDetail.setAnswerContent(objectMapper.writeValueAsString(fullResponse));
// aiChatMessageDetail.setPromptToken(1L);
// aiChatMessageDetail.setCompletionToken(1L);
// aiChatMessageDetail.setTotalToken(1L);
aiChatMessageDetail.setModelId(request.getModelId());
aiChatMessageDetail.setKnowledgeBaseId(request.getKnowledgeBaseId());
aiChatMessageDetail.setTakeFlag(request.getCarryHistoryFlag());
aiChatMessageDetail.setCompleteFlag("1");
aiChatMessageDetail.setTenantId(loginUser.getTenantId());
aiChatMessageDetail.setCreateBy(loginUser.getUserId());
aiChatMessageDetail.setCreateDept(loginUser.getDeptId());
aiChatMessageDetailMapper.insert(aiChatMessageDetail);
} catch (Exception e) {
throw new RuntimeException("保存聊天记录失败", e);
}
// log.info("聊天记录已保存ID: {}", record.getId());
}
}

@ -8,7 +8,9 @@ import com.tencentcloudapi.lkeap.v20240522.LkeapClient;
import com.tencentcloudapi.lkeap.v20240522.models.EmbeddingObject;
import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingRequest;
import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse;
import com.tencentcloudapi.lkeap.v20240522.models.Usage;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.process.provider.processor.utils.ProcessorUtils;
import org.dromara.ai.test.vectorization.config.EmbeddingConfig;
import org.dromara.ai.process.dto.AIRequest;
import org.dromara.ai.process.dto.AIResponse;
@ -31,7 +33,7 @@ import java.util.List;
*/
@Slf4j
@Component
public class TencentLkeProcessor implements IUnifiedAIProviderProcessor {
public class TencentLkeProcessor extends BaseAIProviderProcessor {
// 客户端配置
private ClientProfile clientProfile;
// 配置属性
@ -86,6 +88,53 @@ public class TencentLkeProcessor implements IUnifiedAIProviderProcessor {
return embeddings.isEmpty() ? new ArrayList<>() : embeddings.get(0);
}
@Override
public List<Double> getEmbedding(GetEmbeddingResponse embeddingResponse) {
EmbeddingObject[] embeddingObjects = embeddingResponse.getData();
// 转换结果格式
List<List<Double>> result = new ArrayList<>();
for (EmbeddingObject obj : embeddingObjects) {
result.add(ProcessorUtils.convertFloatArrayToDoubleList(obj.getEmbedding()));
}
return result.isEmpty() ? new ArrayList<>() : result.get(0);
}
/**
* Usage
* @param aiRequest
* @return Double
* @throws RuntimeException
*/
@Override
public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest) {
try {
String apiKey = aiRequest.getApiKey();
String apiSecret = aiRequest.getApiSecret();
// 创建凭证对象
Credential cred = new Credential(apiKey, apiSecret);
// 创建客户端实例
LkeapClient client = new LkeapClient(
cred,
properties.getTencentLke().getRegion(),
clientProfile
);
// 创建请求对象
GetEmbeddingRequest req = new GetEmbeddingRequest();
req.setModel(properties.getTencentLke().getModel());
req.setInputs(aiRequest.getTexts());
// 发送请求并获取响应
GetEmbeddingResponse resp = client.GetEmbedding(req);
return resp;
} catch (TencentCloudSDKException e) {
log.error("Failed to get embeddings from Tencent Cloud", e);
throw new RuntimeException("Failed to get embeddings", e);
}
}
@Override
public List<List<Double>> getEmbeddings(AIRequest aiRequest) {
try {
@ -109,11 +158,13 @@ public class TencentLkeProcessor implements IUnifiedAIProviderProcessor {
// 发送请求并获取响应
GetEmbeddingResponse resp = client.GetEmbedding(req);
EmbeddingObject[] embeddingObjects = resp.getData();
//获取token使用数量然后保存
Usage usage = resp.getUsage();
// 转换结果格式
List<List<Double>> result = new ArrayList<>();
for (EmbeddingObject obj : embeddingObjects) {
result.add(convertFloatArrayToDoubleList(obj.getEmbedding()));
result.add(ProcessorUtils.convertFloatArrayToDoubleList(obj.getEmbedding()));
}
return result;
@ -129,19 +180,6 @@ public class TencentLkeProcessor implements IUnifiedAIProviderProcessor {
return AIProviderEnum.TENCENT_LKE;
}
/**
* FloatDouble
* @param floatArray Float
* @return Double
*/
private List<Double> convertFloatArrayToDoubleList(Float[] floatArray) {
List<Double> doubleList = new ArrayList<>(floatArray.length);
for (Float f : floatArray) {
doubleList.add(f.doubleValue());
}
return doubleList;
}
@Override
public Mono<AIResponse> chatTest(AIRequest request) {
@ -153,9 +191,28 @@ public class TencentLkeProcessor implements IUnifiedAIProviderProcessor {
return null;
}
@Override
public Flux<String> chatStreamContent(AIRequest request, LoginUser loginUser) {
return null;
}
@Override
public Flux<String> chatStream(AIRequest request, LoginUser loginUser) {
return null;
}
@Override
protected String extractContentFromStreamJson(String jsonChunk) throws Exception {
return "";
}
@Override
protected AIResponse extractAIResponse(String json) throws Exception {
return null;
}
@Override
protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception {
return null;
}
}

@ -7,6 +7,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse;
import org.dromara.ai.domain.AiChatMessage;
import org.dromara.ai.domain.AiChatMessageDetail;
import org.dromara.ai.mapper.AiChatMessageDetailMapper;
@ -14,6 +15,7 @@ import org.dromara.ai.mapper.AiChatMessageMapper;
import org.dromara.ai.process.dto.AIMessage;
import org.dromara.ai.process.dto.AIRequest;
import org.dromara.ai.process.dto.AIResponse;
import org.dromara.ai.process.dto.TokenUsage;
import org.dromara.ai.process.enums.AIProviderEnum;
import org.dromara.common.encrypt.utils.EncryptUtils;
import org.dromara.system.api.model.LoginUser;
@ -90,8 +92,9 @@ public class TongYiQianWenProcessor extends BaseAIProviderProcessor {
}
}
@Override
public Flux<String> chatStream(AIRequest request, LoginUser loginUser) {
public Flux<String> chatStreamContent(AIRequest request, LoginUser loginUser) {
try {
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("model", deepSeekChatModel);
@ -169,15 +172,17 @@ public class TongYiQianWenProcessor extends BaseAIProviderProcessor {
}
JsonNode usageNode = node.path("usage");
AIResponse.Usage usage = new AIResponse.Usage(
usageNode.path("prompt_tokens").asInt(),
usageNode.path("completion_tokens").asInt(),
usageNode.path("total_tokens").asInt()
TokenUsage tokenUsage = new TokenUsage(
usageNode.path("prompt_tokens").asLong(),
usageNode.path("completion_tokens").asLong(),
usageNode.path("total_tokens").asLong()
);
return new AIResponse(content, usage);
return new AIResponse(content, tokenUsage);
}
@Override
public AIProviderEnum supportedProvider() {
return AIProviderEnum.TONGYI_QIANWEN;
@ -193,6 +198,16 @@ public class TongYiQianWenProcessor extends BaseAIProviderProcessor {
return List.of();
}
@Override
public List<Double> getEmbedding(GetEmbeddingResponse embeddingResponse){
return List.of();
}
@Override
public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest) {
return null;
}
@Override
public List<List<Double>> getEmbeddings(AIRequest aiRequest) {
return List.of();
@ -247,4 +262,13 @@ public class TongYiQianWenProcessor extends BaseAIProviderProcessor {
}
@Override
public Flux<String> chatStream(AIRequest request, LoginUser loginUser) {
return null;
}
@Override
protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception {
return null;
}
}

@ -0,0 +1,25 @@
package org.dromara.ai.process.provider.processor.utils;
import java.util.ArrayList;
import java.util.List;
/**
* @Author xins
* @Date 2025/9/26 17:12
* @Description:
*/
public class ProcessorUtils {
/**
* FloatDouble
* @param floatArray Float
* @return Double
*/
public static List<Double> convertFloatArrayToDoubleList(Float[] floatArray) {
List<Double> doubleList = new ArrayList<>(floatArray.length);
for (Float f : floatArray) {
doubleList.add(f.doubleValue());
}
return doubleList;
}
}

@ -74,4 +74,12 @@ public interface IAiChatMessageDetailService {
* @return LIST<AIMessage></AIMessage>
*/
public List<AIMessage> getAIChatMessages(String sessionId);
/**
* join user and model
* @param bo
* @param pageQuery
* @return
*/
public TableDataInfo<AiChatMessageDetailVo> queryPageJoinList(AiChatMessageDetailBo bo, PageQuery pageQuery);
}

@ -0,0 +1,78 @@
package org.dromara.ai.service;
import org.dromara.ai.domain.AiTokenUsage;
import org.dromara.ai.domain.vo.AiTokenUsageVo;
import org.dromara.ai.domain.bo.AiTokenUsageBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.Collection;
import java.util.List;
/**
* token使Service
*
* @author xins
* @date 2025-09-30
*/
public interface IAiTokenUsageService {
/**
* token使
*
* @param tokenUsageId
* @return token使
*/
AiTokenUsageVo queryById(Long tokenUsageId);
/**
* token使
*
* @param bo
* @param pageQuery
* @return token使
*/
TableDataInfo<AiTokenUsageVo> queryPageList(AiTokenUsageBo bo, PageQuery pageQuery);
/**
* token使
*
* @param bo
* @return token使
*/
List<AiTokenUsageVo> queryList(AiTokenUsageBo bo);
/**
* token使
*
* @param bo token使
* @return
*/
Boolean insertByBo(AiTokenUsageBo bo);
/**
* token使
*
* @param bo token使
* @return
*/
Boolean updateByBo(AiTokenUsageBo bo);
/**
* token使
*
* @param ids
* @param isValid
* @return
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* token使,Join sys_user and ai_model
*
* @param bo
* @param pageQuery
* @return token使
*/
public TableDataInfo<AiTokenUsageVo> queryPageJoinList(AiTokenUsageBo bo, PageQuery pageQuery);
}

@ -5,17 +5,17 @@ import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse;
import com.tencentcloudapi.lkeap.v20240522.models.Usage;
import org.dromara.ai.domain.AiFormSettingDetail;
import org.dromara.ai.domain.AiModel;
import org.dromara.ai.domain.dto.AiTableConditionWrapper;
import org.dromara.ai.domain.dto.AiTableData;
import org.dromara.ai.domain.dto.AiTableQueryCondition;
import org.dromara.ai.mapper.AiModelMapper;
import org.dromara.ai.process.dto.AIFillFormRequest;
import org.dromara.ai.process.dto.AIResponse;
import org.dromara.ai.mapper.AiTokenUsageMapper;
import org.dromara.ai.process.dto.*;
import org.dromara.ai.mapper.SQLServerDatabaseMetaMapper;
import org.dromara.ai.process.dto.AIMessage;
import org.dromara.ai.process.dto.AIRequest;
import org.dromara.ai.process.provider.processor.AIProviderProcessorFactory;
import org.dromara.ai.process.provider.processor.IUnifiedAIProviderProcessor;
import org.dromara.ai.service.IAIAssistantService;
@ -37,7 +37,7 @@ import java.util.stream.Collectors;
/**
* @Author xins
* @Date 2025/7/17 14:31
* @Description:
* @Description:AI
*/
@Service
public class AIAssistantServiceImpl implements IAIAssistantService {
@ -128,6 +128,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
AIMessage message = messages.get(messages.size() - 1);
String messageContent = message.getContent().toString();
aiRequest.setText(messageContent);
aiRequest.setTexts(new String[]{messageContent});
StringBuilder sb = new StringBuilder(messageContent);
Long embeddingModelId = aiRequest.getEmbeddingModelId();
@ -141,7 +142,18 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
aiRequest.setApiKey(EncryptUtils.decryptByBase64(aiModel.getApiKey()));
aiRequest.setApiSecret(EncryptUtils.decryptByBase64(aiModel.getApiSecret()));
IUnifiedAIProviderProcessor tencentLkeProcessor = aiProviderProcessorFactory.getProcessorByPlatformId(aiModel.getPlatformId());
List<Double> queryEmbedding = tencentLkeProcessor.getEmbedding(aiRequest);
GetEmbeddingResponse embeddingResponses = tencentLkeProcessor.getEmbeddingResponses(aiRequest);
//获取token使用数量然后保存
Usage usage = embeddingResponses.getUsage();
TokenUsage tokenUsage = new TokenUsage(null,null,usage.getTotalTokens());
tencentLkeProcessor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION_VECTOR,"AI问答获取向量", null, tokenUsage,
embeddingModelId, aiRequest.getKnowledgeBaseId(), null,
null, null, "0", "1",LoginHelper.getUserId());
List<Double> queryEmbedding = tencentLkeProcessor.getEmbedding(embeddingResponses);
int topK = aiRequest.getRetrieveLimit() == null || aiRequest.getRetrieveLimit() <= 0 ?
5 : aiRequest.getRetrieveLimit();//retrieveLimit检索限制
@ -270,6 +282,9 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
// WHERE del_flag = '0'
// ORDER BY order_num, dept_id;
//```
processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_SQL,prompt, content,response.block().getTokenUsage(),
aiRequest.getModelId(), null,null,
null,null,"0","1",LoginHelper.getUserId());
return extractSqlFromContent(content);
} else {
@ -278,6 +293,9 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
}
/**
*
*/
@ -413,6 +431,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
AIRequest aiRequest = new AIRequest();
aiRequest.setMessages(Collections.singletonList(aiMessage));
//todo modelId
aiRequest.setModelId(1L);
Mono<AIResponse> response = processor.chat(aiRequest);
@ -420,6 +439,10 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
String content = response.block().getContent().toString();
JSONObject contentJson = JSONObject.parseObject(content);
parseRelateTable(aiFormSettingDetailList, contentJson);
processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_FORM,sb.toString(), content, response.block().getTokenUsage(),
1L, null, null,
null, null, "0", "1",LoginHelper.getUserId());
System.out.println(contentJson.toJSONString());
return contentJson;
@ -462,7 +485,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
//[{fieldKey:'del_flag',fieldValue:'0',conditionSymbol:'='}]
// 创建条件包装类
AiTableConditionWrapper wrapper = new AiTableConditionWrapper();
if(StringUtils.isNotBlank(relateFilterCondition)){
if (StringUtils.isNotBlank(relateFilterCondition)) {
// 解析JSON字符串为条件列表
List<AiTableQueryCondition> conditions = JSON.parseArray(relateFilterCondition, AiTableQueryCondition.class);
wrapper.setAiTableQueryConditions(conditions);
@ -470,7 +493,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
}
List<Map<String, Object>> tableData = sQLServerDatabaseMetaMapper.dynamicSelect(relateTableName, relateTableFieldList, conditionMap, wrapper,"", "");
List<Map<String, Object>> tableData = sQLServerDatabaseMetaMapper.dynamicSelect(relateTableName, relateTableFieldList, conditionMap, wrapper, "", "");
JSONArray relateTableDataArr = new JSONArray();
for (Map<String, Object> tableD : tableData) {
// JSONObject tableDataJson = new JSONObject();

@ -1,5 +1,10 @@
package org.dromara.ai.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.dromara.ai.domain.AiTokenUsage;
import org.dromara.ai.domain.bo.AiTokenUsageBo;
import org.dromara.ai.domain.vo.AiTokenUsageVo;
import org.dromara.ai.process.dto.AIMessage;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
@ -175,4 +180,22 @@ public class AiChatMessageDetailServiceImpl implements IAiChatMessageDetailServi
return aiMessages;
}
/**
* join user and model
* @param bo
* @param pageQuery
* @return
*/
@Override
public TableDataInfo<AiChatMessageDetailVo> queryPageJoinList(AiChatMessageDetailBo bo, PageQuery pageQuery) {
QueryWrapper<AiChatMessageDetail> wrapper = Wrappers.query();
wrapper
.eq(ObjectUtil.isNotNull(bo.getMessageDetailType()), "acmd.message_detail_type", bo.getMessageDetailType())
.eq(ObjectUtil.isNotNull(bo.getCreateBy()), "acmd.create_by", bo.getCreateBy())
.eq(ObjectUtil.isNotNull(bo.getModelId()), "acmd.model_id", bo.getModelId())
.orderByAsc("acmd.message_detail_id");
Page<AiChatMessageDetailVo> page = baseMapper.selectAiChatMessageDetailJoinList(pageQuery.build(), wrapper);
return TableDataInfo.build(page);
}
}

@ -6,6 +6,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.github.yulichang.interfaces.MPJBaseJoin;
import com.google.protobuf.ServiceException;
import com.tencentcloudapi.lkeap.v20240522.models.EmbeddingObject;
import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse;
import com.tencentcloudapi.lkeap.v20240522.models.Usage;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.ListUtils;
import org.dromara.ai.domain.*;
@ -20,8 +23,11 @@ import org.dromara.ai.mapper.AiKnowledgeContentMapper;
import org.dromara.ai.domain.dto.AIKnowledgeEmbedding;
import org.dromara.ai.mapper.AiModelMapper;
import org.dromara.ai.process.dto.AIRequest;
import org.dromara.ai.process.dto.TokenUsage;
import org.dromara.ai.process.provider.processor.impl.TencentLkeProcessor;
import org.dromara.ai.process.provider.processor.utils.ProcessorUtils;
import org.dromara.ai.vectordb.service.IVectorDBService;
import org.dromara.common.constant.HwMomAiConstants;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.encrypt.utils.EncryptUtils;
@ -31,6 +37,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.yulichang.toolkit.JoinWrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import lombok.RequiredArgsConstructor;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.stereotype.Service;
import org.dromara.ai.domain.bo.AiKnowledgeBaseBo;
import org.dromara.ai.domain.vo.AiKnowledgeBaseVo;
@ -344,7 +351,24 @@ public class AiKnowledgeBaseServiceImpl implements IAiKnowledgeBaseService {
try {
for (int i = 0; i < partitionChunkList.size(); i++) {
aiRequest.setTexts(partitionChunkList.get(i).toArray(new String[0]));//超过5个后调用腾讯云API返回错误too manyembeddings
List<List<Double>> vectorList = tencentLkeProcessor.getEmbeddings(aiRequest);
GetEmbeddingResponse embeddingResponses = tencentLkeProcessor.getEmbeddingResponses(aiRequest);
EmbeddingObject[] embeddingObjects = embeddingResponses.getData();
//获取token使用数量然后保存
Usage usage = embeddingResponses.getUsage();
TokenUsage tokenUsage = new TokenUsage(null,null,usage.getTotalTokens());
tencentLkeProcessor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_CONTENT_VECTOR,"AI上传知识库内容获取向量", null, tokenUsage,
modelId, aiRequest.getKnowledgeBaseId(), aiKnowledgeEmbedding.getKnowledgeContentId(),
null, null, "0", "1", LoginHelper.getUserId());
// 转换结果格式
List<List<Double>> vectorList = new ArrayList<>();
for (EmbeddingObject obj : embeddingObjects) {
vectorList.add(ProcessorUtils.convertFloatArrayToDoubleList(obj.getEmbedding()));
}
vectorDBService.insertKnowledgeEmbedding(aiKnowledgeEmbedding.getKnowledgeBaseId(),
aiKnowledgeEmbedding.getKnowledgeContentId(),
partitionChunkList.get(i), vectorList, partitionFidList.get(i));

@ -0,0 +1,162 @@
package org.dromara.ai.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.yulichang.toolkit.JoinWrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.dromara.ai.domain.bo.AiTokenUsageBo;
import org.dromara.ai.domain.vo.AiTokenUsageVo;
import org.dromara.ai.domain.AiTokenUsage;
import org.dromara.ai.mapper.AiTokenUsageMapper;
import org.dromara.ai.service.IAiTokenUsageService;
import java.util.List;
import java.util.Map;
import java.util.Collection;
/**
* token使Service
*
* @author xins
* @date 2025-09-30
*/
@RequiredArgsConstructor
@Service
public class AiTokenUsageServiceImpl implements IAiTokenUsageService {
private final AiTokenUsageMapper baseMapper;
/**
* token使
*
* @param tokenUsageId
* @return token使
*/
@Override
public AiTokenUsageVo queryById(Long tokenUsageId){
return baseMapper.selectVoById(tokenUsageId);
}
/**
* token使
*
* @param bo
* @param pageQuery
* @return token使
*/
@Override
public TableDataInfo<AiTokenUsageVo> queryPageList(AiTokenUsageBo bo, PageQuery pageQuery) {
MPJLambdaWrapper<AiTokenUsage> lqw = buildQueryWrapper(bo);
Page<AiTokenUsageVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* token使
*
* @param bo
* @return token使
*/
@Override
public List<AiTokenUsageVo> queryList(AiTokenUsageBo bo) {
MPJLambdaWrapper<AiTokenUsage> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private MPJLambdaWrapper<AiTokenUsage> buildQueryWrapper(AiTokenUsageBo bo) {
Map<String, Object> params = bo.getParams();
MPJLambdaWrapper<AiTokenUsage> lqw = JoinWrappers.lambda(AiTokenUsage.class)
.selectAll(AiTokenUsage.class)
.eq(bo.getTokenUsageId() != null, AiTokenUsage::getTokenUsageId, bo.getTokenUsageId())
.eq(bo.getUserId() != null, AiTokenUsage::getUserId, bo.getUserId())
.eq(bo.getToken() != null, AiTokenUsage::getToken, bo.getToken())
.eq(bo.getModelId() != null, AiTokenUsage::getModelId, bo.getModelId())
.eq(bo.getPromptToken() != null, AiTokenUsage::getPromptToken, bo.getPromptToken())
.eq(bo.getCompletionToken() != null, AiTokenUsage::getCompletionToken, bo.getCompletionToken())
.eq(bo.getTotalToken() != null, AiTokenUsage::getTotalToken, bo.getTotalToken())
.orderByDesc(AiTokenUsage::getCreateTime);
return lqw;
}
/**
* token使
*
* @param bo token使
* @return
*/
@Override
public Boolean insertByBo(AiTokenUsageBo bo) {
AiTokenUsage add = MapstructUtils.convert(bo, AiTokenUsage.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setTokenUsageId(add.getTokenUsageId());
}
return flag;
}
/**
* token使
*
* @param bo token使
* @return
*/
@Override
public Boolean updateByBo(AiTokenUsageBo bo) {
AiTokenUsage update = MapstructUtils.convert(bo, AiTokenUsage.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
*
*/
private void validEntityBeforeSave(AiTokenUsage entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* token使
*
* @param ids
* @param isValid
* @return
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteByIds(ids) > 0;
}
/**
* token使,Join sys_user and ai_model
*
* @param bo
* @param pageQuery
* @return token使
*/
@Override
public TableDataInfo<AiTokenUsageVo> queryPageJoinList(AiTokenUsageBo bo, PageQuery pageQuery) {
QueryWrapper<AiTokenUsage> wrapper = Wrappers.query();
wrapper
.eq(ObjectUtil.isNotNull(bo.getModelId()), "atu.model_id", bo.getModelId())
.like(StringUtils.isNotBlank(bo.getNickName()), "su.nick_name", bo.getNickName())
.orderByAsc("atu.token_usage_id");
Page<AiTokenUsageVo> page = baseMapper.selectAiTokenUsageJoinList(pageQuery.build(), wrapper);
return TableDataInfo.build(page);
}
}

@ -4,4 +4,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.ai.mapper.AiChatMessageDetailMapper">
<select id="selectAiChatMessageDetailJoinList" resultType="AiChatMessageDetailVo">
select
acmd.message_detail_id,
acmd.create_by,
acmd.question_content,
acmd.answer_content,
acmd.prompt_token,
acmd.completion_token,
acmd.total_token,
acmd.take_flag,
acmd.message_detail_type,
acmd.knowledge_base_id,
acmd.knowledge_content_id,
su.nick_name,
am.model_name
from ai_chat_message_detail acmd
left join sys_user su on acmd.create_by=su.user_id
left join ai_model am on acmd.model_id = am.model_id
${ew.getCustomSqlSegment}
</select>
</mapper>

@ -0,0 +1,24 @@
<?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="org.dromara.ai.mapper.AiTokenUsageMapper">
<select id="selectAiTokenUsageJoinList" resultType="AiTokenUsageVo">
select
atu.token_usage_id,
atu.user_id,
atu.model_id,
atu.prompt_token,
atu.completion_token,
atu.total_token,
su.nick_name,
am.model_name
from ai_token_usage atu
left join sys_user su on atu.user_id=su.user_id
left join ai_model am on atu.model_id = am.model_id
${ew.getCustomSqlSegment}
</select>
</mapper>

@ -148,4 +148,10 @@ public class DmsBaseMachineInfo extends TenantEntity {
*/
private String ossId;
/**
*
*/
@TableField(exist = false)
private String deviceTypeName;
}

@ -147,4 +147,9 @@ public class DmsBaseMachineInfoBo extends BaseEntity {
*/
private String ossId;
/**
*
*/
private String deviceTypeName;
}

@ -205,4 +205,9 @@ public class DmsBaseMachineInfoVo implements Serializable {
*/
private String ossId;
/**
*
*/
private String deviceTypeName;
}

@ -161,7 +161,7 @@ public class DmsBaseMachineInfoServiceImpl implements IDmsBaseMachineInfoService
.leftJoin(ProdBaseWorkshopInfo.class, ProdBaseWorkshopInfo::getWorkshopId, ProdBaseMachineInfo::getWorkshopId)
.leftJoin(ProdBaseDeviceMode.class, ProdBaseDeviceMode::getDeviceModeId, ProdBaseMachineInfo::getDeviceModeId)*/
.select(DmsBaseDeviceType::getDeviceTypeName)
.selectAs(DmsBaseDeviceType::getDeviceTypeName, DmsBaseMachineInfo::getDeviceTypeName)
.leftJoin(DmsBaseDeviceType.class, DmsBaseDeviceType::getDeviceTypeId, DmsBaseMachineInfo::getMachineType)
.eq(bo.getMachineId() != null, DmsBaseMachineInfo::getMachineId, bo.getMachineId())

@ -0,0 +1,590 @@
-- =============================================
-- WMS报表存储过程
-- 用于定时插入和更新报表数据
-- =============================================
-- 1. 退库原因分析报表数据插入存储过程
CREATE OR ALTER PROCEDURE sp_insert_return_reason_analysis
@tenant_id NVARCHAR(50) = NULL,
@material_category_id BIGINT = NULL
AS
BEGIN
SET NOCOUNT ON;
-- 删除当天的数据,重新插入
DELETE FROM wms_report_return_reason_analysis
WHERE statistics_date = CAST(GETDATE() AS DATE)
AND (@tenant_id IS NULL OR tenant_id = @tenant_id);
-- 插入新数据
INSERT INTO wms_report_return_reason_analysis (
tenant_id, return_reason_category, return_order_count, total_return_amount,
order_count_ratio, amount_ratio, material_name, material_category_name,
material_code, statistics_date
)
SELECT
ro.tenant_id,
CASE
WHEN ro.return_reason LIKE '%质量%' OR ro.return_reason LIKE '%不合格%' THEN '质量问题'
WHEN ro.return_reason LIKE '%订单%' OR ro.return_reason LIKE '%变更%' THEN '订单变更'
WHEN ro.return_reason LIKE '%损坏%' OR ro.return_reason LIKE '%破损%' THEN '物料损坏'
WHEN ro.return_reason LIKE '%过期%' OR ro.return_reason LIKE '%超期%' THEN '过期物料'
ELSE ro.return_reason
END AS returnReasonCategory,
COUNT(*) AS returnOrderCount,
SUM(ro.return_amount) AS totalReturnAmount,
CAST(CASE
WHEN (SELECT COUNT(*) FROM wms_return_order WHERE tenant_id = ro.tenant_id AND order_status = '1') = 0 THEN 0
ELSE COUNT(*) * 100.0 / (SELECT COUNT(*) FROM wms_return_order WHERE tenant_id = ro.tenant_id AND order_status = '1')
END AS DECIMAL(10,2)) AS orderCountRatio,
CAST(CASE
WHEN (SELECT SUM(return_amount) FROM wms_return_order WHERE tenant_id = ro.tenant_id AND order_status = '1') = 0
OR (SELECT SUM(return_amount) FROM wms_return_order WHERE tenant_id = ro.tenant_id AND order_status = '1') IS NULL THEN 0
ELSE SUM(ro.return_amount) * 100.0 / (SELECT SUM(return_amount) FROM wms_return_order WHERE tenant_id = ro.tenant_id AND order_status = '1')
END AS DECIMAL(10,2)) AS amountRatio,
mi.material_name,
mc.material_category_name,
mi.material_code,
CAST(GETDATE() AS DATE)
FROM wms_return_order ro
INNER JOIN base_material_info_copy1 mi ON ro.material_id = mi.material_id
INNER JOIN base_material_category mc ON mi.material_category_id = mc.material_category_id
WHERE ro.order_status = '1'
AND (@tenant_id IS NULL OR ro.tenant_id = @tenant_id)
AND (@material_category_id IS NULL OR mc.material_category_id = @material_category_id)
GROUP BY ro.tenant_id,
CASE
WHEN ro.return_reason LIKE '%质量%' OR ro.return_reason LIKE '%不合格%' THEN '质量问题'
WHEN ro.return_reason LIKE '%订单%' OR ro.return_reason LIKE '%变更%' THEN '订单变更'
WHEN ro.return_reason LIKE '%损坏%' OR ro.return_reason LIKE '%破损%' THEN '物料损坏'
WHEN ro.return_reason LIKE '%过期%' OR ro.return_reason LIKE '%超期%' THEN '过期物料'
ELSE ro.return_reason
END,
mi.material_name,
mc.material_category_name,
mi.material_code;
-- 更新汇总表
MERGE wms_report_summary AS target
USING (
SELECT
@tenant_id AS tenant_id,
'退库原因分析' AS report_type,
CAST(GETDATE() AS DATE) AS report_date,
COUNT(*) AS total_records
FROM wms_report_return_reason_analysis
WHERE statistics_date = CAST(GETDATE() AS DATE)
AND (@tenant_id IS NULL OR tenant_id = @tenant_id)
) AS source ON target.tenant_id = source.tenant_id
AND target.report_type = source.report_type
AND target.report_date = source.report_date
WHEN MATCHED THEN
UPDATE SET total_records = source.total_records, last_update_time = GETDATE()
WHEN NOT MATCHED THEN
INSERT (tenant_id, report_type, report_date, total_records)
VALUES (source.tenant_id, source.report_type, source.report_date, source.total_records);
END;
GO-- 2. 库
CREATE OR ALTER PROCEDURE sp_insert_inventory_trend_analysis
@tenant_id NVARCHAR(50) = NULL,
@material_category_id BIGINT = NULL
AS
BEGIN
SET NOCOUNT ON;
-- 删除当天的数据,重新插入
DELETE FROM wms_report_inventory_trend_analysis
WHERE statistics_date = CAST(GETDATE() AS DATE)
AND (@tenant_id IS NULL OR tenant_id = @tenant_id);
-- 插入新数据
INSERT INTO wms_report_inventory_trend_analysis (
tenant_id, material_id, material_code, material_name, material_category_name,
statistics_month, statistics_week, current_inventory_qty, week_instock_qty,
week_outstock_qty, last_week_inventory_qty, key_node_mark, statistics_date
)
SELECT
inv.tenant_id,
inv.material_id,
mi.material_code,
mi.material_name,
mc.material_category_name,
CONVERT(VARCHAR(7), GETDATE(), 126) AS statisticsMonth,
DATEPART(WEEK, GETDATE()) AS statisticsWeek,
SUM(inv.inventory_qty) AS currentInventoryQty,
ISNULL(instock_data.weekInstockQty, 0) AS weekInstockQty,
ISNULL(outstock_data.weekOutstockQty, 0) AS weekOutstockQty,
(SUM(inv.inventory_qty) + ISNULL(outstock_data.weekOutstockQty, 0) - ISNULL(instock_data.weekInstockQty, 0)) AS lastWeekInventoryQty,
CASE
WHEN ISNULL(instock_data.weekInstockQty, 0) > 1000 THEN '大额入库'
WHEN ISNULL(outstock_data.weekOutstockQty, 0) > 1000 THEN '大额出库'
ELSE '正常变动'
END AS keyNodeMark,
CAST(GETDATE() AS DATE)
FROM wms_inventory inv
INNER JOIN base_material_info_copy1 mi ON inv.material_id = mi.material_id
INNER JOIN base_material_category mc ON mi.material_category_id = mc.material_category_id
LEFT JOIN (
SELECT material_id, tenant_id, SUM(instock_qty) AS weekInstockQty
FROM wms_instock_record
WHERE create_time >= DATEADD(DAY, -7, GETDATE())
GROUP BY material_id, tenant_id
) instock_data ON inv.material_id = instock_data.material_id AND inv.tenant_id = instock_data.tenant_id
LEFT JOIN (
SELECT material_id, tenant_id, SUM(outstock_qty) AS weekOutstockQty
FROM wms_outstock_record
WHERE create_time >= DATEADD(DAY, -7, GETDATE())
GROUP BY material_id, tenant_id
) outstock_data ON inv.material_id = outstock_data.material_id AND inv.tenant_id = outstock_data.tenant_id
WHERE inv.inventory_status = '1'
AND (@tenant_id IS NULL OR inv.tenant_id = @tenant_id)
AND (@material_category_id IS NULL OR mc.material_category_id = @material_category_id)
GROUP BY inv.tenant_id, inv.material_id, mi.material_code, mi.material_name, mc.material_category_name,
instock_data.weekInstockQty, outstock_data.weekOutstockQty;
-- 更新汇总表
MERGE wms_report_summary AS target
USING (
SELECT
@tenant_id AS tenant_id,
'库存变动趋势分析' AS report_type,
CAST(GETDATE() AS DATE) AS report_date,
COUNT(*) AS total_records
FROM wms_report_inventory_trend_analysis
WHERE statistics_date = CAST(GETDATE() AS DATE)
AND (@tenant_id IS NULL OR tenant_id = @tenant_id)
) AS source ON target.tenant_id = source.tenant_id
AND target.report_type = source.report_type
AND target.report_date = source.report_date
WHEN MATCHED THEN
UPDATE SET total_records = source.total_records, last_update_time = GETDATE()
WHEN NOT MATCHED THEN
INSERT (tenant_id, report_type, report_date, total_records)
VALUES (source.tenant_id, source.report_type, source.report_date, source.total_records);
END;
GO
-- 3. 安全库存预警报表数据插入存储过程
CREATE OR ALTER PROCEDURE sp_insert_safety_stock_alert
@tenant_id NVARCHAR(50) = NULL,
@material_category_id BIGINT = NULL
AS
BEGIN
SET NOCOUNT ON;
-- 删除当天的数据,重新插入
DELETE FROM wms_report_safety_stock_alert
WHERE statistics_date = CAST(GETDATE() AS DATE)
AND (@tenant_id IS NULL OR tenant_id = @tenant_id);
-- 插入新数据
INSERT INTO wms_report_safety_stock_alert (
tenant_id, material_id, material_code, material_name, material_category_name,
current_inventory_qty, safe_stock_amount, min_stock_amount, max_stock_amount,
alert_status, difference_amount, last_update_time, statistics_date
)
SELECT
inv.tenant_id,
inv.material_id,
mi.material_code,
mi.material_name,
mc.material_category_name,
SUM(inv.inventory_qty) AS currentInventoryQty,
mi.safe_stock_amount,
mi.min_stock_amount,
mi.max_stock_amount,
CASE
WHEN SUM(inv.inventory_qty) < mi.min_stock_amount THEN '短缺预警'
WHEN SUM(inv.inventory_qty) > mi.max_stock_amount THEN '积压预警'
ELSE '正常'
END AS alertStatus,
CASE
WHEN SUM(inv.inventory_qty) < mi.min_stock_amount THEN mi.min_stock_amount - SUM(inv.inventory_qty)
WHEN SUM(inv.inventory_qty) > mi.max_stock_amount THEN SUM(inv.inventory_qty) - mi.max_stock_amount
WHEN SUM(inv.inventory_qty) < mi.safe_stock_amount THEN mi.safe_stock_amount - SUM(inv.inventory_qty)
ELSE 0
END AS differenceAmount,
inv.update_time,
CAST(GETDATE() AS DATE)
FROM wms_inventory inv
INNER JOIN base_material_info_copy1 mi ON inv.material_id = mi.material_id
INNER JOIN base_material_category mc ON mi.material_category_id = mc.material_category_id
WHERE inv.inventory_status = '1'
AND mi.active_flag = '1'
AND (mi.safe_stock_amount IS NOT NULL OR mi.min_stock_amount IS NOT NULL OR mi.max_stock_amount IS NOT NULL)
AND (@tenant_id IS NULL OR inv.tenant_id = @tenant_id)
AND (@material_category_id IS NULL OR mc.material_category_id = @material_category_id)
GROUP BY inv.tenant_id, inv.material_id, mi.material_code, mi.material_name, mc.material_category_name,
mi.safe_stock_amount, mi.min_stock_amount, mi.max_stock_amount, inv.update_time
HAVING SUM(inv.inventory_qty) < mi.safe_stock_amount
OR SUM(inv.inventory_qty) < mi.min_stock_amount
OR SUM(inv.inventory_qty) > mi.max_stock_amount;
-- 更新汇总表
MERGE wms_report_summary AS target
USING (
SELECT
@tenant_id AS tenant_id,
'安全库存预警' AS report_type,
CAST(GETDATE() AS DATE) AS report_date,
COUNT(*) AS total_records
FROM wms_report_safety_stock_alert
WHERE statistics_date = CAST(GETDATE() AS DATE)
AND (@tenant_id IS NULL OR tenant_id = @tenant_id)
) AS source ON target.tenant_id = source.tenant_id
AND target.report_type = source.report_type
AND target.report_date = source.report_date
WHEN MATCHED THEN
UPDATE SET total_records = source.total_records, last_update_time = GETDATE()
WHEN NOT MATCHED THEN
INSERT (tenant_id, report_type, report_date, total_records)
VALUES (source.tenant_id, source.report_type, source.report_date, source.total_records);
END;
GO-- 4. 呆滞料
CREATE OR ALTER PROCEDURE sp_insert_stagnant_inventory
@tenant_id NVARCHAR(50) = NULL,
@material_category_id BIGINT = NULL
AS
BEGIN
SET NOCOUNT ON;
-- 删除当天的数据,重新插入
DELETE FROM wms_report_stagnant_inventory
WHERE statistics_date = CAST(GETDATE() AS DATE)
AND (@tenant_id IS NULL OR tenant_id = @tenant_id);
-- 插入新数据
INSERT INTO wms_report_stagnant_inventory (
tenant_id, material_id, material_code, material_name, material_category_name,
stagnant_inventory_qty, material_unit, last_outstock_time, stagnant_days,
stagnant_reason, material_spec, warehouse_name, last_activity_time, statistics_date
)
SELECT
inv.tenant_id,
inv.material_id,
mi.material_code,
mi.material_name,
mc.material_category_name,
SUM(inv.inventory_qty) AS stagnantInventoryQty,
mi.material_unit,
CASE
WHEN last_out.lastOutstockTime IS NULL THEN '从未出库'
ELSE CONVERT(VARCHAR(19), last_out.lastOutstockTime, 120)
END AS lastOutstockTime,
CASE
WHEN last_out.lastOutstockTime IS NULL THEN DATEDIFF(DAY, first_in.firstInstockTime, GETDATE())
ELSE DATEDIFF(DAY, last_out.lastOutstockTime, GETDATE())
END AS stagnantDays,
CASE
WHEN last_out.lastOutstockTime IS NULL THEN '从未出库'
WHEN DATEDIFF(DAY, last_out.lastOutstockTime, GETDATE()) > 180 THEN '超过6个月未出库'
ELSE '正常'
END AS stagnantReason,
mi.material_spec,
w.warehouse_name,
CASE
WHEN last_out.lastOutstockTime IS NULL THEN CONVERT(VARCHAR(19), first_in.firstInstockTime, 120)
ELSE CONVERT(VARCHAR(19), last_out.lastOutstockTime, 120)
END AS lastActivityTime,
CAST(GETDATE() AS DATE)
FROM wms_inventory inv
INNER JOIN base_material_info_copy1 mi ON inv.material_id = mi.material_id
INNER JOIN base_material_category mc ON mi.material_category_id = mc.material_category_id
INNER JOIN wms_base_warehouse w ON inv.warehouse_id = w.warehouse_id
LEFT JOIN (
SELECT
material_id,
tenant_id,
MIN(create_time) AS firstInstockTime
FROM wms_instock_record
WHERE create_time IS NOT NULL
GROUP BY material_id, tenant_id
) first_in ON inv.material_id = first_in.material_id AND inv.tenant_id = first_in.tenant_id
LEFT JOIN (
SELECT
material_id,
tenant_id,
MAX(create_time) AS lastOutstockTime
FROM wms_outstock_record
WHERE create_time IS NOT NULL
GROUP BY material_id, tenant_id
) last_out ON inv.material_id = last_out.material_id AND inv.tenant_id = last_out.tenant_id
WHERE inv.inventory_status = '1'
AND inv.inventory_qty > 0
AND mi.active_flag = '1'
AND mi.del_flag = '0'
AND (
last_out.lastOutstockTime IS NULL
OR DATEDIFF(DAY, last_out.lastOutstockTime, GETDATE()) >= 180
)
AND (@tenant_id IS NULL OR inv.tenant_id = @tenant_id)
AND (@material_category_id IS NULL OR mc.material_category_id = @material_category_id)
GROUP BY
inv.tenant_id,
inv.material_id,
mi.material_code,
mi.material_name,
mc.material_category_name,
mi.material_unit,
last_out.lastOutstockTime,
mi.material_spec,
w.warehouse_name,
first_in.firstInstockTime;
-- 更新汇总表
MERGE wms_report_summary AS target
USING (
SELECT
@tenant_id AS tenant_id,
'呆滞料库存' AS report_type,
CAST(GETDATE() AS DATE) AS report_date,
COUNT(*) AS total_records
FROM wms_report_stagnant_inventory
WHERE statistics_date = CAST(GETDATE() AS DATE)
AND (@tenant_id IS NULL OR tenant_id = @tenant_id)
) AS source ON target.tenant_id = source.tenant_id
AND target.report_type = source.report_type
AND target.report_date = source.report_date
WHEN MATCHED THEN
UPDATE SET total_records = source.total_records, last_update_time = GETDATE()
WHEN NOT MATCHED THEN
INSERT (tenant_id, report_type, report_date, total_records)
VALUES (source.tenant_id, source.report_type, source.report_date, source.total_records);
END;
GO
-- 5. 库存差异报表数据插入存储过程
CREATE OR ALTER PROCEDURE sp_insert_inventory_difference
@tenant_id NVARCHAR(50) = NULL,
@material_category_id BIGINT = NULL
AS
BEGIN
SET NOCOUNT ON;
-- 删除当天的数据,重新插入
DELETE FROM wms_report_inventory_difference
WHERE statistics_date = CAST(GETDATE() AS DATE)
AND (@tenant_id IS NULL OR tenant_id = @tenant_id);
-- 插入新数据
INSERT INTO wms_report_inventory_difference (
tenant_id, check_code, check_type, warehouse_id, warehouse_name,
material_id, material_code, material_name, material_category_name,
book_inventory_qty, actual_inventory_qty, difference_qty, difference_type,
difference_rate, difference_level, check_time, check_by, statistics_date
)
SELECT
ic.tenant_id,
ic.check_code,
CASE ic.check_type
WHEN '0' THEN '抽检'
WHEN '1' THEN '盘点'
WHEN '2' THEN '库位/货架盘点'
ELSE '未知'
END AS checkType,
ic.plan_warehouse_id,
w.warehouse_name,
icr.material_id,
mi.material_code,
mi.material_name,
mc.material_category_name,
icr.inventory_qty AS bookInventoryQty,
icr.check_qty AS actualInventoryQty,
(icr.check_qty - icr.inventory_qty) AS differenceQty,
CASE
WHEN ABS(icr.check_qty - icr.inventory_qty) = 0 THEN '无差异'
WHEN (icr.check_qty - icr.inventory_qty) > 0 THEN '盘盈'
ELSE '盘亏'
END AS differenceType,
CASE
WHEN icr.inventory_qty > 0 THEN
ABS((icr.check_qty - icr.inventory_qty) / icr.inventory_qty * 100)
ELSE 0
END AS differenceRate,
CASE
WHEN ABS(icr.check_qty - icr.inventory_qty) > 10 THEN '重大差异'
WHEN ABS(icr.check_qty - icr.inventory_qty) > 5 THEN '一般差异'
WHEN ABS(icr.check_qty - icr.inventory_qty) > 0 THEN '轻微差异'
ELSE '无差异'
END AS differenceLevel,
ic.create_time,
ic.create_by,
CAST(GETDATE() AS DATE)
FROM wms_inventory_check ic
INNER JOIN wms_base_warehouse w ON ic.plan_warehouse_id = w.warehouse_id
INNER JOIN wms_inventory_check_record icr ON icr.check_code = ic.check_code AND icr.tenant_id = ic.tenant_id
INNER JOIN base_material_info_copy1 mi ON icr.material_id = mi.material_id
INNER JOIN base_material_category mc ON mi.material_category_id = mc.material_category_id
WHERE ic.check_status = '3'
AND (@tenant_id IS NULL OR ic.tenant_id = @tenant_id)
AND (@material_category_id IS NULL OR mc.material_category_id = @material_category_id);
-- 更新汇总表
MERGE wms_report_summary AS target
USING (
SELECT
@tenant_id AS tenant_id,
'库存差异' AS report_type,
CAST(GETDATE() AS DATE) AS report_date,
COUNT(*) AS total_records
FROM wms_report_inventory_difference
WHERE statistics_date = CAST(GETDATE() AS DATE)
AND (@tenant_id IS NULL OR tenant_id = @tenant_id)
) AS source ON target.tenant_id = source.tenant_id
AND target.report_type = source.report_type
AND target.report_date = source.report_date
WHEN MATCHED THEN
UPDATE SET total_records = source.total_records, last_update_time = GETDATE()
WHEN NOT MATCHED THEN
INSERT (tenant_id, report_type, report_date, total_records)
VALUES (source.tenant_id, source.report_type, source.report_date, source.total_records);
END;
GO--
6.
CREATE OR ALTER PROCEDURE sp_insert_inventory_turnover
@tenant_id NVARCHAR(50) = NULL,
@material_category_id BIGINT = NULL
AS
BEGIN
SET NOCOUNT ON;
-- 删除当天的数据,重新插入
DELETE FROM wms_report_inventory_turnover
WHERE statistics_date = CAST(GETDATE() AS DATE)
AND (@tenant_id IS NULL OR tenant_id = @tenant_id);
-- 插入新数据
INSERT INTO wms_report_inventory_turnover (
tenant_id, material_id, material_code, material_name, material_category_name,
statistics_month, begin_inventory_qty, end_inventory_qty, month_outstock_qty,
inventory_turnover_rate, simple_turnover_rate, turnover_evaluation, statistics_date
)
SELECT
base_data.tenantId,
base_data.materialId,
base_data.materialCode,
base_data.materialName,
base_data.materialCategoryName,
base_data.statisticsMonth,
base_data.beginInventoryQty,
base_data.endInventoryQty,
base_data.monthOutstockQty,
CASE
WHEN (base_data.beginInventoryQty + base_data.endInventoryQty) > 0 THEN
CAST(base_data.monthOutstockQty * 2.0 / (base_data.beginInventoryQty + base_data.endInventoryQty) * 100 AS DECIMAL(10,2))
ELSE 0
END AS inventoryTurnoverRate,
CASE
WHEN base_data.endInventoryQty > 0 THEN
CAST(base_data.monthOutstockQty / base_data.endInventoryQty * 100 AS DECIMAL(10,2))
ELSE 0
END AS simpleTurnoverRate,
CASE
WHEN base_data.monthOutstockQty = 0 THEN '无流动'
WHEN CAST(base_data.monthOutstockQty * 2.0 / (base_data.beginInventoryQty + base_data.endInventoryQty) * 100 AS DECIMAL(10,2)) >= 100 THEN '快速周转'
WHEN CAST(base_data.monthOutstockQty * 2.0 / (base_data.beginInventoryQty + base_data.endInventoryQty) * 100 AS DECIMAL(10,2)) >= 50 THEN '正常周转'
ELSE '缓慢周转'
END AS turnoverEvaluation,
CAST(GETDATE() AS DATE)
FROM (
SELECT
inv.tenant_id AS tenantId,
inv.material_id AS materialId,
mi.material_code AS materialCode,
mi.material_name AS materialName,
mc.material_category_name AS materialCategoryName,
CONVERT(VARCHAR(7), GETDATE(), 126) AS statisticsMonth,
SUM(inv.inventory_qty) AS endInventoryQty,
SUM(inv.inventory_qty) + ISNULL(month_out.monthOutstockQty, 0) - ISNULL(month_in.monthInstockQty, 0) AS beginInventoryQty,
ISNULL(month_out.monthOutstockQty, 0) AS monthOutstockQty
FROM wms_inventory inv
INNER JOIN base_material_info_copy1 mi ON inv.material_id = mi.material_id
INNER JOIN base_material_category mc ON mi.material_category_id = mc.material_category_id
LEFT JOIN (
SELECT material_id, tenant_id, SUM(outstock_qty) AS monthOutstockQty
FROM wms_outstock_record
WHERE create_time >= DATEADD(MONTH, -1, GETDATE())
GROUP BY material_id, tenant_id
) month_out ON inv.material_id = month_out.material_id AND inv.tenant_id = month_out.tenant_id
LEFT JOIN (
SELECT material_id, tenant_id, SUM(instock_qty) AS monthInstockQty
FROM wms_instock_record
WHERE create_time >= DATEADD(MONTH, -1, GETDATE())
GROUP BY material_id, tenant_id
) month_in ON inv.material_id = month_in.material_id AND inv.tenant_id = month_in.tenant_id
WHERE inv.inventory_status = '1'
AND (@tenant_id IS NULL OR inv.tenant_id = @tenant_id)
AND (@material_category_id IS NULL OR mc.material_category_id = @material_category_id)
GROUP BY inv.tenant_id, inv.material_id, mi.material_code, mi.material_name, mc.material_category_name,
month_out.monthOutstockQty, month_in.monthInstockQty
) base_data
WHERE base_data.beginInventoryQty > 0 OR base_data.endInventoryQty > 0 OR base_data.monthOutstockQty > 0;
-- 更新汇总表
MERGE wms_report_summary AS target
USING (
SELECT
@tenant_id AS tenant_id,
'库存周转' AS report_type,
CAST(GETDATE() AS DATE) AS report_date,
COUNT(*) AS total_records
FROM wms_report_inventory_turnover
WHERE statistics_date = CAST(GETDATE() AS DATE)
AND (@tenant_id IS NULL OR tenant_id = @tenant_id)
) AS source ON target.tenant_id = source.tenant_id
AND target.report_type = source.report_type
AND target.report_date = source.report_date
WHEN MATCHED THEN
UPDATE SET total_records = source.total_records, last_update_time = GETDATE()
WHEN NOT MATCHED THEN
INSERT (tenant_id, report_type, report_date, total_records)
VALUES (source.tenant_id, source.report_type, source.report_date, source.total_records);
END;
GO
-- 7. 主存储过程 - 执行所有报表数据更新
CREATE OR ALTER PROCEDURE sp_update_all_wms_reports
@tenant_id NVARCHAR(50) = NULL,
@material_category_id BIGINT = NULL
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
PRINT '开始更新WMS报表数据...'
-- 1. 更新退库原因分析报表
PRINT '更新退库原因分析报表...'
EXEC sp_insert_return_reason_analysis @tenant_id, @material_category_id;
-- 2. 更新库存变动趋势分析报表
PRINT '更新库存变动趋势分析报表...'
EXEC sp_insert_inventory_trend_analysis @tenant_id, @material_category_id;
-- 3. 更新安全库存预警报表
PRINT '更新安全库存预警报表...'
EXEC sp_insert_safety_stock_alert @tenant_id, @material_category_id;
-- 4. 更新呆滞料库存报表
PRINT '更新呆滞料库存报表...'
EXEC sp_insert_stagnant_inventory @tenant_id, @material_category_id;
-- 5. 更新库存差异报表
PRINT '更新库存差异报表...'
EXEC sp_insert_inventory_difference @tenant_id, @material_category_id;
-- 6. 更新库存周转报表
PRINT '更新库存周转报表...'
EXEC sp_insert_inventory_turnover @tenant_id, @material_category_id;
PRINT 'WMS报表数据更新完成!'
END TRY
BEGIN CATCH
PRINT 'WMS报表数据更新失败: ' + ERROR_MESSAGE()
THROW;
END CATCH
END;
GO

@ -0,0 +1,454 @@
-- =============================================
-- WMS报表数据库表设计
-- 基于WmsReportMapper中的查询SQL设计包含完整中文注释
-- =============================================
-- 1. 退库原因分析报表表
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[wms_report_return_reason_analysis]') AND type in (N'U'))
DROP TABLE [dbo].[wms_report_return_reason_analysis]
GO
CREATE TABLE wms_report_return_reason_analysis (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
tenant_id NVARCHAR(50) NOT NULL,
return_reason_category NVARCHAR(100) NOT NULL,
return_order_count INT NOT NULL DEFAULT 0,
total_return_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
order_count_ratio DECIMAL(10,2) NOT NULL DEFAULT 0,
amount_ratio DECIMAL(10,2) NOT NULL DEFAULT 0,
material_name NVARCHAR(200),
material_category_name NVARCHAR(100),
material_code NVARCHAR(100),
statistics_date DATE NOT NULL DEFAULT GETDATE(),
create_time DATETIME2 NOT NULL DEFAULT GETDATE(),
update_time DATETIME2 NOT NULL DEFAULT GETDATE(),
INDEX IX_tenant_date (tenant_id, statistics_date),
INDEX IX_category (return_reason_category),
INDEX IX_material (material_code)
);
-- 添加表注释
EXEC sp_addextendedproperty 'MS_Description', N'退库原因分析报表', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis'
GO
-- 添加字段注释
EXEC sp_addextendedproperty 'MS_Description', N'主键ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'租户编号', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'tenant_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'退库原因分类(质量问题/订单变更/物料损坏/过期物料等)', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'return_reason_category'
GO
EXEC sp_addextendedproperty 'MS_Description', N'退库单数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'return_order_count'
GO
EXEC sp_addextendedproperty 'MS_Description', N'退库总数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'total_return_amount'
GO
EXEC sp_addextendedproperty 'MS_Description', N'单据数量占比(%', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'order_count_ratio'
GO
EXEC sp_addextendedproperty 'MS_Description', N'数量占比(%', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'amount_ratio'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'material_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料分类名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'material_category_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料编码', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'material_code'
GO
EXEC sp_addextendedproperty 'MS_Description', N'统计日期', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'statistics_date'
GO
EXEC sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'create_time'
GO
EXEC sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_return_reason_analysis', 'COLUMN', N'update_time'
GO
-- 2. 库存变动趋势分析报表表
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[wms_report_inventory_trend_analysis]') AND type in (N'U'))
DROP TABLE [dbo].[wms_report_inventory_trend_analysis]
GO
CREATE TABLE wms_report_inventory_trend_analysis (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
tenant_id NVARCHAR(50) NOT NULL,
material_id BIGINT NOT NULL,
material_code NVARCHAR(100) NOT NULL,
material_name NVARCHAR(200) NOT NULL,
material_category_name NVARCHAR(100),
statistics_month NVARCHAR(7) NOT NULL, -- YYYY-MM格式
statistics_week INT NOT NULL,
current_inventory_qty DECIMAL(18,2) NOT NULL DEFAULT 0,
week_instock_qty DECIMAL(18,2) NOT NULL DEFAULT 0,
week_outstock_qty DECIMAL(18,2) NOT NULL DEFAULT 0,
last_week_inventory_qty DECIMAL(18,2) NOT NULL DEFAULT 0,
key_node_mark NVARCHAR(50),
statistics_date DATE NOT NULL DEFAULT GETDATE(),
create_time DATETIME2 NOT NULL DEFAULT GETDATE(),
update_time DATETIME2 NOT NULL DEFAULT GETDATE(),
INDEX IX_tenant_material (tenant_id, material_id),
INDEX IX_month_week (statistics_month, statistics_week),
INDEX IX_material_code (material_code)
);
-- 添加表注释
EXEC sp_addextendedproperty 'MS_Description', N'库存变动趋势分析报表', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis'
GO
-- 添加字段注释
EXEC sp_addextendedproperty 'MS_Description', N'主键ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'租户编号', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'tenant_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'material_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料编码', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'material_code'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'material_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料分类名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'material_category_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'统计月份YYYY-MM格式', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'statistics_month'
GO
EXEC sp_addextendedproperty 'MS_Description', N'统计周次', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'statistics_week'
GO
EXEC sp_addextendedproperty 'MS_Description', N'当前库存数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'current_inventory_qty'
GO
EXEC sp_addextendedproperty 'MS_Description', N'本周入库数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'week_instock_qty'
GO
EXEC sp_addextendedproperty 'MS_Description', N'本周出库数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'week_outstock_qty'
GO
EXEC sp_addextendedproperty 'MS_Description', N'上周库存数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'last_week_inventory_qty'
GO
EXEC sp_addextendedproperty 'MS_Description', N'关键节点标记(大额入库/大额出库/正常变动)', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'key_node_mark'
GO
EXEC sp_addextendedproperty 'MS_Description', N'统计日期', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'statistics_date'
GO
EXEC sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'create_time'
GO
EXEC sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_trend_analysis', 'COLUMN', N'update_time'
GO
-- 3. 安全库存预警报表表
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[wms_report_safety_stock_alert]') AND type in (N'U'))
DROP TABLE [dbo].[wms_report_safety_stock_alert]
GO
CREATE TABLE wms_report_safety_stock_alert (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
tenant_id NVARCHAR(50) NOT NULL,
material_id BIGINT NOT NULL,
material_code NVARCHAR(100) NOT NULL,
material_name NVARCHAR(200) NOT NULL,
material_category_name NVARCHAR(100),
current_inventory_qty DECIMAL(18,2) NOT NULL DEFAULT 0,
safe_stock_amount DECIMAL(18,2),
min_stock_amount DECIMAL(18,2),
max_stock_amount DECIMAL(18,2),
alert_status NVARCHAR(50) NOT NULL, -- 短缺预警/积压预警/正常
difference_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
last_update_time DATETIME2,
statistics_date DATE NOT NULL DEFAULT GETDATE(),
create_time DATETIME2 NOT NULL DEFAULT GETDATE(),
update_time DATETIME2 NOT NULL DEFAULT GETDATE(),
INDEX IX_tenant_material (tenant_id, material_id),
INDEX IX_alert_status (alert_status),
INDEX IX_material_code (material_code)
);
-- 添加表注释
EXEC sp_addextendedproperty 'MS_Description', N'安全库存预警报表', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert'
GO
-- 添加字段注释
EXEC sp_addextendedproperty 'MS_Description', N'主键ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'租户编号', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'tenant_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'material_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料编码', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'material_code'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'material_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料分类名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'material_category_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'当前库存数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'current_inventory_qty'
GO
EXEC sp_addextendedproperty 'MS_Description', N'安全库存数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'safe_stock_amount'
GO
EXEC sp_addextendedproperty 'MS_Description', N'最小库存数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'min_stock_amount'
GO
EXEC sp_addextendedproperty 'MS_Description', N'最大库存数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'max_stock_amount'
GO
EXEC sp_addextendedproperty 'MS_Description', N'预警状态(短缺预警/积压预警/正常)', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'alert_status'
GO
EXEC sp_addextendedproperty 'MS_Description', N'差异数量(超出或不足的数量)', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'difference_amount'
GO
EXEC sp_addextendedproperty 'MS_Description', N'最后更新时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'last_update_time'
GO
EXEC sp_addextendedproperty 'MS_Description', N'统计日期', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'statistics_date'
GO
EXEC sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'create_time'
GO
EXEC sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_safety_stock_alert', 'COLUMN', N'update_time'
GO
-- 4. 呆滞料库存报表表
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[wms_report_stagnant_inventory]') AND type in (N'U'))
DROP TABLE [dbo].[wms_report_stagnant_inventory]
GO
CREATE TABLE wms_report_stagnant_inventory (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
tenant_id NVARCHAR(50) NOT NULL,
material_id BIGINT NOT NULL,
material_code NVARCHAR(100) NOT NULL,
material_name NVARCHAR(200) NOT NULL,
material_category_name NVARCHAR(100),
stagnant_inventory_qty DECIMAL(18,2) NOT NULL DEFAULT 0,
material_unit NVARCHAR(20),
last_outstock_time NVARCHAR(50), -- 可能是"从未出库"或日期时间
stagnant_days INT NOT NULL DEFAULT 0,
stagnant_reason NVARCHAR(100),
material_spec NVARCHAR(200),
warehouse_name NVARCHAR(100),
last_activity_time NVARCHAR(50),
statistics_date DATE NOT NULL DEFAULT GETDATE(),
create_time DATETIME2 NOT NULL DEFAULT GETDATE(),
update_time DATETIME2 NOT NULL DEFAULT GETDATE(),
INDEX IX_tenant_material (tenant_id, material_id),
INDEX IX_stagnant_days (stagnant_days),
INDEX IX_material_code (material_code)
);
-- 添加表注释
EXEC sp_addextendedproperty 'MS_Description', N'呆滞料库存报表', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory'
GO
-- 添加字段注释
EXEC sp_addextendedproperty 'MS_Description', N'主键ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'租户编号', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'tenant_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'material_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料编码', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'material_code'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'material_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料分类名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'material_category_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'呆滞库存数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'stagnant_inventory_qty'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料单位', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'material_unit'
GO
EXEC sp_addextendedproperty 'MS_Description', N'最后出库时间(可能是"从未出库"或具体日期时间)', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'last_outstock_time'
GO
EXEC sp_addextendedproperty 'MS_Description', N'呆滞天数', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'stagnant_days'
GO
EXEC sp_addextendedproperty 'MS_Description', N'呆滞原因(从未出库/超过6个月未出库/正常)', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'stagnant_reason'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料规格', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'material_spec'
GO
EXEC sp_addextendedproperty 'MS_Description', N'仓库名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'warehouse_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'最后活动时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'last_activity_time'
GO
EXEC sp_addextendedproperty 'MS_Description', N'统计日期', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'statistics_date'
GO
EXEC sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'create_time'
GO
EXEC sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_stagnant_inventory', 'COLUMN', N'update_time'
GO-- 5. 库存差异报表表
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[wms_report_inventory_difference]') AND type in (N'U'))
DROP TABLE [dbo].[wms_report_inventory_difference]
GO
CREATE TABLE wms_report_inventory_difference (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
tenant_id NVARCHAR(50) NOT NULL,
check_code NVARCHAR(100) NOT NULL,
check_type NVARCHAR(50) NOT NULL, -- 抽检/盘点/库位/货架盘点
warehouse_id BIGINT,
warehouse_name NVARCHAR(100),
material_id BIGINT NOT NULL,
material_code NVARCHAR(100) NOT NULL,
material_name NVARCHAR(200) NOT NULL,
material_category_name NVARCHAR(100),
book_inventory_qty DECIMAL(18,2) NOT NULL DEFAULT 0,
actual_inventory_qty DECIMAL(18,2) NOT NULL DEFAULT 0,
difference_qty DECIMAL(18,2) NOT NULL DEFAULT 0,
difference_type NVARCHAR(20) NOT NULL, -- 无差异/盘盈/盘亏
difference_rate DECIMAL(10,2) NOT NULL DEFAULT 0,
difference_level NVARCHAR(20) NOT NULL, -- 重大差异/一般差异/轻微差异/无差异
check_time DATETIME2,
check_by NVARCHAR(100),
statistics_date DATE NOT NULL DEFAULT GETDATE(),
create_time DATETIME2 NOT NULL DEFAULT GETDATE(),
update_time DATETIME2 NOT NULL DEFAULT GETDATE(),
INDEX IX_tenant_check (tenant_id, check_code),
INDEX IX_difference_type (difference_type),
INDEX IX_material_code (material_code)
);
-- 添加表注释
EXEC sp_addextendedproperty 'MS_Description', N'库存差异报表', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference'
GO
-- 添加字段注释
EXEC sp_addextendedproperty 'MS_Description', N'主键ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'租户编号', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'tenant_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'盘点单号', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'check_code'
GO
EXEC sp_addextendedproperty 'MS_Description', N'盘点类型(抽检/盘点/库位/货架盘点)', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'check_type'
GO
EXEC sp_addextendedproperty 'MS_Description', N'仓库ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'warehouse_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'仓库名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'warehouse_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'material_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料编码', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'material_code'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'material_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料分类名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'material_category_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'账面库存数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'book_inventory_qty'
GO
EXEC sp_addextendedproperty 'MS_Description', N'实际库存数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'actual_inventory_qty'
GO
EXEC sp_addextendedproperty 'MS_Description', N'差异数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'difference_qty'
GO
EXEC sp_addextendedproperty 'MS_Description', N'差异类型(无差异/盘盈/盘亏)', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'difference_type'
GO
EXEC sp_addextendedproperty 'MS_Description', N'差异率(%', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'difference_rate'
GO
EXEC sp_addextendedproperty 'MS_Description', N'差异等级(重大差异/一般差异/轻微差异/无差异)', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'difference_level'
GO
EXEC sp_addextendedproperty 'MS_Description', N'盘点时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'check_time'
GO
EXEC sp_addextendedproperty 'MS_Description', N'盘点人', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'check_by'
GO
EXEC sp_addextendedproperty 'MS_Description', N'统计日期', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'statistics_date'
GO
EXEC sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'create_time'
GO
EXEC sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_difference', 'COLUMN', N'update_time'
GO
-- 6. 库存周转报表表
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[wms_report_inventory_turnover]') AND type in (N'U'))
DROP TABLE [dbo].[wms_report_inventory_turnover]
GO
CREATE TABLE wms_report_inventory_turnover (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
tenant_id NVARCHAR(50) NOT NULL,
material_id BIGINT NOT NULL,
material_code NVARCHAR(100) NOT NULL,
material_name NVARCHAR(200) NOT NULL,
material_category_name NVARCHAR(100),
statistics_month NVARCHAR(7) NOT NULL, -- YYYY-MM格式
begin_inventory_qty DECIMAL(18,2) NOT NULL DEFAULT 0,
end_inventory_qty DECIMAL(18,2) NOT NULL DEFAULT 0,
month_outstock_qty DECIMAL(18,2) NOT NULL DEFAULT 0,
inventory_turnover_rate DECIMAL(10,2) NOT NULL DEFAULT 0,
simple_turnover_rate DECIMAL(10,2) NOT NULL DEFAULT 0,
turnover_evaluation NVARCHAR(50), -- 无流动/快速周转/正常周转/缓慢周转
statistics_date DATE NOT NULL DEFAULT GETDATE(),
create_time DATETIME2 NOT NULL DEFAULT GETDATE(),
update_time DATETIME2 NOT NULL DEFAULT GETDATE(),
INDEX IX_tenant_material (tenant_id, material_id),
INDEX IX_month (statistics_month),
INDEX IX_turnover_rate (inventory_turnover_rate),
INDEX IX_material_code (material_code)
);
-- 添加表注释
EXEC sp_addextendedproperty 'MS_Description', N'库存周转报表', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover'
GO
-- 添加字段注释
EXEC sp_addextendedproperty 'MS_Description', N'主键ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'租户编号', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'tenant_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'material_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料编码', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'material_code'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'material_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'物料分类名称', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'material_category_name'
GO
EXEC sp_addextendedproperty 'MS_Description', N'统计月份YYYY-MM格式', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'statistics_month'
GO
EXEC sp_addextendedproperty 'MS_Description', N'期初库存数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'begin_inventory_qty'
GO
EXEC sp_addextendedproperty 'MS_Description', N'期末库存数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'end_inventory_qty'
GO
EXEC sp_addextendedproperty 'MS_Description', N'月出库数量', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'month_outstock_qty'
GO
EXEC sp_addextendedproperty 'MS_Description', N'库存周转率(%', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'inventory_turnover_rate'
GO
EXEC sp_addextendedproperty 'MS_Description', N'简单周转率(%', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'simple_turnover_rate'
GO
EXEC sp_addextendedproperty 'MS_Description', N'周转评价(无流动/快速周转/正常周转/缓慢周转)', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'turnover_evaluation'
GO
EXEC sp_addextendedproperty 'MS_Description', N'统计日期', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'statistics_date'
GO
EXEC sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'create_time'
GO
EXEC sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_inventory_turnover', 'COLUMN', N'update_time'
GO
-- =============================================
-- 7. 报表数据汇总表(可选,用于快速查询)
-- =============================================
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[wms_report_summary]') AND type in (N'U'))
DROP TABLE [dbo].[wms_report_summary]
GO
CREATE TABLE wms_report_summary (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
tenant_id NVARCHAR(50) NOT NULL,
report_type NVARCHAR(50) NOT NULL, -- 报表类型
report_date DATE NOT NULL,
total_records INT NOT NULL DEFAULT 0,
last_update_time DATETIME2 NOT NULL DEFAULT GETDATE(),
create_time DATETIME2 NOT NULL DEFAULT GETDATE(),
INDEX IX_tenant_type_date (tenant_id, report_type, report_date)
);
-- 添加表注释
EXEC sp_addextendedproperty 'MS_Description', N'WMS报表数据汇总表', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_summary'
GO
-- 添加字段注释
EXEC sp_addextendedproperty 'MS_Description', N'主键ID', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_summary', 'COLUMN', N'id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'租户编号', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_summary', 'COLUMN', N'tenant_id'
GO
EXEC sp_addextendedproperty 'MS_Description', N'报表类型(退库原因分析/库存变动趋势分析/安全库存预警/呆滞料库存/库存差异/库存周转)', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_summary', 'COLUMN', N'report_type'
GO
EXEC sp_addextendedproperty 'MS_Description', N'报表日期', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_summary', 'COLUMN', N'report_date'
GO
EXEC sp_addextendedproperty 'MS_Description', N'记录总数', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_summary', 'COLUMN', N'total_records'
GO
EXEC sp_addextendedproperty 'MS_Description', N'最后更新时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_summary', 'COLUMN', N'last_update_time'
GO
EXEC sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', N'dbo', 'TABLE', N'wms_report_summary', 'COLUMN', N'create_time'
GO
-- =============================================
-- 表创建完成提示
-- =============================================
PRINT '=== WMS报表表结构创建完成 ==='
PRINT '已创建以下报表表:'
PRINT '1. wms_report_return_reason_analysis - 退库原因分析报表'
PRINT '2. wms_report_inventory_trend_analysis - 库存变动趋势分析报表'
PRINT '3. wms_report_safety_stock_alert - 安全库存预警报表'
PRINT '4. wms_report_stagnant_inventory - 呆滞料库存报表'
PRINT '5. wms_report_inventory_difference - 库存差异报表'
PRINT '6. wms_report_inventory_turnover - 库存周转报表'
PRINT '7. wms_report_summary - 报表数据汇总表'
PRINT '=== 所有表均包含完整的中文注释可在Navicat中查看 ==='
Loading…
Cancel
Save