From c2ef40f2cfd589a91738a202b9e3949f57a2c681 Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Mon, 29 Sep 2025 15:48:56 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(dms):=20=E6=B7=BB=E5=8A=A0=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E7=B1=BB=E5=9E=8B=E5=90=8D=E7=A7=B0=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 DmsBaseMachineInfo 实体类中新增 deviceTypeName 字段 - 在 DmsBaseMachineInfoBo 业务对象中新增 deviceTypeName段 - 在 字 DmsBaseMachineInfoServiceImpl 中配置 deviceTypeName 的查询映射 - 在 DmsBaseMachineInfoVo 视图对象中新增 deviceTypeName 字段 --- .../java/org/dromara/dms/domain/DmsBaseMachineInfo.java | 6 ++++++ .../org/dromara/dms/domain/bo/DmsBaseMachineInfoBo.java | 5 +++++ .../org/dromara/dms/domain/vo/DmsBaseMachineInfoVo.java | 5 +++++ .../dms/service/impl/DmsBaseMachineInfoServiceImpl.java | 2 +- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/domain/DmsBaseMachineInfo.java b/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/domain/DmsBaseMachineInfo.java index 8ce3c5f9..59a3c5b5 100644 --- a/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/domain/DmsBaseMachineInfo.java +++ b/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/domain/DmsBaseMachineInfo.java @@ -148,4 +148,10 @@ public class DmsBaseMachineInfo extends TenantEntity { */ private String ossId; + /** + * 设备类型名称 + */ + @TableField(exist = false) + private String deviceTypeName; + } diff --git a/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/domain/bo/DmsBaseMachineInfoBo.java b/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/domain/bo/DmsBaseMachineInfoBo.java index 52b33351..91ed621e 100644 --- a/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/domain/bo/DmsBaseMachineInfoBo.java +++ b/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/domain/bo/DmsBaseMachineInfoBo.java @@ -147,4 +147,9 @@ public class DmsBaseMachineInfoBo extends BaseEntity { */ private String ossId; + /** + * 设备类型名称 + */ + private String deviceTypeName; + } diff --git a/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/domain/vo/DmsBaseMachineInfoVo.java b/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/domain/vo/DmsBaseMachineInfoVo.java index 42b462c4..4b505aae 100644 --- a/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/domain/vo/DmsBaseMachineInfoVo.java +++ b/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/domain/vo/DmsBaseMachineInfoVo.java @@ -205,4 +205,9 @@ public class DmsBaseMachineInfoVo implements Serializable { */ private String ossId; + /** + * 设备类型名称 + */ + private String deviceTypeName; + } diff --git a/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/service/impl/DmsBaseMachineInfoServiceImpl.java b/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/service/impl/DmsBaseMachineInfoServiceImpl.java index 58753479..39f65fc8 100644 --- a/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/service/impl/DmsBaseMachineInfoServiceImpl.java +++ b/ruoyi-modules/hwmom-dms/src/main/java/org/dromara/dms/service/impl/DmsBaseMachineInfoServiceImpl.java @@ -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()) From 148e150029222e5b8f08e5ab6d3aafc7d7ca6455 Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Mon, 29 Sep 2025 15:50:53 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(wms):=20=E6=B7=BB=E5=8A=A06=E4=B8=AA?= =?UTF-8?q?=E6=8A=A5=E8=A1=A8=E7=9A=84=E5=AD=98=E5=82=A8=E8=BF=87=E7=A8=8B?= =?UTF-8?q?=E5=92=8C=E8=A1=A8=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增7个WMS报表相关存储过程,包括退库原因分析、库存变动趋势分析、安全库存预警、呆滞料库存、库存差异、库存周转等 - 创建6个报表数据表和1个报表汇总表,支持定时数据更新和统计分析 - 实现主存储过程sp_update_all_wms_reports统一执行所有报表数据更新 - 添加索引优化查询性能,支持租户ID和统计日期等关键字段筛选- 支持物料分类和租户维度的数据隔离和统计分析 --- .../hwmom-wms/wms_report_procedures.sql | 590 ++++++++++++++++++ ruoyi-modules/hwmom-wms/wms_report_tables.sql | 159 +++++ 2 files changed, 749 insertions(+) create mode 100644 ruoyi-modules/hwmom-wms/wms_report_procedures.sql create mode 100644 ruoyi-modules/hwmom-wms/wms_report_tables.sql diff --git a/ruoyi-modules/hwmom-wms/wms_report_procedures.sql b/ruoyi-modules/hwmom-wms/wms_report_procedures.sql new file mode 100644 index 00000000..e7cc60fc --- /dev/null +++ b/ruoyi-modules/hwmom-wms/wms_report_procedures.sql @@ -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 \ No newline at end of file diff --git a/ruoyi-modules/hwmom-wms/wms_report_tables.sql b/ruoyi-modules/hwmom-wms/wms_report_tables.sql new file mode 100644 index 00000000..414c2d2e --- /dev/null +++ b/ruoyi-modules/hwmom-wms/wms_report_tables.sql @@ -0,0 +1,159 @@ +-- ============================================= +-- WMS报表数据库表设计 +-- 基于WmsReportMapper中的查询SQL设计 +-- ============================================= + +-- 1. 退库原因分析报表表 +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) +); + +-- 2. 库存变动趋势分析报表表 +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) +); + +-- 3. 安全库存预警报表表 +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) +); + +-- 4. 呆滞料库存报表表 +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) +);-- 5. + 库存差异报表表 +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) +); + +-- 6. 库存周转报表表 +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) +); + +-- ============================================= +-- 报表数据汇总表(可选,用于快速查询) +-- ============================================= +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) +); \ No newline at end of file From f7d3253c9bcf463c0ea7ec5163c93a16c51299e2 Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Mon, 29 Sep 2025 16:10:30 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(wms):=20WMS=E6=8A=A5=E8=A1=A8=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=95=B0=E6=8D=AE=E5=BA=93=E8=A1=A8=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=AE=8C=E6=95=B4=E4=B8=AD=E6=96=87=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建退库原因分析报表表(wms_report_return_reason_analysis) - 创建库存变动趋势分析报表表(wms_report_inventory_trend_analysis) - 创建安全库存预警报表表(wms_report_safety_stock_alert) - 创建呆滞料库存报表表(wms_report_stagnant_inventory)- 创建库存差异报表表(wms_report_inventory_difference) - 创建库存周转报表表(wms_report_inventory_turnover) - 创建报表数据汇总表(wms_report_summary)-为所有表添加完整的中文表注释和字段注释- 添加表存在检查和删除逻辑确保脚本可重复执行 - 添加创建完成提示信息便于部署确认 --- ruoyi-modules/hwmom-wms/wms_report_tables.sql | 559 +++++++++++++----- 1 file changed, 427 insertions(+), 132 deletions(-) diff --git a/ruoyi-modules/hwmom-wms/wms_report_tables.sql b/ruoyi-modules/hwmom-wms/wms_report_tables.sql index 414c2d2e..abbc90ad 100644 --- a/ruoyi-modules/hwmom-wms/wms_report_tables.sql +++ b/ruoyi-modules/hwmom-wms/wms_report_tables.sql @@ -1,159 +1,454 @@ -- ============================================= -- WMS报表数据库表设计 --- 基于WmsReportMapper中的查询SQL设计 +-- 基于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) + 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) + 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) + 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) -);-- 5. - 库存差异报表表 -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) + 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) + 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) -); \ No newline at end of file + 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中查看 ===' From f6dcf8afa978de03152e2a1f74a4eeab08e2bc48 Mon Sep 17 00:00:00 2001 From: xs Date: Tue, 30 Sep 2025 17:25:18 +0800 Subject: [PATCH 4/4] =?UTF-8?q?1.5.8=E5=90=8E=E7=AB=AF=20AI=20Token?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E8=AE=B0=E5=BD=95=E4=BF=9D=E5=AD=98=E5=92=8C?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AiChatMessageController.java | 17 ++ .../ai/controller/AiTokenUsageController.java | 117 +++++++++ .../ai/domain/AiChatMessageDetail.java | 12 +- .../org/dromara/ai/domain/AiTokenUsage.java | 61 +++++ .../ai/domain/bo/AiChatMessageDetailBo.java | 11 + .../dromara/ai/domain/bo/AiTokenUsageBo.java | 73 ++++++ .../ai/domain/vo/AiChatMessageDetailVo.java | 22 ++ .../dromara/ai/domain/vo/AiTokenUsageVo.java | 83 +++++++ .../ai/mapper/AiChatMessageDetailMapper.java | 13 + .../dromara/ai/mapper/AiTokenUsageMapper.java | 37 +++ .../dromara/ai/process/dto/AIResponse.java | 34 +-- .../dromara/ai/process/dto/TokenUsage.java | 46 ++++ .../IUnifiedAIProviderProcessor.java | 51 +++- .../impl/BaseAIProviderProcessor.java | 234 +++++++++++++++++- .../processor/impl/DeepSeekProcessor.java | 185 +++++++++----- .../processor/impl/TencentLkeProcessor.java | 87 +++++-- .../impl/TongYiQianWenProcessor.java | 36 ++- .../processor/utils/ProcessorUtils.java | 25 ++ .../service/IAiChatMessageDetailService.java | 8 + .../ai/service/IAiTokenUsageService.java | 78 ++++++ .../service/impl/AIAssistantServiceImpl.java | 39 ++- .../impl/AiChatMessageDetailServiceImpl.java | 23 ++ .../impl/AiKnowledgeBaseServiceImpl.java | 26 +- .../service/impl/AiTokenUsageServiceImpl.java | 162 ++++++++++++ .../mapper/ai/AiChatMessageDetailMapper.xml | 23 ++ .../mapper/ai/AiTokenUsageMapper.xml | 24 ++ 26 files changed, 1412 insertions(+), 115 deletions(-) create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiTokenUsageController.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiTokenUsage.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiTokenUsageBo.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiTokenUsageVo.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiTokenUsageMapper.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/TokenUsage.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/utils/ProcessorUtils.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiTokenUsageService.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiTokenUsageServiceImpl.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiTokenUsageMapper.xml diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiChatMessageController.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiChatMessageController.java index 756253b4..786eb501 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiChatMessageController.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiChatMessageController.java @@ -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 list = aiChatMessageService.queryList(bo); return R.ok(list); } + + + + /** + * 查询使用详情列表 + */ +// @SaCheckPermission("ai:aiChatMessage:list") + @GetMapping("/listDetail") + public TableDataInfo listDetail(AiChatMessageDetailBo bo, PageQuery pageQuery) { + return aiChatMessageDetailService.queryPageJoinList(bo, pageQuery); + } + } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiTokenUsageController.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiTokenUsageController.java new file mode 100644 index 00000000..c7046216 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiTokenUsageController.java @@ -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 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 list = aiTokenUsageService.queryList(bo); + ExcelUtil.exportExcel(list, "用户token使用详情", AiTokenUsageVo.class, response); + } + + /** + * 获取用户token使用详情详细信息 + * + * @param tokenUsageId 主键 + */ + @SaCheckPermission("ai:tokenUsage:query") + @GetMapping("/{tokenUsageId}") + public R 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 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 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 remove(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] tokenUsageIds) { + return toAjax(aiTokenUsageService.deleteWithValidByIds(List.of(tokenUsageIds), true)); + } + + + /** + * 下拉框查询用户token使用详情列表 + */ + + @GetMapping("/getAiTokenUsageList") + public R> getAiTokenUsageList(AiTokenUsageBo bo) { + List list = aiTokenUsageService.queryList(bo); + return R.ok(list); + } +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiChatMessageDetail.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiChatMessageDetail.java index d50c9562..a1ac11e7 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiChatMessageDetail.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiChatMessageDetail.java @@ -37,6 +37,12 @@ public class AiChatMessageDetail extends TenantEntity { */ private String sessionId; + /** + * 聊天类型(1AI提问,2知识库提问,3生成SQL,4AI智能填报,5获取向量) + */ + private String messageDetailType; + + /** * 提问内容 */ @@ -72,6 +78,11 @@ public class AiChatMessageDetail extends TenantEntity { */ private Long knowledgeBaseId; + /** + * 知识库内容ID,获取向量时使用,关联ai_knowledge_content + */ + private Long knowledgeContentId; + /** * 是否携带历史内容(1是,0否) */ @@ -82,5 +93,4 @@ public class AiChatMessageDetail extends TenantEntity { */ private String completeFlag; - } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiTokenUsage.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiTokenUsage.java new file mode 100644 index 00000000..911ea876 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/AiTokenUsage.java @@ -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; + + /** + * 模型ID,关联ai_model + */ + private Long modelId; + + /** + * 提问token数量 + */ + private Long promptToken; + + /** + * 回复token数量 + */ + private Long completionToken; + + /** + * 累计使用token + */ + private Long totalToken; + + +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiChatMessageDetailBo.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiChatMessageDetailBo.java index 13cb0677..6377094e 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiChatMessageDetailBo.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiChatMessageDetailBo.java @@ -38,6 +38,12 @@ public class AiChatMessageDetailBo extends BaseEntity { @NotNull(message = "会话ID不能为空", groups = { AddGroup.class, EditGroup.class }) private String sessionId; + /** + * 聊天类型(1AI提问,2知识库提问,3生成SQL,4AI智能填报,5获取向量) + */ + @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; + /** * 是否携带历史内容(1是,0否) */ diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiTokenUsageBo.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiTokenUsageBo.java new file mode 100644 index 00000000..f39c6af8 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/bo/AiTokenUsageBo.java @@ -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; + + /** + * 模型ID,关联ai_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; + + + +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiChatMessageDetailVo.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiChatMessageDetailVo.java index 19d28e43..679b4f3f 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiChatMessageDetailVo.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiChatMessageDetailVo.java @@ -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; + /** + * 聊天类型(1AI提问,2知识库提问,3生成SQL,4AI智能填报,5获取向量) + */ + 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; + /** * 是否携带历史内容(1是,0否) */ @@ -101,5 +114,14 @@ public class AiChatMessageDetailVo implements Serializable { @ExcelProperty(value = "完整标识(1是,0否),代表回复信息是否完整回复,中间可以暂停继续。") private String completeFlag; + /** + * 用户昵称 + */ + private String nickName; + + /** + * AI模型名称 + */ + private String modelName; } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiTokenUsageVo.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiTokenUsageVo.java new file mode 100644 index 00000000..592f75e9 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/vo/AiTokenUsageVo.java @@ -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; + + /** + * 模型ID,关联ai_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; +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiChatMessageDetailMapper.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiChatMessageDetailMapper.java index 9e4118bd..469553ea 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiChatMessageDetailMapper.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiChatMessageDetailMapper.java @@ -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 { + @DataPermission({ + @DataColumn(key = "deptName", value = "acmd.create_dept"), + @DataColumn(key = "userName", value = "acmd.user_id") + }) + Page selectAiChatMessageDetailJoinList(@Param("page") Page page, + @Param(Constants.WRAPPER) Wrapper queryWrapper); + } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiTokenUsageMapper.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiTokenUsageMapper.java new file mode 100644 index 00000000..c4c4ff6d --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/AiTokenUsageMapper.java @@ -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 { + + + /** + * 获取用户token使用记录,join sys_user and ai_model + * @param page + * @param queryWrapper + * @return List + */ + @DataPermission({ + @DataColumn(key = "deptName", value = "atu.create_dept"), + @DataColumn(key = "userName", value = "atu.user_id") + }) + Page selectAiTokenUsageJoinList(@Param("page") Page page, + @Param(Constants.WRAPPER) Wrapper queryWrapper); +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/AIResponse.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/AIResponse.java index a2c01649..4f514859 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/AIResponse.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/AIResponse.java @@ -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; +// } +// } } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/TokenUsage.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/TokenUsage.java new file mode 100644 index 00000000..3d7df560 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/TokenUsage.java @@ -0,0 +1,46 @@ +package org.dromara.ai.process.dto; + +/** + * @Author xins + * @Date 2025/9/25 16:28 + * @Description: 调用AI模型使用的Token情况dto + */ +// 如果还没有这个类,需要创建 +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; + } +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/IUnifiedAIProviderProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/IUnifiedAIProviderProcessor.java index ecefa8da..c771c781 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/IUnifiedAIProviderProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/IUnifiedAIProviderProcessor.java @@ -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 chat(AIRequest request); + /** + * 发送聊天请求(流式) + * @param request 聊天请求参数 + * @return 流式内容Flux + */ + Flux chatStreamContent(AIRequest request, LoginUser loginUser); + /** * 发送聊天请求(流式) * @param request 聊天请求参数 @@ -42,19 +51,53 @@ public interface IUnifiedAIProviderProcessor { /** - * 获取单个文本的向量表示 - * @param text 输入文本 + * 获取单个文本的向量数据 + * @param aiRequest * @return 向量表示列表(Double类型) * @throws RuntimeException 如果向量化过程中发生错误 */ public List getEmbedding(AIRequest aiRequest); /** - * 获取多个文本的向量表示 - * @param texts 输入文本数组 + * 获取单个文本的向量数据 + * @param embeddingResponse + * @return + */ + public List getEmbedding(GetEmbeddingResponse embeddingResponse); + + + /** + * 获取多个文本的向量数据和Usage + * @param aiRequest + * @return 向量表示列表(Double类型) + * @throws RuntimeException 如果向量化过程中发生错误 + */ + public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest); + + /** + * 获取多个文本的向量数据 + * @param aiRequest * @return 向量表示列表的列表(每个文本对应一个Double列表) * @throws RuntimeException 如果向量化过程中发生错误 */ public List> 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); } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/BaseAIProviderProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/BaseAIProviderProcessor.java index f1c538e4..9fb73f4f 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/BaseAIProviderProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/BaseAIProviderProcessor.java @@ -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 executeStreamRequest(String url, String requestBody, String apiKey) { + protected Flux 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()))); } + + + + + + /** + * 处理流式响应的JSON块,返回包含内容和token信息的对象 + */ + protected StreamChunkResult parseStreamChunk(String jsonChunk) { + try { + return extractContentAndTokensFromStreamJson(jsonChunk); + } catch (Exception e) { + return new StreamChunkResult(null, null); + } + } + + /** + * 从流式JSON中提取内容和token信息(由子类实现) + */ + protected abstract StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception; + + + /** + * 执行流式HTTP请求,返回包含内容和token信息的Flux + */ + protected Flux 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() + .eq(AiChatMessage::getSessionId, sessionId)); + List 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 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 diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/DeepSeekProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/DeepSeekProcessor.java index d6a78a20..4d1616f6 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/DeepSeekProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/DeepSeekProcessor.java @@ -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 chatTest(AIRequest request) { AIMessage aiMessage = new AIMessage(); @@ -101,7 +98,7 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { } @Override - public Flux chatStream(AIRequest request, LoginUser loginUser) { + public Flux 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 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 getEmbedding(GetEmbeddingResponse embeddingResponse){ + return List.of(); + } + + @Override + public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest) { + return null; + } + @Override public List> 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() - .eq(AiChatMessage::getSessionId, sessionId)); - List 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()); - - } } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TencentLkeProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TencentLkeProcessor.java index 8b9de4a8..b51ca894 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TencentLkeProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TencentLkeProcessor.java @@ -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 getEmbedding(GetEmbeddingResponse embeddingResponse) { + EmbeddingObject[] embeddingObjects = embeddingResponse.getData(); + // 转换结果格式 + List> 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> 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> 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; } - /** - * 将Float数组转换为Double列表 - * @param floatArray Float数组 - * @return Double列表 - */ - private List convertFloatArrayToDoubleList(Float[] floatArray) { - List doubleList = new ArrayList<>(floatArray.length); - for (Float f : floatArray) { - doubleList.add(f.doubleValue()); - } - return doubleList; - } - @Override public Mono chatTest(AIRequest request) { @@ -153,9 +191,28 @@ public class TencentLkeProcessor implements IUnifiedAIProviderProcessor { return null; } + @Override + public Flux chatStreamContent(AIRequest request, LoginUser loginUser) { + return null; + } + @Override public Flux 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; + } } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TongYiQianWenProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TongYiQianWenProcessor.java index d0363a3c..bbb28706 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TongYiQianWenProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TongYiQianWenProcessor.java @@ -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 chatStream(AIRequest request, LoginUser loginUser) { + public Flux 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 getEmbedding(GetEmbeddingResponse embeddingResponse){ + return List.of(); + } + + @Override + public GetEmbeddingResponse getEmbeddingResponses(AIRequest aiRequest) { + return null; + } + @Override public List> getEmbeddings(AIRequest aiRequest) { return List.of(); @@ -247,4 +262,13 @@ public class TongYiQianWenProcessor extends BaseAIProviderProcessor { } + @Override + public Flux chatStream(AIRequest request, LoginUser loginUser) { + return null; + } + + @Override + protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception { + return null; + } } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/utils/ProcessorUtils.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/utils/ProcessorUtils.java new file mode 100644 index 00000000..1e170565 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/utils/ProcessorUtils.java @@ -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 { + + /** + * 将Float数组转换为Double列表 + * @param floatArray Float数组 + * @return Double列表 + */ + public static List convertFloatArrayToDoubleList(Float[] floatArray) { + List doubleList = new ArrayList<>(floatArray.length); + for (Float f : floatArray) { + doubleList.add(f.doubleValue()); + } + return doubleList; + } +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiChatMessageDetailService.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiChatMessageDetailService.java index ca0bc660..460951c3 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiChatMessageDetailService.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiChatMessageDetailService.java @@ -74,4 +74,12 @@ public interface IAiChatMessageDetailService { * @return LIST */ public List getAIChatMessages(String sessionId); + + /** + * 分页获取会话详情信息,join user and model + * @param bo + * @param pageQuery + * @return + */ + public TableDataInfo queryPageJoinList(AiChatMessageDetailBo bo, PageQuery pageQuery); } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiTokenUsageService.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiTokenUsageService.java new file mode 100644 index 00000000..57942f75 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiTokenUsageService.java @@ -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 queryPageList(AiTokenUsageBo bo, PageQuery pageQuery); + + /** + * 查询符合条件的用户token使用详情列表 + * + * @param bo 查询条件 + * @return 用户token使用详情列表 + */ + List 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 ids, Boolean isValid); + + /** + * 分页查询用户token使用详情列表,Join sys_user and ai_model + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 用户token使用详情分页列表 + */ + public TableDataInfo queryPageJoinList(AiTokenUsageBo bo, PageQuery pageQuery); +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIAssistantServiceImpl.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIAssistantServiceImpl.java index 29270da2..4e852ad3 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIAssistantServiceImpl.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIAssistantServiceImpl.java @@ -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 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 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 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 conditions = JSON.parseArray(relateFilterCondition, AiTableQueryCondition.class); wrapper.setAiTableQueryConditions(conditions); @@ -470,7 +493,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService { } - List> tableData = sQLServerDatabaseMetaMapper.dynamicSelect(relateTableName, relateTableFieldList, conditionMap, wrapper,"", ""); + List> tableData = sQLServerDatabaseMetaMapper.dynamicSelect(relateTableName, relateTableFieldList, conditionMap, wrapper, "", ""); JSONArray relateTableDataArr = new JSONArray(); for (Map tableD : tableData) { // JSONObject tableDataJson = new JSONObject(); diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiChatMessageDetailServiceImpl.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiChatMessageDetailServiceImpl.java index 6ee47888..f6cca9e9 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiChatMessageDetailServiceImpl.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiChatMessageDetailServiceImpl.java @@ -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 queryPageJoinList(AiChatMessageDetailBo bo, PageQuery pageQuery) { + QueryWrapper 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 page = baseMapper.selectAiChatMessageDetailJoinList(pageQuery.build(), wrapper); + return TableDataInfo.build(page); + } } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiKnowledgeBaseServiceImpl.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiKnowledgeBaseServiceImpl.java index 286fb3d9..7ce190e8 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiKnowledgeBaseServiceImpl.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiKnowledgeBaseServiceImpl.java @@ -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> 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> 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)); diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiTokenUsageServiceImpl.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiTokenUsageServiceImpl.java new file mode 100644 index 00000000..237843df --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiTokenUsageServiceImpl.java @@ -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 queryPageList(AiTokenUsageBo bo, PageQuery pageQuery) { + MPJLambdaWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询符合条件的用户token使用详情列表 + * + * @param bo 查询条件 + * @return 用户token使用详情列表 + */ + @Override + public List queryList(AiTokenUsageBo bo) { + MPJLambdaWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private MPJLambdaWrapper buildQueryWrapper(AiTokenUsageBo bo) { + Map params = bo.getParams(); + MPJLambdaWrapper 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 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 queryPageJoinList(AiTokenUsageBo bo, PageQuery pageQuery) { + QueryWrapper 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 page = baseMapper.selectAiTokenUsageJoinList(pageQuery.build(), wrapper); + return TableDataInfo.build(page); + } +} diff --git a/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiChatMessageDetailMapper.xml b/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiChatMessageDetailMapper.xml index 9b97b2a1..b1ebea3f 100644 --- a/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiChatMessageDetailMapper.xml +++ b/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiChatMessageDetailMapper.xml @@ -4,4 +4,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> + + + + diff --git a/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiTokenUsageMapper.xml b/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiTokenUsageMapper.xml new file mode 100644 index 00000000..90d8c808 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/AiTokenUsageMapper.xml @@ -0,0 +1,24 @@ + + + + + + + +