This commit is contained in:
2026-05-29 15:49:02 +08:00
139 changed files with 21228 additions and 646 deletions

18
.vscode/launch.json vendored
View File

@@ -1,6 +1,24 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "jeecgboot-vue3: 调试前端 (Chrome)",
"url": "http://localhost:3100",
"webRoot": "${workspaceFolder}/jeecgboot-vue3",
"preLaunchTask": "jeecgboot-vue3: dev"
},
{
"type": "node",
"request": "launch",
"name": "jeecgboot-vue3: dev",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["run", "dev"],
"cwd": "${workspaceFolder}/jeecgboot-vue3",
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**", "**/node_modules/**"]
},
{
"type": "java",
"name": "JeecgSystemApplication (单体)",

View File

@@ -14,6 +14,10 @@
"default": true
}
],
"java.maven.downloadSources": true,
"java.eclipse.downloadSources": true,
"java.project.importOnFirstTimeStartup": "automatic",
"java.configuration.checkProjectSettingsExclusions": false,
"java.import.exclusions": [
"**/jeecg-server-cloud/**",
"**/jeecg-boot-platform/**",

18
.vscode/tasks.json vendored
View File

@@ -54,6 +54,24 @@
"dependsOn": "YY.Admin: build",
"problemMatcher": []
},
{
"label": "jeecgboot-vue3: dev",
"type": "shell",
"command": "pnpm run dev",
"options": {
"cwd": "${workspaceFolder}/jeecgboot-vue3"
},
"isBackground": true,
"problemMatcher": {
"owner": "vite",
"pattern": { "regexp": "^$" },
"background": {
"activeOnStart": true,
"beginsPattern": ".",
"endsPattern": "(Local:|ready in|http://localhost)"
}
}
},
{
"label": "YY.Admin: run (script)",
"type": "process",

View File

@@ -14,6 +14,10 @@
"default": true
}
],
"java.maven.downloadSources": true,
"java.eclipse.downloadSources": true,
"java.project.importOnFirstTimeStartup": "automatic",
"java.configuration.checkProjectSettingsExclusions": false,
"java.import.exclusions": [
"**/jeecg-server-cloud/**",
"**/jeecg-boot-platform/**",

View File

@@ -0,0 +1,151 @@
-- 胶料快检记录主子表字典 + 建表 + 菜单质量管理下+ 按钮 + 胶料信息检验按钮 + 租户 admin 授权
-- 权限前缀mes:mes_xsl_rubber_quick_test_record:*
-- 菜单 ID 1860000000000000192
-- SET @mes_tenant_id多租户 admin 授权目标租户
SET NAMES utf8mb4;
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检记录检验结果', 'xslmes_rubber_quick_test_record_result', '1合格0不合格', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_record_result' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '合格', '1', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_record_result' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '1');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '不合格', '0', 2, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_record_result' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '0');
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检班次', 'xslmes_rubber_quick_test_work_shift', '班次', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_work_shift' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, v.txt, v.val, v.ord, 1, 'admin', NOW()
FROM `sys_dict` d
CROSS JOIN (
SELECT '早班' AS txt, '1' AS val, 1 AS ord UNION ALL
SELECT '中班', '2', 2 UNION ALL
SELECT '晚班', '3', 3
) v
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_work_shift'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = v.val);
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检班组', 'xslmes_rubber_quick_test_work_team', '班组', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_work_team' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, v.txt, v.val, v.ord, 1, 'admin', NOW()
FROM `sys_dict` d
CROSS JOIN (
SELECT '甲班' AS txt, '1' AS val, 1 AS ord UNION ALL
SELECT '乙班', '2', 2 UNION ALL
SELECT '丙班', '3', 3
) v
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_work_team'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = v.val);
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_record` (
`id` varchar(32) NOT NULL COMMENT '主键',
`record_no` varchar(32) DEFAULT NULL COMMENT '单号JL+日期+4位流水如JL202605280001',
`rubber_material_id` varchar(32) DEFAULT NULL COMMENT '胶料 mes_material.id',
`rubber_material_name` varchar(128) DEFAULT NULL COMMENT '胶料名称冗余',
`std_id` varchar(32) DEFAULT NULL COMMENT '引用的实验标准 mes_xsl_rubber_quick_test_std.id',
`prod_equipment_ledger_id` varchar(32) DEFAULT NULL COMMENT '生产机台 mes_xsl_equipment_ledger.id',
`prod_equipment_name` varchar(128) DEFAULT NULL COMMENT '生产机台名称冗余',
`production_date` date DEFAULT NULL COMMENT '生产日期',
`train_no` varchar(64) DEFAULT NULL COMMENT '车次编号',
`work_shift` varchar(8) DEFAULT NULL COMMENT '班次字典xslmes_rubber_quick_test_work_shift',
`work_team` varchar(8) DEFAULT NULL COMMENT '班组字典xslmes_rubber_quick_test_work_team',
`inspect_times` int DEFAULT NULL COMMENT '检验次数',
`inspect_time` datetime DEFAULT NULL COMMENT '检验时间',
`inspector_user_id` varchar(32) DEFAULT NULL COMMENT '检验人用户ID',
`inspector_username` varchar(64) DEFAULT NULL COMMENT '检验人账号冗余',
`inspector_realname` varchar(64) DEFAULT NULL COMMENT '检验人姓名冗余',
`quick_test_type_id` varchar(32) DEFAULT NULL COMMENT '检验类型 mes_xsl_rubber_quick_test_type.id',
`quick_test_type_name` varchar(128) DEFAULT NULL COMMENT '检验类型名称冗余',
`inspect_result` varchar(2) DEFAULT NULL COMMENT '检验结果字典xslmes_rubber_quick_test_record_result1合格0不合格',
`production_plan_no` varchar(100) DEFAULT NULL COMMENT '生产计划号',
`inspect_equipment_ledger_id` varchar(32) DEFAULT NULL COMMENT '检验机台 mes_xsl_equipment_ledger.id',
`inspect_equipment_name` varchar(128) DEFAULT NULL COMMENT '检验机台名称冗余',
`rubber_card_no` varchar(100) DEFAULT NULL COMMENT '胶料卡片号',
`rubber_batch_no` varchar(100) DEFAULT NULL COMMENT '胶料批次',
`tenant_id` int DEFAULT NULL COMMENT '租户',
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '部门',
`create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(32) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int DEFAULT '0' COMMENT '删除标记0正常1删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_mrqtr_record_no` (`record_no`),
KEY `idx_mrqtr_material` (`rubber_material_id`),
KEY `idx_mrqtr_std` (`std_id`),
KEY `idx_mrqtr_tenant` (`tenant_id`),
KEY `idx_mrqtr_inspect_time` (`inspect_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES胶料快检记录';
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_record_line` (
`id` varchar(32) NOT NULL COMMENT '主键',
`record_id` varchar(32) NOT NULL COMMENT '主表 mes_xsl_rubber_quick_test_record.id',
`data_point_id` varchar(32) DEFAULT NULL COMMENT '数据点 mes_xsl_rubber_quick_test_data_point.id',
`inspect_item` varchar(128) DEFAULT NULL COMMENT '检验项目数据点名称只读带出',
`lower_limit` decimal(18,6) DEFAULT NULL COMMENT '检验下限只读带出',
`inspect_value` decimal(18,6) DEFAULT NULL COMMENT '检验值',
`upper_limit` decimal(18,6) DEFAULT NULL COMMENT '检验上限只读带出',
`sort_no` int DEFAULT NULL COMMENT '排序号',
`create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(32) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_mrqtrl_record` (`record_id`),
UNIQUE KEY `uk_mrqtrl_record_point` (`record_id`, `data_point_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES胶料快检记录明细';
SET @mes_tenant_id = 1002;
SET @mes_quality_pid = IFNULL(
(SELECT `id` FROM `sys_permission` WHERE `del_flag` = 0 AND `menu_type` = 0 AND `name` = '质量管理' LIMIT 1),
'1860000000000000162'
);
INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `component_name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `keep_alive`, `internal_or_external`, `create_by`, `create_time`)
VALUES ('1860000000000000192', @mes_quality_pid, '胶料快检记录', '/xslmes/mesXslRubberQuickTestRecord', 'xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecordList', 'MesXslRubberQuickTestRecordList', 1, NULL, '1', 5, 1, 0, 0, '1', 0, 1, 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE
`parent_id` = VALUES(`parent_id`), `name` = VALUES(`name`), `url` = VALUES(`url`), `component` = VALUES(`component`),
`component_name` = VALUES(`component_name`), `sort_no` = VALUES(`sort_no`), `is_leaf` = VALUES(`is_leaf`), `keep_alive` = VALUES(`keep_alive`);
UPDATE `sys_permission` SET `icon` = 'ant-design:file-search-outlined' WHERE `id` = '1860000000000000192' AND `del_flag` = 0;
INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `status`, `del_flag`, `create_by`, `create_time`) VALUES
('1860000000000000193', '1860000000000000192', '新增', 2, 'mes:mes_xsl_rubber_quick_test_record:add', '1', '1', 0, 'admin', NOW()),
('1860000000000000194', '1860000000000000192', '编辑', 2, 'mes:mes_xsl_rubber_quick_test_record:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000000195', '1860000000000000192', '删除', 2, 'mes:mes_xsl_rubber_quick_test_record:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000000196', '1860000000000000192', '批量删除', 2, 'mes:mes_xsl_rubber_quick_test_record:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000000197', '1860000000000000192', '导出', 2, 'mes:mes_xsl_rubber_quick_test_record:exportXls', '1', '1', 0, 'admin', NOW()),
('1860000000000000198', '1860000000000000192', '导入', 2, 'mes:mes_xsl_rubber_quick_test_record:importExcel', '1', '1', 0, 'admin', NOW()),
('1860000000000000199', '1860000000000000192', '从胶料生成', 2, 'mes:mes_xsl_rubber_quick_test_record:batchFromMaterial', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`), `name` = VALUES(`name`);
INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `status`, `del_flag`, `create_by`, `create_time`) VALUES
('1860000000000000200', '1860000000000000011', '胶料快检', 2, 'mes:mes_material:rubberQuickTestInspect', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`), `name` = VALUES(`name`);
INSERT INTO `sys_role_permission`(`id`, `role_id`, `permission_id`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NOW(), '127.0.0.1'
FROM sys_role r
CROSS JOIN sys_permission p
WHERE r.tenant_id = @mes_tenant_id
AND r.role_code = 'admin'
AND p.id IN (
'1860000000000000192',
'1860000000000000193', '1860000000000000194', '1860000000000000195', '1860000000000000196',
'1860000000000000197', '1860000000000000198', '1860000000000000199',
'1860000000000000200'
)
AND NOT EXISTS (
SELECT 1 FROM sys_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
);

View File

@@ -22,6 +22,7 @@ import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.*;
import java.sql.Date;
import java.util.*;
@@ -1046,7 +1047,7 @@ public class oConvertUtils {
BigDecimal bigDecimal = new BigDecimal(uploadCount);
//换算成MB
BigDecimal divide = bigDecimal.divide(new BigDecimal(1048576));
count = divide.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
count = divide.setScale(2, RoundingMode.HALF_UP).doubleValue();
return count;
}
return count;

View File

@@ -414,3 +414,55 @@ jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/ser
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerPsCompileServiceImpl.java
jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/MesXslMixingSpec.data.ts
jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/MesXslMixingSpecList.vue
-- author:cursor---date:20260526--for: 【配方日志查询】配合示方/混炼示方修改日志记录与查询接口 -----------
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_107__mes_xsl_formula_spec_edit_log.sql
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslFormulaSpecEditLog.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslFormulaSpecEditChangeItemVO.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslFormulaSpecEditLogDetailVO.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslFormulaSpecEditLogMapper.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/common/MesXslFormulaSpecEditLogDiffUtil.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslFormulaSpecEditLogService.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslFormulaSpecEditLogServiceImpl.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslFormulaSpecEditLogController.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslFormulaSpecServiceImpl.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java
-- author:cursor---date:20260526--for: 【配方日志查询】前端列表页与修改对比弹窗 -----------
jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpecEditLog/MesXslFormulaSpecEditLog.api.ts
jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpecEditLog/MesXslFormulaSpecEditLog.data.ts
jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpecEditLog/MesXslFormulaSpecEditLogList.vue
jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpecEditLog/components/MesXslFormulaSpecEditLogCompareModal.vue
-- author:cursor---date:20260526--for: 【配方日志查询】明细对比展示逐行逐字段变更内容 -----------
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/common/MesXslFormulaSpecEditLogDiffUtil.java
jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpecEditLog/MesXslFormulaSpecEditLog.data.ts
-- author:jiangxh---date:20260525--for: 【MES】胶料快检记录主子表、质量管理菜单、胶料信息批量检验生成 ---
jeecg-boot/db/mes-xsl-rubber-quick-test-record-menu-permission.sql
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_108__mes_xsl_rubber_quick_test_record.sql
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecord.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecordLine.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslRubberQuickTestRecordPage.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslRubberQuickTestRecordBatchFromMaterialVO.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRubberQuickTestRecordMapper.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRubberQuickTestRecordLineMapper.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestRecordService.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestRecordServiceImpl.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestRecordController.java
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecordList.vue
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecord.data.ts
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecord.api.ts
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/components/MesXslRubberQuickTestRecordModal.vue
jeecgboot-vue3/src/views/mes/material/MesMaterialList.vue
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_109__mes_xsl_rubber_quick_test_record_no.sql
-- author:jiangxh---date:20260528--for: 【MES】胶料快检记录单号、无弹窗直接生成、列表去掉新增、明细不可增删 ---
jeecg-boot/db/mes-xsl-rubber-quick-test-record-menu-permission.sql
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecord.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestRecordService.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestRecordServiceImpl.java
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecordList.vue
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecord.data.ts
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/components/MesXslRubberQuickTestRecordModal.vue
jeecgboot-vue3/src/views/mes/material/MesMaterialList.vue

View File

@@ -0,0 +1,546 @@
package org.jeecg.modules.xslmes.common;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.xslmes.entity.MesXslFormulaSpec;
import org.jeecg.modules.xslmes.entity.MesXslFormulaSpecLine;
import org.jeecg.modules.xslmes.vo.MesXslFormulaSpecEditChangeItemVO;
import org.jeecg.modules.xslmes.vo.MesXslMixingSpecPage;
/**
* 配方示方修改日志:快照序列化与差异对比
*/
public final class MesXslFormulaSpecEditLogDiffUtil {
public static final String SECTION_MAIN = "main";
public static final String SECTION_LINE = "line";
public static final String SECTION_MATERIAL = "material";
public static final String SECTION_STEP = "step";
public static final String SECTION_DOWN_STEP = "downStep";
public static final String SECTION_TCU = "tcu";
private static final Set<String> IGNORE_FIELDS = Set.of(
"id",
"createBy",
"createTime",
"updateBy",
"updateTime",
"delFlag",
"tenantId",
"sysOrgCode",
"createBy_dictText",
"lineList",
"materialList",
"stepList",
"downStepList",
"tcuList");
private static final Map<String, String> FORMULA_MAIN_LABELS = buildFormulaMainLabels();
private static final Map<String, String> MIXING_MAIN_LABELS = buildMixingMainLabels();
private static final Map<String, String> SECTION_LABELS = Map.of(
SECTION_MAIN, "主表",
SECTION_LINE, "配合明细",
SECTION_MATERIAL, "橡胶及配合剂",
SECTION_STEP, "混合步骤",
SECTION_DOWN_STEP, "下密炼机条件",
SECTION_TCU, "TCU温度条件");
private static final Map<String, Map<String, String>> SECTION_FIELD_LABELS = buildSectionFieldLabels();
private static final Map<String, String[]> SECTION_SUMMARY_FIELDS = buildSectionSummaryFields();
private MesXslFormulaSpecEditLogDiffUtil() {}
public static String buildFormulaSnapshotJson(MesXslFormulaSpec main, List<MesXslFormulaSpecLine> lineList) {
Map<String, Object> snapshot = new LinkedHashMap<>(2);
snapshot.put(SECTION_MAIN, toJsonObject(main));
snapshot.put(SECTION_LINE, lineList == null ? List.of() : lineList);
return toJson(snapshot);
}
public static String buildMixingSnapshotJson(MesXslMixingSpecPage page) {
if (page == null) {
return null;
}
Map<String, Object> snapshot = new LinkedHashMap<>(5);
snapshot.put(SECTION_MAIN, toJsonObject(page));
snapshot.put(SECTION_MATERIAL, page.getMaterialList() == null ? List.of() : page.getMaterialList());
snapshot.put(SECTION_STEP, page.getStepList() == null ? List.of() : page.getStepList());
snapshot.put(SECTION_DOWN_STEP, page.getDownStepList() == null ? List.of() : page.getDownStepList());
snapshot.put(SECTION_TCU, page.getTcuList() == null ? List.of() : page.getTcuList());
return toJson(snapshot);
}
public static List<MesXslFormulaSpecEditChangeItemVO> compare(
String specType, String beforeSnapshot, String afterSnapshot) {
List<MesXslFormulaSpecEditChangeItemVO> items = new ArrayList<>();
JSONObject before = parseObject(beforeSnapshot);
JSONObject after = parseObject(afterSnapshot);
Map<String, String> mainLabels = SPEC_TYPE_MIXING.equals(specType) ? MIXING_MAIN_LABELS : FORMULA_MAIN_LABELS;
compareMainSection(items, before == null ? null : before.getJSONObject(SECTION_MAIN),
after == null ? null : after.getJSONObject(SECTION_MAIN), mainLabels);
if (SPEC_TYPE_FORMULA.equals(specType)) {
compareChildListSection(items, SECTION_LINE, before, after);
} else {
compareChildListSection(items, SECTION_MATERIAL, before, after);
compareChildListSection(items, SECTION_STEP, before, after);
compareChildListSection(items, SECTION_DOWN_STEP, before, after);
compareChildListSection(items, SECTION_TCU, before, after);
}
return items;
}
public static String buildSummary(String actionType, List<MesXslFormulaSpecEditChangeItemVO> items) {
if ("create".equals(actionType)) {
return "新增示方";
}
if ("delete".equals(actionType)) {
return "删除示方";
}
if (items == null || items.isEmpty()) {
return "内容无变化";
}
Set<String> labels = new LinkedHashSet<>();
for (MesXslFormulaSpecEditChangeItemVO item : items) {
if (SECTION_MAIN.equals(item.getSection()) && StringUtils.isNotBlank(item.getFieldLabel())) {
labels.add(item.getFieldLabel());
} else if (StringUtils.isNotBlank(item.getSectionLabel())) {
labels.add(item.getSectionLabel());
}
}
if (labels.isEmpty()) {
return "内容有变更";
}
String joined = String.join("", labels);
if (joined.length() > 180) {
joined = joined.substring(0, 177) + "...";
}
return "修改:" + joined;
}
private static void compareMainSection(
List<MesXslFormulaSpecEditChangeItemVO> items,
JSONObject before,
JSONObject after,
Map<String, String> labels) {
Set<String> keys = new LinkedHashSet<>();
if (before != null) {
keys.addAll(before.keySet());
}
if (after != null) {
keys.addAll(after.keySet());
}
for (String key : keys) {
if (IGNORE_FIELDS.contains(key)) {
continue;
}
String beforeVal = formatValue(before == null ? null : before.get(key));
String afterVal = formatValue(after == null ? null : after.get(key));
if (Objects.equals(beforeVal, afterVal)) {
continue;
}
MesXslFormulaSpecEditChangeItemVO item = new MesXslFormulaSpecEditChangeItemVO();
item.setSection(SECTION_MAIN);
item.setSectionLabel(SECTION_LABELS.get(SECTION_MAIN));
item.setFieldKey(key);
item.setFieldLabel(labels.getOrDefault(key, key));
item.setBeforeValue(beforeVal);
item.setAfterValue(afterVal);
items.add(item);
}
}
private static void compareChildListSection(
List<MesXslFormulaSpecEditChangeItemVO> items, String section, JSONObject before, JSONObject after) {
List<JSONObject> beforeList = toJsonObjectList(before == null ? null : before.get(section));
List<JSONObject> afterList = toJsonObjectList(after == null ? null : after.get(section));
if (Objects.equals(normalizeListJson(beforeList), normalizeListJson(afterList))) {
return;
}
Map<String, String> fieldLabels = SECTION_FIELD_LABELS.getOrDefault(section, Map.of());
String[] summaryFields = SECTION_SUMMARY_FIELDS.getOrDefault(section, new String[0]);
Map<String, JSONObject> beforeMap = indexRows(beforeList);
Map<String, JSONObject> afterMap = indexRows(afterList);
Set<String> rowKeys = new LinkedHashSet<>();
rowKeys.addAll(beforeMap.keySet());
rowKeys.addAll(afterMap.keySet());
for (String rowKey : rowKeys) {
JSONObject beforeRow = beforeMap.get(rowKey);
JSONObject afterRow = afterMap.get(rowKey);
String rowLabel = formatRowLabel(rowKey);
if (beforeRow == null && afterRow != null) {
addChangeItem(items, section, rowKey + "_add", rowLabel + "(新增)", "",
formatRowSummary(afterRow, fieldLabels, summaryFields));
continue;
}
if (beforeRow != null && afterRow == null) {
addChangeItem(items, section, rowKey + "_del", rowLabel + "(删除)",
formatRowSummary(beforeRow, fieldLabels, summaryFields), "");
continue;
}
compareRowFields(items, section, rowKey, rowLabel, beforeRow, afterRow, fieldLabels);
}
}
private static void compareRowFields(
List<MesXslFormulaSpecEditChangeItemVO> items,
String section,
String rowKey,
String rowLabel,
JSONObject beforeRow,
JSONObject afterRow,
Map<String, String> fieldLabels) {
Set<String> keys = new LinkedHashSet<>();
keys.addAll(beforeRow.keySet());
keys.addAll(afterRow.keySet());
for (String key : keys) {
if (IGNORE_FIELDS.contains(key) || isRowMetaField(key)) {
continue;
}
String beforeVal = formatValue(beforeRow.get(key));
String afterVal = formatValue(afterRow.get(key));
if (Objects.equals(beforeVal, afterVal)) {
continue;
}
addChangeItem(
items,
section,
rowKey + "_" + key,
rowLabel + "·" + fieldLabels.getOrDefault(key, key),
beforeVal,
afterVal);
}
}
private static void addChangeItem(
List<MesXslFormulaSpecEditChangeItemVO> items,
String section,
String fieldKey,
String fieldLabel,
String beforeValue,
String afterValue) {
MesXslFormulaSpecEditChangeItemVO item = new MesXslFormulaSpecEditChangeItemVO();
item.setSection(section);
item.setSectionLabel(SECTION_LABELS.getOrDefault(section, section));
item.setFieldKey(fieldKey);
item.setFieldLabel(fieldLabel);
item.setBeforeValue(beforeValue);
item.setAfterValue(afterValue);
items.add(item);
}
/** 按 sortNo 优先、否则按列表序号建立行索引 */
private static Map<String, JSONObject> indexRows(List<JSONObject> rows) {
Map<String, JSONObject> map = new LinkedHashMap<>();
if (rows == null) {
return map;
}
int index = 0;
for (JSONObject row : rows) {
index++;
if (row == null) {
continue;
}
Integer sortNo = row.getInteger("sortNo");
String key = sortNo != null ? "sort_" + sortNo : "idx_" + index;
map.put(key, row);
}
return map;
}
private static String formatRowLabel(String rowKey) {
if (rowKey.startsWith("sort_")) {
return "" + rowKey.substring(5) + "";
}
if (rowKey.startsWith("idx_")) {
return "" + rowKey.substring(4) + "";
}
return rowKey;
}
private static String formatRowSummary(JSONObject row, Map<String, String> fieldLabels, String[] summaryFields) {
if (row == null || row.isEmpty()) {
return "";
}
List<String> parts = new ArrayList<>();
for (String key : summaryFields) {
String val = formatValue(row.get(key));
if (StringUtils.isBlank(val)) {
continue;
}
parts.add(fieldLabels.getOrDefault(key, key) + "=" + val);
}
if (parts.isEmpty()) {
for (String key : fieldLabels.keySet()) {
String val = formatValue(row.get(key));
if (StringUtils.isBlank(val)) {
continue;
}
parts.add(fieldLabels.get(key) + "=" + val);
if (parts.size() >= 4) {
break;
}
}
}
return String.join("", parts);
}
private static boolean isRowMetaField(String key) {
return "formulaSpecId".equals(key) || "mixingSpecId".equals(key);
}
private static List<JSONObject> toJsonObjectList(Object listObj) {
if (listObj == null) {
return List.of();
}
JSONArray array = listObj instanceof JSONArray jsonArray ? jsonArray : JSON.parseArray(toJson(listObj));
if (array == null || array.isEmpty()) {
return List.of();
}
List<JSONObject> rows = new ArrayList<>(array.size());
for (int i = 0; i < array.size(); i++) {
Object element = array.get(i);
if (element instanceof JSONObject jsonObject) {
rows.add(jsonObject);
} else if (element != null) {
rows.add(JSON.parseObject(toJson(element)));
}
}
return rows;
}
private static Map<String, Map<String, String>> buildSectionFieldLabels() {
Map<String, Map<String, String>> map = new LinkedHashMap<>();
map.put(SECTION_LINE, buildFormulaLineLabels());
map.put(SECTION_MATERIAL, buildMixingMaterialLabels());
map.put(SECTION_STEP, buildMixingStepLabels());
map.put(SECTION_DOWN_STEP, buildMixingStepLabels());
map.put(SECTION_TCU, buildMixingTcuLabels());
return map;
}
private static Map<String, String[]> buildSectionSummaryFields() {
Map<String, String[]> map = new LinkedHashMap<>();
map.put(SECTION_LINE, new String[] {"mixerMaterialName", "mixerMaterialCode", "phr", "step"});
map.put(SECTION_MATERIAL, new String[] {"mixerMaterialName", "unitWeight", "accumWeight", "seqNo"});
map.put(SECTION_STEP, new String[] {"actionName", "actionSec", "tempC", "speedRpm"});
map.put(SECTION_DOWN_STEP, new String[] {"actionName", "actionSec", "tempC", "speedRpm"});
map.put(SECTION_TCU, new String[] {"sectionType", "frontRotorTemp", "rearRotorTemp", "drugWeighPos"});
return map;
}
private static Map<String, String> buildFormulaLineLabels() {
Map<String, String> map = new LinkedHashMap<>();
map.put("sortNo", "序号");
map.put("phr", "PHR");
map.put("mixerMaterialCode", "物料编码");
map.put("mixerMaterialName", "物料名称");
map.put("materialDesc", "物料描述");
map.put("step", "STEP");
map.put("weighMode", "称量方式");
map.put("weightPercent", "重量%");
map.put("volume", "体积");
map.put("remark", "备注");
for (int i = 1; i <= 7; i++) {
map.put("stage" + i, "混合段" + i);
}
return map;
}
private static Map<String, String> buildMixingMaterialLabels() {
Map<String, String> map = new LinkedHashMap<>();
map.put("sortNo", "序号");
map.put("materialMajor", "物料大类");
map.put("materialMinor", "物料小类");
map.put("materialKind", "种类");
map.put("mixerMaterialName", "物料名称");
map.put("mixerMaterialDesc", "物料描述");
map.put("unitWeight", "单重");
map.put("accumWeight", "累计");
map.put("seqNo", "顺序");
return map;
}
private static Map<String, String> buildMixingStepLabels() {
Map<String, String> map = new LinkedHashMap<>();
map.put("sortNo", "序号");
map.put("actionName", "动作");
map.put("actionSec", "时间(秒)");
map.put("protectSec", "保护时间");
map.put("tempC", "温度(℃)");
map.put("powerKw", "功率(Kw)");
map.put("energyKwh", "能量(Kwh)");
map.put("comboMode", "组合");
map.put("speedRpm", "转速(rpm)");
map.put("pressureMpa", "压力(Mpa)");
map.put("boltPercent", "栓(%)");
return map;
}
private static Map<String, String> buildMixingTcuLabels() {
Map<String, String> map = new LinkedHashMap<>();
map.put("sortNo", "序号");
map.put("sectionType", "区分");
map.put("frontRotorTemp", "前转子温度");
map.put("rearRotorTemp", "后转子温度");
map.put("frontChamberTemp", "前混炼室温度");
map.put("rearChamberTemp", "后混炼室温度");
map.put("topPlugTemp", "上下顶栓温度");
map.put("drugWeighPos", "药品称量位置");
return map;
}
private static JSONObject parseObject(String json) {
if (StringUtils.isBlank(json)) {
return null;
}
return JSON.parseObject(json);
}
private static JSONObject toJsonObject(Object obj) {
if (obj == null) {
return new JSONObject();
}
JSONObject jsonObject = JSON.parseObject(toJson(obj));
for (String ignore : IGNORE_FIELDS) {
jsonObject.remove(ignore);
}
return jsonObject;
}
private static String toJson(Object obj) {
return JSON.toJSONStringWithDateFormat(
obj, "yyyy-MM-dd HH:mm:ss", SerializerFeature.WriteMapNullValue);
}
private static String normalizeListJson(Object listObj) {
if (listObj == null) {
return "[]";
}
if (listObj instanceof List<?> list) {
return normalizeListJson(list);
}
return toJson(listObj);
}
private static String normalizeListJson(List<JSONObject> rows) {
if (rows == null || rows.isEmpty()) {
return "[]";
}
List<JSONObject> normalized = new ArrayList<>(rows.size());
for (JSONObject row : rows) {
if (row == null) {
continue;
}
JSONObject copy = new JSONObject(row);
for (String ignore : IGNORE_FIELDS) {
copy.remove(ignore);
}
copy.remove("formulaSpecId");
copy.remove("mixingSpecId");
normalized.add(copy);
}
return toJson(normalized);
}
private static String formatValue(Object value) {
if (value == null) {
return "";
}
if (value instanceof BigDecimal decimal) {
return decimal.stripTrailingZeros().toPlainString();
}
if (value instanceof Date date) {
return toJson(date).replace("\"", "");
}
return String.valueOf(value);
}
private static Map<String, String> buildFormulaMainLabels() {
Map<String, String> map = new LinkedHashMap<>();
map.put("category", "分类");
map.put("specCode", "示方编号");
map.put("rubberCode", "胶料代号");
map.put("rubberMaterialId", "胶料ID");
map.put("basicFormula", "基本配合");
map.put("issueDate", "发行日期");
map.put("purpose", "用途");
map.put("issueNumber", "发行编号");
map.put("mixingStages", "混合段数");
map.put("mixingMachine", "混合机器");
map.put("issueDeptId", "发行部门ID");
map.put("issueDeptName", "发行部门");
map.put("status", "状态");
map.put("aRubberTotalPhr", "A胶合计PHR");
map.put("totalPhr", "总PHR");
map.put("naturalRubber", "天然橡胶");
map.put("syntheticRubber", "合成橡胶");
map.put("totalAmount", "总量");
map.put("weightUnitPrice", "重量单价");
map.put("volumeUnitPrice", "体积单价");
map.put("qRubberSg", "Q胶比重");
map.put("aRubberSg", "A胶比重");
map.put("proofreadBy", "校对人");
map.put("proofreadTime", "校对时间");
map.put("auditBy", "审核人");
map.put("auditTime", "审核时间");
map.put("approveBy", "批准人");
map.put("approveTime", "批准时间");
for (int i = 1; i <= 7; i++) {
map.put("stage" + i + "Total", "混合段" + i + "合计");
}
return map;
}
private static Map<String, String> buildMixingMainLabels() {
Map<String, String> map = new LinkedHashMap<>();
map.put("specName", "规格");
map.put("purpose", "用途");
map.put("machineId", "机台ID");
map.put("machineName", "机台");
map.put("makeDate", "制作日期");
map.put("issueNumber", "发行编号");
map.put("convertFactor", "换算系数");
map.put("fillVolume", "填充体积");
map.put("recycleCarbonSec", "回收炭黑(秒)");
map.put("motherRubberSg", "母胶比重");
map.put("finalRubberSg", "终炼胶比重");
map.put("applyFactory", "适用工厂");
map.put("stageCount", "段数");
map.put("pureMixSec", "纯混炼时间(秒)");
map.put("recycleCarbonKg", "回收炭黑(kg)");
map.put("autoSmallPrintSetting", "自动小票打印设置");
map.put("setTrainCount", "设定车次");
map.put("sideWallWaterTemp", "侧壁水温");
map.put("overtimeDischargeSec", "超时排胶(秒)");
map.put("overtempDischargeSec", "超温排胶(秒)");
map.put("overtempDischargeTemp", "超温排胶温度");
map.put("doorWaterTemp", "门水温");
map.put("rotorWaterTemp", "转子水温");
map.put("maxFeedTemp", "最高投料温度");
map.put("draftBy", "编制人");
map.put("draftTime", "编制时间");
map.put("proofreadBy", "校对人");
map.put("proofreadTime", "校对时间");
map.put("auditBy", "审核人");
map.put("auditTime", "审核时间");
map.put("approveBy", "批准人");
map.put("approveTime", "批准时间");
map.put("changeDate", "变更日期");
return map;
}
public static final String SPEC_TYPE_FORMULA = "formula";
public static final String SPEC_TYPE_MIXING = "mixing";
}

View File

@@ -0,0 +1,67 @@
package org.jeecg.modules.xslmes.common;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.TenantConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
/**
* MES 租户解析工具xslmes 模块内统一入口,避免各 Service 重复依赖 TokenUtils
*/
public final class MesXslTenantUtils {
private MesXslTenantUtils() {}
/**
* 解析租户 ID优先实体上下文其次 TenantContext最后从当前请求头/参数读取
*/
public static Integer resolveTenantId(Integer contextTenantId) {
if (contextTenantId != null) {
return contextTenantId;
}
String tenantId = getTenantIdString();
if (oConvertUtils.isEmpty(tenantId)) {
return null;
}
try {
return Integer.parseInt(tenantId.trim());
} catch (NumberFormatException e) {
return null;
}
}
/** 尽力解析租户 ID无法解析时返回 0 */
public static int resolveTenantIdBestEffort() {
return oConvertUtils.getInt(getTenantIdString(), 0);
}
private static String getTenantIdString() {
String tenantId = TenantContext.getTenant();
if (oConvertUtils.isNotEmpty(tenantId)) {
return tenantId;
}
try {
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
if (request != null) {
tenantId = getTenantIdByRequest(request);
}
} catch (Exception ignored) {
// 非 Web 请求上下文时忽略
}
return tenantId;
}
/** 与 TokenUtils.getTenantIdByRequest 行为一致 */
private static String getTenantIdByRequest(HttpServletRequest request) {
String tenantId = request.getParameter(TenantConstant.TENANT_ID);
if (tenantId == null) {
tenantId = oConvertUtils.getString(request.getHeader(CommonConstant.TENANT_ID));
}
if (oConvertUtils.isNotEmpty(tenantId) && "undefined".equals(tenantId)) {
return null;
}
return tenantId;
}
}

View File

@@ -0,0 +1,63 @@
package org.jeecg.modules.xslmes.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.xslmes.entity.MesXslFormulaSpecEditLog;
import org.jeecg.modules.xslmes.service.IMesXslFormulaSpecEditLogService;
import org.jeecg.modules.xslmes.vo.MesXslFormulaSpecEditLogDetailVO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
* 配方示方修改日志(只读查询)
*/
@Tag(name = "配方日志查询")
@RestController
@RequestMapping("/xslmes/mesXslFormulaSpecEditLog")
@Slf4j
public class MesXslFormulaSpecEditLogController
extends JeecgController<MesXslFormulaSpecEditLog, IMesXslFormulaSpecEditLogService> {
@Operation(summary = "配方日志查询-分页列表")
@RequiresPermissions("xslmes:mes_xsl_formula_spec_edit_log:list")
@GetMapping(value = "/list")
public Result<IPage<MesXslFormulaSpecEditLog>> queryPageList(
MesXslFormulaSpecEditLog entity,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<MesXslFormulaSpecEditLog> qw = QueryGenerator.initQueryWrapper(entity, req.getParameterMap());
qw.orderByDesc("modify_time");
Page<MesXslFormulaSpecEditLog> page = new Page<>(pageNo, pageSize);
return Result.OK(service.page(page, qw));
}
@Operation(summary = "配方日志查询-详情(含对比项)")
@RequiresPermissions("xslmes:mes_xsl_formula_spec_edit_log:list")
@GetMapping(value = "/queryById")
public Result<MesXslFormulaSpecEditLogDetailVO> queryById(@RequestParam(name = "id", required = true) String id) {
MesXslFormulaSpecEditLogDetailVO detail = service.getDetail(id);
if (detail == null) {
return Result.error("未找到对应数据");
}
return Result.OK(detail);
}
@RequiresPermissions("xslmes:mes_xsl_formula_spec_edit_log:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, MesXslFormulaSpecEditLog entity) {
return super.exportXls(request, entity, MesXslFormulaSpecEditLog.class, "配方日志查询");
}
}

View File

@@ -0,0 +1,73 @@
package org.jeecg.modules.xslmes.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.modules.xslmes.service.IMesXslRackTrainCountSettingService;
import org.jeecg.modules.xslmes.vo.MesXslRackTrainCountSettingVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 架子车数设定(数据来源于混炼示方,不建独立业务表)
*/
@Tag(name = "架子车数设定")
@RestController
@RequestMapping("/xslmes/mesXslRackTrainCountSetting")
@Slf4j
public class MesXslRackTrainCountSettingController {
@Autowired
private IMesXslRackTrainCountSettingService rackTrainCountSettingService;
//update-begin---author:cursor ---date:20260528 for【XSLMES-20260528-A01】架子车数设定分页列表-----------
@Operation(summary = "架子车数设定-分页列表查询")
@RequiresPermissions("xslmes:mes_xsl_rack_train_count_setting:list")
@GetMapping(value = "/list")
public Result<IPage<MesXslRackTrainCountSettingVO>> queryPageList(
MesXslRackTrainCountSettingVO query,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
@RequestParam(name = "keyword", required = false) String keyword) {
return Result.OK(rackTrainCountSettingService.queryPage(pageNo, pageSize, query, keyword));
}
//update-end---author:cursor ---date:20260528 for【XSLMES-20260528-A01】架子车数设定分页列表-----------
//update-begin---author:cursor ---date:20260528 for【XSLMES-20260528-A01】架子车数设定详情查询-----------
@Operation(summary = "架子车数设定-通过id查询")
@RequiresPermissions("xslmes:mes_xsl_rack_train_count_setting:list")
@GetMapping(value = "/queryById")
public Result<MesXslRackTrainCountSettingVO> queryById(@RequestParam(name = "id", required = true) String id) {
MesXslRackTrainCountSettingVO detail = rackTrainCountSettingService.queryById(id);
if (detail == null) {
return Result.error("未找到对应数据");
}
return Result.OK(detail);
}
//update-end---author:cursor ---date:20260528 for【XSLMES-20260528-A01】架子车数设定详情查询-----------
//update-begin---author:cursor ---date:20260528 for【XSLMES-20260528-A01】架子车数设定保存-----------
@AutoLog(value = "架子车数设定-保存")
@Operation(summary = "架子车数设定-保存设定车数")
@RequiresPermissions("xslmes:mes_xsl_rack_train_count_setting:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
public Result<String> edit(@RequestBody MesXslRackTrainCountSettingVO setting) {
try {
rackTrainCountSettingService.saveSetting(setting);
return Result.OK("保存成功!");
} catch (IllegalArgumentException ex) {
return Result.error(ex.getMessage());
}
}
//update-end---author:cursor ---date:20260528 for【XSLMES-20260528-A01】架子车数设定保存-----------
}

View File

@@ -0,0 +1,200 @@
package org.jeecg.modules.xslmes.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.mes.material.entity.MesMaterial;
import org.jeecg.modules.mes.material.service.IMesMaterialService;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecord;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordLine;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestType;
import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestRecordService;
import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestTypeService;
import org.jeecg.modules.xslmes.vo.MesXslRubberQuickTestRecordBatchFromMaterialVO;
import org.jeecg.modules.xslmes.vo.MesXslRubberQuickTestRecordPage;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
/**
* MES 胶料快检记录(主子表)
*/
@Tag(name = "MES胶料快检记录")
@RestController
@RequestMapping("/xslmes/mesXslRubberQuickTestRecord")
@Slf4j
public class MesXslRubberQuickTestRecordController
extends JeecgController<MesXslRubberQuickTestRecord, IMesXslRubberQuickTestRecordService> {
@Autowired
private IMesXslRubberQuickTestRecordService mesXslRubberQuickTestRecordService;
@Autowired
private IMesMaterialService mesMaterialService;
@Autowired
private IMesXslRubberQuickTestTypeService mesXslRubberQuickTestTypeService;
@Operation(summary = "MES胶料快检记录-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<MesXslRubberQuickTestRecord>> queryPageList(
MesXslRubberQuickTestRecord model,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<MesXslRubberQuickTestRecord> queryWrapper =
QueryGenerator.initQueryWrapper(model, req.getParameterMap());
queryWrapper.orderByDesc("inspect_time", "create_time");
Page<MesXslRubberQuickTestRecord> page = new Page<>(pageNo, pageSize);
IPage<MesXslRubberQuickTestRecord> pageList = mesXslRubberQuickTestRecordService.page(page, queryWrapper);
return Result.OK(pageList);
}
@AutoLog(value = "MES胶料快检记录-添加")
@Operation(summary = "MES胶料快检记录-添加")
@RequiresPermissions("mes:mes_xsl_rubber_quick_test_record:add")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody MesXslRubberQuickTestRecordPage page) {
MesXslRubberQuickTestRecord main = new MesXslRubberQuickTestRecord();
BeanUtils.copyProperties(page, main);
String err = validateForSave(main, page.getLineList());
if (err != null) {
return Result.error(err);
}
try {
mesXslRubberQuickTestRecordService.saveMain(main, page.getLineList());
} catch (Exception e) {
log.error(e.getMessage(), e);
return Result.error(e.getMessage());
}
return Result.OK("添加成功!");
}
@AutoLog(value = "MES胶料快检记录-编辑")
@Operation(summary = "MES胶料快检记录-编辑")
@RequiresPermissions("mes:mes_xsl_rubber_quick_test_record:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
public Result<String> edit(@RequestBody MesXslRubberQuickTestRecordPage page) {
MesXslRubberQuickTestRecord main = new MesXslRubberQuickTestRecord();
BeanUtils.copyProperties(page, main);
String err = validateForSave(main, page.getLineList());
if (err != null) {
return Result.error(err);
}
try {
mesXslRubberQuickTestRecordService.updateMain(main, page.getLineList());
} catch (Exception e) {
log.error(e.getMessage(), e);
return Result.error(e.getMessage());
}
return Result.OK("编辑成功!");
}
@AutoLog(value = "MES胶料快检记录-从胶料批量生成")
@Operation(summary = "MES胶料快检记录-从胶料信息批量生成")
@RequiresPermissions("mes:mes_material:rubberQuickTestInspect")
@PostMapping(value = "/batchFromMaterial")
public Result<List<String>> batchFromMaterial(@RequestBody MesXslRubberQuickTestRecordBatchFromMaterialVO vo) {
try {
List<String> ids = mesXslRubberQuickTestRecordService.batchFromMaterial(vo);
return Result.OK("成功生成 " + ids.size() + " 条快检记录", ids);
} catch (Exception e) {
log.error(e.getMessage(), e);
return Result.error(e.getMessage());
}
}
@AutoLog(value = "MES胶料快检记录-删除")
@Operation(summary = "MES胶料快检记录-通过id删除")
@RequiresPermissions("mes:mes_xsl_rubber_quick_test_record:delete")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
mesXslRubberQuickTestRecordService.delMain(id);
return Result.OK("删除成功!");
}
@AutoLog(value = "MES胶料快检记录-批量删除")
@Operation(summary = "MES胶料快检记录-批量删除")
@RequiresPermissions("mes:mes_xsl_rubber_quick_test_record:deleteBatch")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
mesXslRubberQuickTestRecordService.delBatchMain(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
@Operation(summary = "MES胶料快检记录-通过id查询")
@GetMapping(value = "/queryById")
public Result<MesXslRubberQuickTestRecord> queryById(@RequestParam(name = "id", required = true) String id) {
MesXslRubberQuickTestRecord entity = mesXslRubberQuickTestRecordService.getById(id);
if (entity == null) {
return Result.error("未找到对应数据");
}
return Result.OK(entity);
}
@Operation(summary = "MES胶料快检记录-查询明细")
@GetMapping(value = "/queryLineListByRecordId")
public Result<List<MesXslRubberQuickTestRecordLine>> queryLineListByRecordId(
@RequestParam(name = "id", required = true) String id) {
return Result.OK(mesXslRubberQuickTestRecordService.selectLinesByRecordId(id));
}
@RequiresPermissions("mes:mes_xsl_rubber_quick_test_record:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, MesXslRubberQuickTestRecord model) {
return super.exportXls(request, model, MesXslRubberQuickTestRecord.class, "MES胶料快检记录");
}
@RequiresPermissions("mes:mes_xsl_rubber_quick_test_record:importExcel")
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, MesXslRubberQuickTestRecord.class);
}
private String validateForSave(MesXslRubberQuickTestRecord main, List<MesXslRubberQuickTestRecordLine> lineList) {
if (main == null) {
return "参数不能为空";
}
if (oConvertUtils.isEmpty(main.getRubberMaterialId())) {
return "请选择胶料";
}
MesMaterial material = mesMaterialService.getById(main.getRubberMaterialId());
if (material == null) {
return "所选胶料不存在";
}
main.setRubberMaterialName(material.getMaterialName());
if (oConvertUtils.isNotEmpty(main.getQuickTestTypeId())) {
MesXslRubberQuickTestType type = mesXslRubberQuickTestTypeService.getById(main.getQuickTestTypeId());
if (type == null) {
return "所选检验类型不存在";
}
main.setQuickTestTypeName(type.getTypeName());
}
if (lineList == null || lineList.isEmpty()) {
return "请维护检验明细";
}
for (int i = 0; i < lineList.size(); i++) {
MesXslRubberQuickTestRecordLine line = lineList.get(i);
if (line == null || oConvertUtils.isEmpty(line.getInspectItem())) {
return "" + (i + 1) + " 行检验项目不能为空";
}
}
return null;
}
}

View File

@@ -0,0 +1,79 @@
package org.jeecg.modules.xslmes.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 配方示方修改日志(配合示方/混炼示方)
*/
@Data
@TableName("mes_xsl_formula_spec_edit_log")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description = "配方示方修改日志")
public class MesXslFormulaSpecEditLog implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private String id;
@Excel(name = "示方分类", width = 12, dicCode = "xslmes_formula_spec_edit_log_type")
@Dict(dicCode = "xslmes_formula_spec_edit_log_type")
@Schema(description = "示方分类 formula=配合示方 mixing=混炼示方")
private String specType;
@Schema(description = "示方主表ID")
private String specId;
@Excel(name = "示方标识", width = 22)
@Schema(description = "示方标识(胶料代号/规格名)")
private String specTitle;
@Excel(name = "发行编号", width = 16)
@Schema(description = "发行编号")
private String issueNumber;
@Excel(name = "操作类型", width = 10, dicCode = "xslmes_formula_spec_edit_log_action")
@Dict(dicCode = "xslmes_formula_spec_edit_log_action")
@Schema(description = "操作类型 create=新增 update=修改 delete=删除")
private String actionType;
@Excel(name = "修改内容", width = 50)
@Schema(description = "修改内容摘要")
private String changeSummary;
@Schema(description = "变更前快照JSON")
private String beforeSnapshot;
@Schema(description = "变更后快照JSON")
private String afterSnapshot;
@Excel(name = "修改时间", width = 20, format = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "修改时间")
private Date modifyTime;
@Schema(description = "修改人账号")
private String modifyBy;
@Excel(name = "修改人", width = 14)
@Schema(description = "修改人姓名")
private String modifyByName;
@Schema(description = "租户ID")
private Integer tenantId;
}

View File

@@ -0,0 +1,132 @@
package org.jeecg.modules.xslmes.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
/**
* MES 胶料快检记录主表
*/
@Data
@TableName("mes_xsl_rubber_quick_test_record")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description = "MES胶料快检记录")
public class MesXslRubberQuickTestRecord implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private String id;
@Excel(name = "单号", width = 16)
@Schema(description = "单号JL+yyyyMMdd+4位流水")
private String recordNo;
@Schema(description = "胶料ID mes_material.id")
private String rubberMaterialId;
@Excel(name = "胶料名称", width = 20)
@Schema(description = "胶料名称冗余")
private String rubberMaterialName;
@Schema(description = "引用的实验标准ID")
private String stdId;
@Schema(description = "生产机台ID")
private String prodEquipmentLedgerId;
@Excel(name = "生产机台", width = 16)
@Schema(description = "生产机台名称冗余")
private String prodEquipmentName;
@Excel(name = "生产日期", width = 12, format = "yyyy-MM-dd")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date productionDate;
@Excel(name = "车次编号", width = 14)
private String trainNo;
@Excel(name = "班次", width = 10, dicCode = "xslmes_rubber_quick_test_work_shift")
@Dict(dicCode = "xslmes_rubber_quick_test_work_shift")
private String workShift;
@Excel(name = "班组", width = 10, dicCode = "xslmes_rubber_quick_test_work_team")
@Dict(dicCode = "xslmes_rubber_quick_test_work_team")
private String workTeam;
@Excel(name = "检验次数", width = 10)
private Integer inspectTimes;
@Excel(name = "检验时间", width = 18, format = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date inspectTime;
@Schema(description = "检验人用户ID")
private String inspectorUserId;
private String inspectorUsername;
@Excel(name = "检验人", width = 12)
private String inspectorRealname;
@Schema(description = "检验类型ID")
private String quickTestTypeId;
@Excel(name = "检验类型", width = 16)
private String quickTestTypeName;
@Excel(name = "检验结果", width = 10, dicCode = "xslmes_rubber_quick_test_record_result")
@Dict(dicCode = "xslmes_rubber_quick_test_record_result")
private String inspectResult;
@Excel(name = "生产计划号", width = 16)
private String productionPlanNo;
@Schema(description = "检验机台ID")
private String inspectEquipmentLedgerId;
@Excel(name = "检验机台", width = 16)
private String inspectEquipmentName;
@Excel(name = "胶料卡片号", width = 16)
private String rubberCardNo;
@Excel(name = "胶料批次", width = 16)
private String rubberBatchNo;
private Integer tenantId;
private String sysOrgCode;
private String createBy;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String updateBy;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@TableLogic
private Integer delFlag;
@TableField(exist = false)
private List<MesXslRubberQuickTestRecordLine> lineList;
}

View File

@@ -0,0 +1,60 @@
package org.jeecg.modules.xslmes.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
/**
* MES 胶料快检记录明细
*/
@Data
@TableName("mes_xsl_rubber_quick_test_record_line")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description = "MES胶料快检记录明细")
public class MesXslRubberQuickTestRecordLine implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private String id;
private String recordId;
private String dataPointId;
@Excel(name = "检验项目", width = 18)
private String inspectItem;
@Excel(name = "检验下限", width = 12, type = 10)
private BigDecimal lowerLimit;
@Excel(name = "检验值", width = 12, type = 10)
private BigDecimal inspectValue;
@Excel(name = "检验上限", width = 12, type = 10)
private BigDecimal upperLimit;
private Integer sortNo;
private String createBy;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String updateBy;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}

View File

@@ -0,0 +1,9 @@
package org.jeecg.modules.xslmes.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.xslmes.entity.MesXslFormulaSpecEditLog;
/**
* 配方示方修改日志
*/
public interface MesXslFormulaSpecEditLogMapper extends BaseMapper<MesXslFormulaSpecEditLog> {}

View File

@@ -0,0 +1,6 @@
package org.jeecg.modules.xslmes.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordLine;
public interface MesXslRubberQuickTestRecordLineMapper extends BaseMapper<MesXslRubberQuickTestRecordLine> {}

View File

@@ -0,0 +1,6 @@
package org.jeecg.modules.xslmes.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecord;
public interface MesXslRubberQuickTestRecordMapper extends BaseMapper<MesXslRubberQuickTestRecord> {}

View File

@@ -0,0 +1,43 @@
package org.jeecg.modules.xslmes.service;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import org.jeecg.modules.xslmes.entity.MesXslFormulaSpec;
import org.jeecg.modules.xslmes.entity.MesXslFormulaSpecEditLog;
import org.jeecg.modules.xslmes.entity.MesXslFormulaSpecLine;
import org.jeecg.modules.xslmes.vo.MesXslFormulaSpecEditChangeItemVO;
import org.jeecg.modules.xslmes.vo.MesXslFormulaSpecEditLogDetailVO;
import org.jeecg.modules.xslmes.vo.MesXslMixingSpecPage;
/**
* 配方示方修改日志
*/
public interface IMesXslFormulaSpecEditLogService extends IService<MesXslFormulaSpecEditLog> {
String SPEC_TYPE_FORMULA = "formula";
String SPEC_TYPE_MIXING = "mixing";
String ACTION_CREATE = "create";
String ACTION_UPDATE = "update";
String ACTION_DELETE = "delete";
void recordFormulaCreate(MesXslFormulaSpec main, List<MesXslFormulaSpecLine> lineList);
void recordFormulaUpdate(
MesXslFormulaSpec beforeMain,
List<MesXslFormulaSpecLine> beforeLines,
MesXslFormulaSpec afterMain,
List<MesXslFormulaSpecLine> afterLines);
void recordFormulaDelete(MesXslFormulaSpec main, List<MesXslFormulaSpecLine> lineList);
void recordMixingCreate(MesXslMixingSpecPage after);
void recordMixingUpdate(MesXslMixingSpecPage before, MesXslMixingSpecPage after);
void recordMixingDelete(MesXslMixingSpecPage before);
MesXslFormulaSpecEditLogDetailVO getDetail(String id);
List<MesXslFormulaSpecEditChangeItemVO> buildChangeItems(
String specType, String beforeSnapshot, String afterSnapshot);
}

View File

@@ -0,0 +1,29 @@
package org.jeecg.modules.xslmes.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.jeecg.modules.xslmes.vo.MesXslRackTrainCountSettingVO;
/**
* 架子车数设定(基于混炼示方查询,不建独立业务表)
*/
public interface IMesXslRackTrainCountSettingService {
/**
* 分页查询架子车数设定列表
*/
IPage<MesXslRackTrainCountSettingVO> queryPage(
Integer pageNo,
Integer pageSize,
MesXslRackTrainCountSettingVO query,
String keyword);
/**
* 按混炼示方ID查询详情
*/
MesXslRackTrainCountSettingVO queryById(String id);
/**
* 保存设定车数(回写混炼示方 set_train_count 字段)
*/
void saveSetting(MesXslRackTrainCountSettingVO setting);
}

View File

@@ -0,0 +1,26 @@
package org.jeecg.modules.xslmes.service;
import com.baomidou.mybatisplus.extension.service.IService;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecord;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordLine;
import org.jeecg.modules.xslmes.vo.MesXslRubberQuickTestRecordBatchFromMaterialVO;
public interface IMesXslRubberQuickTestRecordService extends IService<MesXslRubberQuickTestRecord> {
void saveMain(MesXslRubberQuickTestRecord main, List<MesXslRubberQuickTestRecordLine> lineList);
void updateMain(MesXslRubberQuickTestRecord main, List<MesXslRubberQuickTestRecordLine> lineList);
void delMain(String id);
void delBatchMain(Collection<? extends Serializable> idList);
List<MesXslRubberQuickTestRecordLine> selectLinesByRecordId(String recordId);
String generateRecordNo(MesXslRubberQuickTestRecord context);
List<String> batchFromMaterial(MesXslRubberQuickTestRecordBatchFromMaterialVO vo);
}

View File

@@ -2,11 +2,9 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslDowntimeMainType;
import org.jeecg.modules.xslmes.mapper.MesXslDowntimeMainTypeMapper;
import org.jeecg.modules.xslmes.service.IMesXslDowntimeMainTypeService;
@@ -22,7 +20,7 @@ public class MesXslDowntimeMainTypeServiceImpl extends ServiceImpl<MesXslDowntim
if (oConvertUtils.isEmpty(downtimeType)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslDowntimeMainType> w = new LambdaQueryWrapper<>();
w.eq(MesXslDowntimeMainType::getDowntimeType, downtimeType.trim());
w.and(
@@ -39,25 +37,5 @@ public class MesXslDowntimeMainTypeServiceImpl extends ServiceImpl<MesXslDowntim
return this.count(w) > 0;
}
private static Integer resolveTenantId(MesXslDowntimeMainType context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260518 for【MES】停机类型同租户不可重复仅统计未删除del_flag=0 或 null-----------
}

View File

@@ -2,11 +2,9 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslDowntimeType;
import org.jeecg.modules.xslmes.mapper.MesXslDowntimeTypeMapper;
import org.jeecg.modules.xslmes.service.IMesXslDowntimeTypeService;
@@ -22,7 +20,7 @@ public class MesXslDowntimeTypeServiceImpl extends ServiceImpl<MesXslDowntimeTyp
if (oConvertUtils.isEmpty(downtimeType)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslDowntimeType> w = new LambdaQueryWrapper<>();
w.eq(MesXslDowntimeType::getDowntimeType, downtimeType.trim());
w.and(
@@ -39,25 +37,5 @@ public class MesXslDowntimeTypeServiceImpl extends ServiceImpl<MesXslDowntimeTyp
return this.count(w) > 0;
}
private static Integer resolveTenantId(MesXslDowntimeType context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260518 for【MES】停机类型名称同租户不可重复仅统计未删除del_flag=0 或 null-----------
}

View File

@@ -5,11 +5,9 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslEquipInspectConfig;
import org.jeecg.modules.xslmes.entity.MesXslEquipInspectConfigLine;
import org.jeecg.modules.xslmes.mapper.MesXslEquipInspectConfigLineMapper;
@@ -89,7 +87,7 @@ public class MesXslEquipInspectConfigServiceImpl
if (oConvertUtils.isEmpty(equipmentLedgerId) || oConvertUtils.isEmpty(configType)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslEquipInspectConfig> w = new LambdaQueryWrapper<>();
w.eq(MesXslEquipInspectConfig::getEquipmentLedgerId, equipmentLedgerId.trim());
w.eq(MesXslEquipInspectConfig::getConfigType, configType.trim());
@@ -107,26 +105,5 @@ public class MesXslEquipInspectConfigServiceImpl
return this.count(w) > 0;
}
private static Integer resolveTenantId(MesXslEquipInspectConfig context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
// ignore
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts);
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260519 for【MES】设备点检配置同设备同类型点检/保养)仅允许一条主数据-----------
}

View File

@@ -8,11 +8,9 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslEquipInspectConfig;
import org.jeecg.modules.xslmes.entity.MesXslEquipInspectConfigLine;
import org.jeecg.modules.xslmes.entity.MesXslEquipInspectRecord;
@@ -104,7 +102,7 @@ public class MesXslEquipInspectRecordServiceImpl
public String generateRecordNo(MesXslEquipInspectRecord context) {
String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
String prefix = "EC" + dateStr;
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslEquipInspectRecord> qw = new LambdaQueryWrapper<>();
qw.likeRight(MesXslEquipInspectRecord::getRecordNo, prefix);
@@ -288,25 +286,4 @@ public class MesXslEquipInspectRecordServiceImpl
}
//update-end---author:jiangxh ---date:20260521 for【MES】不合格点检记录登记处理人及处理时间-----------
private static Integer resolveTenantId(MesXslEquipInspectRecord context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
// ignore
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts);
} catch (NumberFormatException e) {
return null;
}
}
}

View File

@@ -2,11 +2,9 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslEquipmentCategory;
import org.jeecg.modules.xslmes.mapper.MesXslEquipmentCategoryMapper;
import org.jeecg.modules.xslmes.service.IMesXslEquipmentCategoryService;
@@ -22,7 +20,7 @@ public class MesXslEquipmentCategoryServiceImpl extends ServiceImpl<MesXslEquipm
if (oConvertUtils.isEmpty(categoryName)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslEquipmentCategory> w = new LambdaQueryWrapper<>();
w.eq(MesXslEquipmentCategory::getCategoryName, categoryName.trim());
w.and(
@@ -60,25 +58,5 @@ public class MesXslEquipmentCategoryServiceImpl extends ServiceImpl<MesXslEquipm
}
//update-end---author:jiangxh ---date:20260514 for【MES】设备类型按类别名称解析关联提供未删除单条查询-----------
private static Integer resolveTenantId(MesXslEquipmentCategory context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260514 for【MES】设备类别名称同租户唯一仅统计未删除del_flag=0 或 null-----------
}

View File

@@ -2,11 +2,9 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslEquipmentLedger;
import org.jeecg.modules.xslmes.mapper.MesXslEquipmentLedgerMapper;
import org.jeecg.modules.xslmes.service.IMesXslEquipmentLedgerService;
@@ -35,7 +33,7 @@ public class MesXslEquipmentLedgerServiceImpl extends ServiceImpl<MesXslEquipmen
if (oConvertUtils.isEmpty(value)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslEquipmentLedger> w = new LambdaQueryWrapper<>();
w.eq(column, value.trim());
w.and(q -> q.eq(MesXslEquipmentLedger::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslEquipmentLedger::getDelFlag));
@@ -48,25 +46,5 @@ public class MesXslEquipmentLedgerServiceImpl extends ServiceImpl<MesXslEquipmen
return this.count(w) > 0;
}
private static Integer resolveTenantId(MesXslEquipmentLedger context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260518 for【MES】设备台账编号、名称同租户不可重复-----------
}

View File

@@ -2,11 +2,9 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslEquipmentPart;
import org.jeecg.modules.xslmes.mapper.MesXslEquipmentPartMapper;
import org.jeecg.modules.xslmes.service.IMesXslEquipmentPartService;
@@ -22,7 +20,7 @@ public class MesXslEquipmentPartServiceImpl extends ServiceImpl<MesXslEquipmentP
if (oConvertUtils.isEmpty(partCode)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslEquipmentPart> w = new LambdaQueryWrapper<>();
w.eq(MesXslEquipmentPart::getPartCode, partCode.trim());
w.and(
@@ -39,26 +37,6 @@ public class MesXslEquipmentPartServiceImpl extends ServiceImpl<MesXslEquipmentP
return this.count(w) > 0;
}
private static Integer resolveTenantId(MesXslEquipmentPart context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260515 for【MES】设备部位代码同租户不可重复仅统计未删除del_flag=0 或 null-----------
//update-begin---author:jiangxh ---date:20260515 for【MES】设备部位按代码+租户查询单条,供设备小部位导入解析大部位-----------

View File

@@ -2,11 +2,9 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslEquipmentSubPart;
import org.jeecg.modules.xslmes.mapper.MesXslEquipmentSubPartMapper;
import org.jeecg.modules.xslmes.service.IMesXslEquipmentSubPartService;
@@ -22,7 +20,7 @@ public class MesXslEquipmentSubPartServiceImpl extends ServiceImpl<MesXslEquipme
if (oConvertUtils.isEmpty(subPartCode)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslEquipmentSubPart> w = new LambdaQueryWrapper<>();
w.eq(MesXslEquipmentSubPart::getSubPartCode, subPartCode.trim());
w.and(
@@ -39,25 +37,5 @@ public class MesXslEquipmentSubPartServiceImpl extends ServiceImpl<MesXslEquipme
return this.count(w) > 0;
}
private static Integer resolveTenantId(MesXslEquipmentSubPart context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260515 for【MES】设备小部位代码同租户不可重复仅统计未删除del_flag=0 或 null-----------
}

View File

@@ -2,11 +2,9 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslEquipmentType;
import org.jeecg.modules.xslmes.mapper.MesXslEquipmentTypeMapper;
import org.jeecg.modules.xslmes.service.IMesXslEquipmentTypeService;
@@ -22,7 +20,7 @@ public class MesXslEquipmentTypeServiceImpl extends ServiceImpl<MesXslEquipmentT
if (oConvertUtils.isEmpty(typeName)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslEquipmentType> w = new LambdaQueryWrapper<>();
w.eq(MesXslEquipmentType::getTypeName, typeName.trim());
w.and(
@@ -39,25 +37,5 @@ public class MesXslEquipmentTypeServiceImpl extends ServiceImpl<MesXslEquipmentT
return this.count(w) > 0;
}
private static Integer resolveTenantId(MesXslEquipmentType context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260514 for【MES】设备类型名称同租户唯一仅统计未删除del_flag=0 或 null-----------
}

View File

@@ -0,0 +1,256 @@
package org.jeecg.modules.xslmes.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.util.Date;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.modules.xslmes.common.MesXslFormulaSpecEditLogDiffUtil;
import org.jeecg.modules.xslmes.entity.MesXslFormulaSpec;
import org.jeecg.modules.xslmes.entity.MesXslFormulaSpecEditLog;
import org.jeecg.modules.xslmes.entity.MesXslFormulaSpecLine;
import org.jeecg.modules.xslmes.mapper.MesXslFormulaSpecEditLogMapper;
import org.jeecg.modules.xslmes.service.IMesXslFormulaSpecEditLogService;
import org.jeecg.modules.xslmes.vo.MesXslFormulaSpecEditChangeItemVO;
import org.jeecg.modules.xslmes.vo.MesXslFormulaSpecEditLogDetailVO;
import org.jeecg.modules.xslmes.vo.MesXslMixingSpecPage;
import org.springframework.stereotype.Service;
/**
* 配方示方修改日志
*/
@Service
@Slf4j
public class MesXslFormulaSpecEditLogServiceImpl
extends ServiceImpl<MesXslFormulaSpecEditLogMapper, MesXslFormulaSpecEditLog>
implements IMesXslFormulaSpecEditLogService {
@Override
public void recordFormulaCreate(MesXslFormulaSpec main, List<MesXslFormulaSpecLine> lineList) {
if (main == null || StringUtils.isBlank(main.getId())) {
return;
}
String afterSnapshot = MesXslFormulaSpecEditLogDiffUtil.buildFormulaSnapshotJson(main, lineList);
List<MesXslFormulaSpecEditChangeItemVO> items =
buildChangeItems(SPEC_TYPE_FORMULA, null, afterSnapshot);
saveLog(SPEC_TYPE_FORMULA, main.getId(), resolveFormulaTitle(main), main.getIssueNumber(),
ACTION_CREATE, null, afterSnapshot, items, main.getTenantId());
}
@Override
public void recordFormulaUpdate(
MesXslFormulaSpec beforeMain,
List<MesXslFormulaSpecLine> beforeLines,
MesXslFormulaSpec afterMain,
List<MesXslFormulaSpecLine> afterLines) {
if (afterMain == null || StringUtils.isBlank(afterMain.getId())) {
return;
}
String beforeSnapshot = MesXslFormulaSpecEditLogDiffUtil.buildFormulaSnapshotJson(beforeMain, beforeLines);
String afterSnapshot = MesXslFormulaSpecEditLogDiffUtil.buildFormulaSnapshotJson(afterMain, afterLines);
List<MesXslFormulaSpecEditChangeItemVO> items =
buildChangeItems(SPEC_TYPE_FORMULA, beforeSnapshot, afterSnapshot);
if (items.isEmpty()) {
return;
}
saveLog(
SPEC_TYPE_FORMULA,
afterMain.getId(),
resolveFormulaTitle(afterMain),
afterMain.getIssueNumber(),
ACTION_UPDATE,
beforeSnapshot,
afterSnapshot,
items,
afterMain.getTenantId());
}
@Override
public void recordFormulaDelete(MesXslFormulaSpec main, List<MesXslFormulaSpecLine> lineList) {
if (main == null || StringUtils.isBlank(main.getId())) {
return;
}
String beforeSnapshot = MesXslFormulaSpecEditLogDiffUtil.buildFormulaSnapshotJson(main, lineList);
List<MesXslFormulaSpecEditChangeItemVO> items =
buildChangeItems(SPEC_TYPE_FORMULA, beforeSnapshot, null);
saveLog(
SPEC_TYPE_FORMULA,
main.getId(),
resolveFormulaTitle(main),
main.getIssueNumber(),
ACTION_DELETE,
beforeSnapshot,
null,
items,
main.getTenantId());
}
@Override
public void recordMixingCreate(MesXslMixingSpecPage after) {
if (after == null || StringUtils.isBlank(after.getId())) {
return;
}
String afterSnapshot = MesXslFormulaSpecEditLogDiffUtil.buildMixingSnapshotJson(after);
List<MesXslFormulaSpecEditChangeItemVO> items =
buildChangeItems(SPEC_TYPE_MIXING, null, afterSnapshot);
saveLog(
SPEC_TYPE_MIXING,
after.getId(),
resolveMixingTitle(after),
after.getIssueNumber(),
ACTION_CREATE,
null,
afterSnapshot,
items,
after.getTenantId());
}
@Override
public void recordMixingUpdate(MesXslMixingSpecPage before, MesXslMixingSpecPage after) {
if (after == null || StringUtils.isBlank(after.getId())) {
return;
}
String beforeSnapshot = MesXslFormulaSpecEditLogDiffUtil.buildMixingSnapshotJson(before);
String afterSnapshot = MesXslFormulaSpecEditLogDiffUtil.buildMixingSnapshotJson(after);
List<MesXslFormulaSpecEditChangeItemVO> items =
buildChangeItems(SPEC_TYPE_MIXING, beforeSnapshot, afterSnapshot);
if (items.isEmpty()) {
return;
}
saveLog(
SPEC_TYPE_MIXING,
after.getId(),
resolveMixingTitle(after),
after.getIssueNumber(),
ACTION_UPDATE,
beforeSnapshot,
afterSnapshot,
items,
after.getTenantId());
}
@Override
public void recordMixingDelete(MesXslMixingSpecPage before) {
if (before == null || StringUtils.isBlank(before.getId())) {
return;
}
String beforeSnapshot = MesXslFormulaSpecEditLogDiffUtil.buildMixingSnapshotJson(before);
List<MesXslFormulaSpecEditChangeItemVO> items =
buildChangeItems(SPEC_TYPE_MIXING, beforeSnapshot, null);
saveLog(
SPEC_TYPE_MIXING,
before.getId(),
resolveMixingTitle(before),
before.getIssueNumber(),
ACTION_DELETE,
beforeSnapshot,
null,
items,
before.getTenantId());
}
@Override
public MesXslFormulaSpecEditLogDetailVO getDetail(String id) {
MesXslFormulaSpecEditLog row = getById(id);
if (row == null) {
return null;
}
MesXslFormulaSpecEditLogDetailVO detail = new MesXslFormulaSpecEditLogDetailVO();
detail.setId(row.getId());
detail.setSpecType(row.getSpecType());
detail.setSpecId(row.getSpecId());
detail.setSpecTitle(row.getSpecTitle());
detail.setIssueNumber(row.getIssueNumber());
detail.setActionType(row.getActionType());
detail.setChangeSummary(row.getChangeSummary());
detail.setBeforeSnapshot(row.getBeforeSnapshot());
detail.setAfterSnapshot(row.getAfterSnapshot());
detail.setModifyTime(row.getModifyTime());
detail.setModifyBy(row.getModifyBy());
detail.setModifyByName(row.getModifyByName());
detail.setTenantId(row.getTenantId());
detail.setBeforeData(parseSnapshot(row.getBeforeSnapshot()));
detail.setAfterData(parseSnapshot(row.getAfterSnapshot()));
detail.setChangeItems(buildChangeItems(row.getSpecType(), row.getBeforeSnapshot(), row.getAfterSnapshot()));
return detail;
}
@Override
public List<MesXslFormulaSpecEditChangeItemVO> buildChangeItems(
String specType, String beforeSnapshot, String afterSnapshot) {
return MesXslFormulaSpecEditLogDiffUtil.compare(specType, beforeSnapshot, afterSnapshot);
}
private void saveLog(
String specType,
String specId,
String specTitle,
String issueNumber,
String actionType,
String beforeSnapshot,
String afterSnapshot,
List<MesXslFormulaSpecEditChangeItemVO> items,
Integer tenantId) {
LoginUser loginUser = resolveLoginUser();
MesXslFormulaSpecEditLog row = new MesXslFormulaSpecEditLog();
row.setSpecType(specType);
row.setSpecId(specId);
row.setSpecTitle(specTitle);
row.setIssueNumber(issueNumber);
row.setActionType(actionType);
row.setChangeSummary(MesXslFormulaSpecEditLogDiffUtil.buildSummary(actionType, items));
row.setBeforeSnapshot(beforeSnapshot);
row.setAfterSnapshot(afterSnapshot);
row.setModifyTime(new Date());
if (loginUser != null) {
row.setModifyBy(loginUser.getUsername());
row.setModifyByName(StringUtils.defaultIfBlank(loginUser.getRealname(), loginUser.getUsername()));
} else {
row.setModifyByName("未知");
}
row.setTenantId(tenantId);
save(row);
}
private static Object parseSnapshot(String snapshot) {
if (StringUtils.isBlank(snapshot)) {
return null;
}
return JSON.parse(snapshot);
}
private static String resolveFormulaTitle(MesXslFormulaSpec main) {
if (main == null) {
return null;
}
if (StringUtils.isNotBlank(main.getRubberCode())) {
return main.getRubberCode();
}
if (StringUtils.isNotBlank(main.getSpecCode())) {
return main.getSpecCode();
}
return main.getIssueNumber();
}
private static String resolveMixingTitle(MesXslMixingSpecPage page) {
if (page == null) {
return null;
}
return StringUtils.defaultIfBlank(page.getSpecName(), page.getIssueNumber());
}
private static LoginUser resolveLoginUser() {
try {
Object principal = SecurityUtils.getSubject().getPrincipal();
if (principal instanceof LoginUser loginUser) {
return loginUser;
}
} catch (Exception ignored) {
// 免密或未登录场景
}
return null;
}
}

View File

@@ -37,6 +37,7 @@ import org.jeecg.modules.xslmes.entity.MesXslMixingSpecMaterial;
import org.jeecg.modules.xslmes.mapper.MesXslFormulaSpecLineMapper;
import org.jeecg.modules.xslmes.mapper.MesXslFormulaSpecMapper;
import org.jeecg.modules.xslmes.service.IMesXslEquipmentLedgerService;
import org.jeecg.modules.xslmes.service.IMesXslFormulaSpecEditLogService;
import org.jeecg.modules.xslmes.service.IMesXslFormulaSpecService;
import org.jeecg.modules.xslmes.service.IMesXslFormulaSpecSettingService;
import org.jeecg.modules.xslmes.service.IMesXslMixerMaterialKindCfgService;
@@ -95,6 +96,9 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
@Resource
private IMesXslMixerMaterialKindCfgService mesXslMixerMaterialKindCfgService;
@Resource
private IMesXslFormulaSpecEditLogService mesXslFormulaSpecEditLogService;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveMain(MesXslFormulaSpec main, List<MesXslFormulaSpecLine> lineList) {
@@ -105,21 +109,43 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
this.save(main);
insertLines(main, lineList);
refreshMainSummary(main.getId());
//update-begin---author:cursor ---date:20260526 for【配方日志查询】配合示方新增落库修改日志-----------
MesXslFormulaSpec saved = getByIdWithLines(main.getId());
if (saved != null) {
mesXslFormulaSpecEditLogService.recordFormulaCreate(saved, saved.getLineList());
}
//update-end---author:cursor ---date:20260526 for【配方日志查询】配合示方新增落库修改日志-----------
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateMain(MesXslFormulaSpec main, List<MesXslFormulaSpecLine> lineList) {
//update-begin---author:cursor ---date:20260526 for【配方日志查询】配合示方编辑前抓取快照-----------
MesXslFormulaSpec before = getByIdWithLines(main.getId());
//update-end---author:cursor ---date:20260526 for【配方日志查询】配合示方编辑前抓取快照-----------
fillMainDefaults(main);
this.updateById(main);
lineMapper.delete(new LambdaQueryWrapper<MesXslFormulaSpecLine>().eq(MesXslFormulaSpecLine::getFormulaSpecId, main.getId()));
insertLines(main, lineList);
refreshMainSummary(main.getId());
//update-begin---author:cursor ---date:20260526 for【配方日志查询】配合示方编辑落库修改日志-----------
MesXslFormulaSpec after = getByIdWithLines(main.getId());
if (after != null) {
mesXslFormulaSpecEditLogService.recordFormulaUpdate(
before, before == null ? null : before.getLineList(), after, after.getLineList());
}
//update-end---author:cursor ---date:20260526 for【配方日志查询】配合示方编辑落库修改日志-----------
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delMain(String id) {
//update-begin---author:cursor ---date:20260526 for【配方日志查询】配合示方删除落库修改日志-----------
MesXslFormulaSpec before = getByIdWithLines(id);
if (before != null) {
mesXslFormulaSpecEditLogService.recordFormulaDelete(before, before.getLineList());
}
//update-end---author:cursor ---date:20260526 for【配方日志查询】配合示方删除落库修改日志-----------
lineMapper.delete(new LambdaQueryWrapper<MesXslFormulaSpecLine>().eq(MesXslFormulaSpecLine::getFormulaSpecId, id));
this.removeById(id);
}

View File

@@ -2,11 +2,9 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslInspectMaintainItem;
import org.jeecg.modules.xslmes.mapper.MesXslInspectMaintainItemMapper;
import org.jeecg.modules.xslmes.service.IMesXslInspectMaintainItemService;
@@ -36,7 +34,7 @@ public class MesXslInspectMaintainItemServiceImpl
if (oConvertUtils.isEmpty(value)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslInspectMaintainItem> w = new LambdaQueryWrapper<>();
w.eq(column, value.trim());
w.and(
@@ -53,26 +51,5 @@ public class MesXslInspectMaintainItemServiceImpl
return this.count(w) > 0;
}
private static Integer resolveTenantId(MesXslInspectMaintainItem context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
// ignore
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts);
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260519 for【MES】点检及保养项目项目名称/编号同租户不可重复-----------
}

View File

@@ -2,11 +2,9 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslManufacturer;
import org.jeecg.modules.xslmes.mapper.MesXslManufacturerMapper;
import org.jeecg.modules.xslmes.service.IMesXslManufacturerService;
@@ -22,7 +20,7 @@ public class MesXslManufacturerServiceImpl extends ServiceImpl<MesXslManufacture
if (oConvertUtils.isEmpty(manufacturerName)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslManufacturer> w = new LambdaQueryWrapper<>();
w.eq(MesXslManufacturer::getManufacturerName, manufacturerName.trim());
w.and(
@@ -39,25 +37,5 @@ public class MesXslManufacturerServiceImpl extends ServiceImpl<MesXslManufacture
return this.count(w) > 0;
}
private static Integer resolveTenantId(MesXslManufacturer context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260515 for【MES】厂家名称同租户不可重复仅统计未删除del_flag=0 或 null-----------
}

View File

@@ -2,7 +2,6 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.http.HttpServletRequest;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
@@ -13,10 +12,7 @@ import java.util.Map;
import java.util.Set;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.config.TenantContext;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.modules.system.entity.SysCategory;
import org.jeecg.modules.system.entity.SysDict;
@@ -24,6 +20,7 @@ import org.jeecg.modules.system.entity.SysDictItem;
import org.jeecg.modules.system.service.ISysCategoryService;
import org.jeecg.modules.system.service.ISysDictItemService;
import org.jeecg.modules.system.service.ISysDictService;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslMixerMaterialKindCfg;
import org.jeecg.modules.xslmes.mapper.MesXslMixerMaterialKindCfgMapper;
import org.jeecg.modules.xslmes.service.IMesXslMixerMaterialKindCfgService;
@@ -73,7 +70,7 @@ public class MesXslMixerMaterialKindCfgServiceImpl
if (lines == null || lines.isEmpty()) {
throw new JeecgBootException("请至少维护一条种类配置明细");
}
Integer resolvedTenantId = resolveTenantId(tenantId);
Integer resolvedTenantId = MesXslTenantUtils.resolveTenantId(tenantId);
Set<String> kindKeys = new HashSet<>();
Set<String> categoryRefIds = new HashSet<>();
for (MesXslMixerMaterialKindCfg line : lines) {
@@ -124,7 +121,7 @@ public class MesXslMixerMaterialKindCfgServiceImpl
cfg.setKindKey(item.getItemValue());
cfg.setKindName(item.getItemText());
cfg.setPriority(item.getSortOrder() != null ? item.getSortOrder() : priorityBase + index * 10);
cfg.setTenantId(resolveTenantId(tenantId));
cfg.setTenantId(MesXslTenantUtils.resolveTenantId(tenantId));
result.add(cfg);
index++;
}
@@ -183,7 +180,7 @@ public class MesXslMixerMaterialKindCfgServiceImpl
cfg.setKindKey(refCode);
cfg.setKindName(category.getName());
cfg.setPriority(priorityBase + i * 10);
cfg.setTenantId(resolveTenantId(tenantId));
cfg.setTenantId(MesXslTenantUtils.resolveTenantId(tenantId));
result.add(cfg);
}
if (result.isEmpty()) {
@@ -197,7 +194,7 @@ public class MesXslMixerMaterialKindCfgServiceImpl
qw.eq(MesXslMixerMaterialKindCfg::getSourceType, sourceType);
qw.eq(MesXslMixerMaterialKindCfg::getSourceRootCode, sourceRootCode);
qw.and(q -> q.eq(MesXslMixerMaterialKindCfg::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslMixerMaterialKindCfg::getDelFlag));
Integer resolvedTenantId = resolveTenantId(tenantId);
Integer resolvedTenantId = MesXslTenantUtils.resolveTenantId(tenantId);
if (resolvedTenantId != null && MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
qw.eq(MesXslMixerMaterialKindCfg::getTenantId, resolvedTenantId);
}
@@ -246,7 +243,7 @@ public class MesXslMixerMaterialKindCfgServiceImpl
@Override
public void checkDuplicate(MesXslMixerMaterialKindCfg line, String excludeId) {
Integer tenantId = resolveTenantId(line.getTenantId());
Integer tenantId = MesXslTenantUtils.resolveTenantId(line.getTenantId());
LambdaQueryWrapper<MesXslMixerMaterialKindCfg> kindQw = new LambdaQueryWrapper<>();
kindQw.eq(MesXslMixerMaterialKindCfg::getKindKey, line.getKindKey().trim());
kindQw.and(q -> q.eq(MesXslMixerMaterialKindCfg::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslMixerMaterialKindCfg::getDelFlag));
@@ -274,30 +271,6 @@ public class MesXslMixerMaterialKindCfgServiceImpl
}
}
private static Integer resolveTenantId(Integer tenantId) {
if (tenantId != null) {
return tenantId;
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
if (request != null) {
ts = TokenUtils.getTenantIdByRequest(request);
}
} catch (Exception ignored) {
// ignore
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts);
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A51】密炼物料种类配置展开与批量保存-----------
//update-begin---author:cursor ---date:20260525 for【XSLMES-20260525-A52】密炼物料种类配置关联解析-----------
@@ -307,7 +280,7 @@ public class MesXslMixerMaterialKindCfgServiceImpl
public MesXslMixerMaterialKindLookupVO loadKindLookup(Integer tenantId) {
LambdaQueryWrapper<MesXslMixerMaterialKindCfg> qw = new LambdaQueryWrapper<>();
qw.and(q -> q.eq(MesXslMixerMaterialKindCfg::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslMixerMaterialKindCfg::getDelFlag));
Integer resolvedTenantId = resolveTenantId(tenantId);
Integer resolvedTenantId = MesXslTenantUtils.resolveTenantId(tenantId);
if (resolvedTenantId != null && MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
qw.eq(MesXslMixerMaterialKindCfg::getTenantId, resolvedTenantId);
}

View File

@@ -38,8 +38,10 @@ import org.jeecg.modules.xslmes.mapper.MesXslMixingSpecMapper;
import org.jeecg.modules.xslmes.mapper.MesXslMixingSpecMaterialMapper;
import org.jeecg.modules.xslmes.mapper.MesXslMixingSpecStepMapper;
import org.jeecg.modules.xslmes.mapper.MesXslMixingSpecTcuMapper;
import org.jeecg.modules.xslmes.service.IMesXslFormulaSpecEditLogService;
import org.jeecg.modules.xslmes.service.IMesXslMixingSpecService;
import org.jeecg.modules.xslmes.vo.MesXslMixingSpecPage;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
@@ -74,6 +76,9 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
@Resource
private ISysCategoryService sysCategoryService;
@Resource
private IMesXslFormulaSpecEditLogService mesXslFormulaSpecEditLogService;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveMain(
@@ -93,6 +98,11 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
trace.step("saveMain", "mainId", main.getId());
saveChildren(main.getId(), materialList, stepList, downStepList, tcuList, trace);
trace.finish();
//update-begin---author:cursor ---date:20260526 for【配方日志查询】混炼示方新增落库修改日志-----------
// 直接用已落库的入参构建快照,避免二次全量 queryPageById() 查询
mesXslFormulaSpecEditLogService.recordMixingCreate(
buildPageFromInput(main, materialList, stepList, downStepList, tcuList));
//update-end---author:cursor ---date:20260526 for【配方日志查询】混炼示方新增落库修改日志-----------
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A17】混炼示方主子表新增保存-----------
}
@@ -109,6 +119,9 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
trace.logPayloadSize(materialList, stepList, downStepList, tcuList);
//update-end---author:cursor ---date:20260525 for【XSLMES-20260525-A47】混炼示方保存分步骤性能日志-----------
//update-begin---author:cursor ---date:20260522 for【XSLMES-20260522-A17】混炼示方主子表编辑保存-----------
//update-begin---author:cursor ---date:20260526 for【配方日志查询】混炼示方编辑前抓取快照-----------
MesXslMixingSpecPage before = queryPageById(main.getId());
//update-end---author:cursor ---date:20260526 for【配方日志查询】混炼示方编辑前抓取快照-----------
normalizeMain(main);
trace.step("normalizeMain");
this.updateById(main);
@@ -116,6 +129,11 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
clearChildren(main.getId(), trace);
saveChildren(main.getId(), materialList, stepList, downStepList, tcuList, trace);
trace.finish();
//update-begin---author:cursor ---date:20260526 for【配方日志查询】混炼示方编辑落库修改日志-----------
// 直接用已落库的入参构建 after 快照,避免二次全量 queryPageById() 查询(节省 5 次 SELECT
mesXslFormulaSpecEditLogService.recordMixingUpdate(before,
buildPageFromInput(main, materialList, stepList, downStepList, tcuList));
//update-end---author:cursor ---date:20260526 for【配方日志查询】混炼示方编辑落库修改日志-----------
//update-end---author:cursor ---date:20260522 for【XSLMES-20260522-A17】混炼示方主子表编辑保存-----------
}
@@ -150,6 +168,14 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
if (CollectionUtils.isEmpty(mains)) {
return;
}
//update-begin---author:cursor ---date:20260526 for【配方日志查询】混炼示方删除落库修改日志-----------
for (String id : ids) {
MesXslMixingSpecPage before = queryPageById(id);
if (before != null) {
mesXslFormulaSpecEditLogService.recordMixingDelete(before);
}
}
//update-end---author:cursor ---date:20260526 for【配方日志查询】混炼示方删除落库修改日志-----------
clearChildrenBatch(ids);
this.removeByIds(ids);
Set<String> specNames = new LinkedHashSet<>();
@@ -814,4 +840,21 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
this.update(wrapper);
}
//update-end---author:cursor ---date:20260526 for【XSLMES-20260526-A61】混炼示方密炼PS审批联动同步审批人-----------
//update-begin---author:cursor ---date:20260527 for【配方日志查询】从入参直接构建快照避免二次查库-----------
private MesXslMixingSpecPage buildPageFromInput(
MesXslMixingSpec main,
List<MesXslMixingSpecMaterial> materialList,
List<MesXslMixingSpecStep> stepList,
List<MesXslMixingSpecDownStep> downStepList,
List<MesXslMixingSpecTcu> tcuList) {
MesXslMixingSpecPage page = new MesXslMixingSpecPage();
BeanUtils.copyProperties(main, page);
page.setMaterialList(materialList != null ? materialList : new ArrayList<>());
page.setStepList(stepList != null ? stepList : new ArrayList<>());
page.setDownStepList(downStepList != null ? downStepList : new ArrayList<>());
page.setTcuList(tcuList != null ? tcuList : new ArrayList<>());
return page;
}
//update-end---author:cursor ---date:20260527 for【配方日志查询】从入参直接构建快照避免二次查库-----------
}

View File

@@ -2,11 +2,9 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslProcessOperation;
import org.jeecg.modules.xslmes.mapper.MesXslProcessOperationMapper;
import org.jeecg.modules.xslmes.service.IMesXslProcessOperationService;
@@ -22,7 +20,7 @@ public class MesXslProcessOperationServiceImpl extends ServiceImpl<MesXslProcess
if (oConvertUtils.isEmpty(operationCode)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslProcessOperation> w = new LambdaQueryWrapper<>();
w.eq(MesXslProcessOperation::getOperationCode, operationCode.trim());
// 仅与「未删除」数据判重del_flag=0 或历史空值视为正常行排除已删除del_flag=1
@@ -41,25 +39,5 @@ public class MesXslProcessOperationServiceImpl extends ServiceImpl<MesXslProcess
}
/** 与 MybatisInterceptor 注入 tenant_id 的取值顺序尽量一致,避免前端未传 tenantId 时查不到已存在数据 */
private static Integer resolveTenantId(MesXslProcessOperation context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260514 for【MES】工序编码同租户唯一仅统计未删除del_flag=0 或 null-----------
}

View File

@@ -0,0 +1,168 @@
package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.xslmes.entity.MesXslMixingSpec;
import org.jeecg.modules.xslmes.service.IMesXslFormulaSpecEditLogService;
import org.jeecg.modules.xslmes.service.IMesXslMixingSpecService;
import org.jeecg.modules.xslmes.service.IMesXslRackTrainCountSettingService;
import org.jeecg.modules.xslmes.vo.MesXslMixingSpecPage;
import org.jeecg.modules.xslmes.vo.MesXslRackTrainCountSettingVO;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MesXslRackTrainCountSettingServiceImpl implements IMesXslRackTrainCountSettingService {
private static final String STATUS_SET = "1";
private static final String STATUS_UNSET = "0";
private final IMesXslMixingSpecService mixingSpecService;
private final IMesXslFormulaSpecEditLogService mesXslFormulaSpecEditLogService;
public MesXslRackTrainCountSettingServiceImpl(
IMesXslMixingSpecService mixingSpecService,
IMesXslFormulaSpecEditLogService mesXslFormulaSpecEditLogService) {
this.mixingSpecService = mixingSpecService;
this.mesXslFormulaSpecEditLogService = mesXslFormulaSpecEditLogService;
}
//update-begin---author:cursor ---date:20260528 for【XSLMES-20260528-A01】架子车数设定分页查询混炼示方-----------
@Override
public IPage<MesXslRackTrainCountSettingVO> queryPage(
Integer pageNo, Integer pageSize, MesXslRackTrainCountSettingVO query, String keyword) {
LambdaQueryWrapper<MesXslMixingSpec> wrapper = buildQueryWrapper(query, keyword);
wrapper.orderByDesc(MesXslMixingSpec::getUpdateTime).orderByDesc(MesXslMixingSpec::getCreateTime);
Page<MesXslMixingSpec> page = mixingSpecService.page(new Page<>(pageNo, pageSize), wrapper);
return page.convert(this::toVo);
}
//update-end---author:cursor ---date:20260528 for【XSLMES-20260528-A01】架子车数设定分页查询混炼示方-----------
//update-begin---author:cursor ---date:20260528 for【XSLMES-20260528-A01】架子车数设定按ID查询-----------
@Override
public MesXslRackTrainCountSettingVO queryById(String id) {
if (StringUtils.isBlank(id)) {
return null;
}
MesXslMixingSpec spec = mixingSpecService.getById(id);
if (spec == null || isDeleted(spec)) {
return null;
}
return toVo(spec);
}
//update-end---author:cursor ---date:20260528 for【XSLMES-20260528-A01】架子车数设定按ID查询-----------
//update-begin---author:cursor ---date:20260528 for【XSLMES-20260528-A01】架子车数设定保存回写混炼示方-----------
@Override
@Transactional(rollbackFor = Exception.class)
public void saveSetting(MesXslRackTrainCountSettingVO setting) {
if (setting == null) {
throw new IllegalArgumentException("参数不能为空");
}
String mixingSpecId = StringUtils.isNotBlank(setting.getMixingSpecId()) ? setting.getMixingSpecId() : setting.getId();
if (StringUtils.isBlank(mixingSpecId)) {
throw new IllegalArgumentException("混炼示方ID不能为空");
}
MesXslMixingSpec existing = mixingSpecService.getById(mixingSpecId);
if (existing == null || isDeleted(existing)) {
throw new IllegalArgumentException("混炼示方不存在或已删除");
}
if (setting.getSetTrainCount() != null && setting.getSetTrainCount() < 0) {
throw new IllegalArgumentException("设定车数不能小于0");
}
//update-begin---author:cursor ---date:20260528 for【XSLMES-20260528-A03】架子车数设定保存写入配方日志-----------
MesXslMixingSpecPage before = mixingSpecService.queryPageById(mixingSpecId);
if (before == null) {
throw new IllegalArgumentException("混炼示方不存在或已删除");
}
Integer newSetTrainCount = setting.getSetTrainCount();
if (Objects.equals(before.getSetTrainCount(), newSetTrainCount)) {
return;
}
MesXslMixingSpec update = new MesXslMixingSpec();
update.setId(mixingSpecId);
update.setSetTrainCount(newSetTrainCount);
mixingSpecService.updateById(update);
MesXslMixingSpecPage after = copyMixingPageWithSetTrainCount(before, newSetTrainCount);
mesXslFormulaSpecEditLogService.recordMixingUpdate(before, after);
//update-end---author:cursor ---date:20260528 for【XSLMES-20260528-A03】架子车数设定保存写入配方日志-----------
}
//update-end---author:cursor ---date:20260528 for【XSLMES-20260528-A01】架子车数设定保存回写混炼示方-----------
private LambdaQueryWrapper<MesXslMixingSpec> buildQueryWrapper(MesXslRackTrainCountSettingVO query, String keyword) {
LambdaQueryWrapper<MesXslMixingSpec> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MesXslMixingSpec::getDelFlag, 0);
if (query != null) {
if (query.getTenantId() != null) {
wrapper.eq(MesXslMixingSpec::getTenantId, query.getTenantId());
}
if (StringUtils.isNotBlank(query.getSpecCode())) {
wrapper.like(MesXslMixingSpec::getSpecName, query.getSpecCode().trim());
}
if (StringUtils.isNotBlank(query.getMixingSpecId())) {
wrapper.eq(MesXslMixingSpec::getId, query.getMixingSpecId().trim());
}
if (StringUtils.isNotBlank(query.getMachineId())) {
wrapper.eq(MesXslMixingSpec::getMachineId, query.getMachineId().trim());
}
if (StringUtils.isNotBlank(query.getMachineName())) {
wrapper.like(MesXslMixingSpec::getMachineName, query.getMachineName().trim());
}
if (STATUS_SET.equals(query.getStatus())) {
wrapper.isNotNull(MesXslMixingSpec::getSetTrainCount);
} else if (STATUS_UNSET.equals(query.getStatus())) {
wrapper.isNull(MesXslMixingSpec::getSetTrainCount);
}
}
if (StringUtils.isNotBlank(keyword)) {
String kw = keyword.trim();
wrapper.and(w -> w.like(MesXslMixingSpec::getSpecName, kw)
.or()
.like(MesXslMixingSpec::getMachineName, kw)
.or()
.like(MesXslMixingSpec::getIssueNumber, kw));
}
return wrapper;
}
private MesXslRackTrainCountSettingVO toVo(MesXslMixingSpec spec) {
MesXslRackTrainCountSettingVO vo = new MesXslRackTrainCountSettingVO();
vo.setId(spec.getId());
vo.setMixingSpecId(spec.getId());
vo.setTenantId(spec.getTenantId());
vo.setSpecCode(spec.getSpecName());
vo.setMachineId(spec.getMachineId());
vo.setMachineName(spec.getMachineName());
vo.setSetTrainCount(spec.getSetTrainCount());
vo.setStatus(resolveStatus(spec.getSetTrainCount()));
vo.setCreateBy(spec.getCreateBy());
vo.setCreateTime(spec.getCreateTime());
vo.setUpdateBy(spec.getUpdateBy());
vo.setUpdateTime(spec.getUpdateTime());
return vo;
}
private String resolveStatus(Integer setTrainCount) {
return setTrainCount != null ? STATUS_SET : STATUS_UNSET;
}
private boolean isDeleted(MesXslMixingSpec spec) {
return spec.getDelFlag() != null && spec.getDelFlag() != 0;
}
/** 基于变更前快照复制 after仅更新设定车数避免二次全量查子表 */
private static MesXslMixingSpecPage copyMixingPageWithSetTrainCount(MesXslMixingSpecPage before, Integer setTrainCount) {
MesXslMixingSpecPage after = new MesXslMixingSpecPage();
BeanUtils.copyProperties(before, after);
after.setMaterialList(before.getMaterialList());
after.setStepList(before.getStepList());
after.setDownStepList(before.getDownStepList());
after.setTcuList(before.getTcuList());
after.setSetTrainCount(setTrainCount);
return after;
}
}

View File

@@ -2,12 +2,10 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestDataPoint;
import org.jeecg.modules.xslmes.mapper.MesXslRubberQuickTestDataPointMapper;
import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestDataPointService;
@@ -25,7 +23,7 @@ public class MesXslRubberQuickTestDataPointServiceImpl
if (oConvertUtils.isEmpty(pointName)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslRubberQuickTestDataPoint> w = new LambdaQueryWrapper<>();
w.eq(MesXslRubberQuickTestDataPoint::getPointName, pointName.trim());
w.and(
@@ -70,25 +68,5 @@ public class MesXslRubberQuickTestDataPointServiceImpl
}
}
private static Integer resolveTenantId(MesXslRubberQuickTestDataPoint context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260522 for【MES】胶料快检数据点名称同租户唯一-----------
}

View File

@@ -12,18 +12,16 @@ import java.util.Collection;
import java.util.List;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestMethod;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestMethodLine;
@@ -66,7 +64,7 @@ public class MesXslRubberQuickTestMethodServiceImpl
public String generateNextMethodCode(MesXslRubberQuickTestMethod context) {
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
Integer max = baseMapper.selectMaxNumericMethodCode(tenantId);
@@ -94,7 +92,7 @@ public class MesXslRubberQuickTestMethodServiceImpl
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslRubberQuickTestMethod> w = new LambdaQueryWrapper<>();
@@ -286,45 +284,6 @@ public class MesXslRubberQuickTestMethodServiceImpl
private static Integer resolveTenantId(MesXslRubberQuickTestMethod context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260522 for【MES】胶料快检实验方法编号001递增、名称同租户唯一、主子保存-----------

View File

@@ -0,0 +1,255 @@
package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.mes.material.entity.MesMaterial;
import org.jeecg.modules.mes.material.service.IMesMaterialService;
import org.jeecg.modules.xslmes.common.XslMesBizConstants;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestMethod;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecord;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordLine;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestStd;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestStdLine;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestType;
import org.jeecg.modules.xslmes.mapper.MesXslRubberQuickTestRecordLineMapper;
import org.jeecg.modules.xslmes.mapper.MesXslRubberQuickTestRecordMapper;
import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestMethodService;
import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestRecordService;
import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestStdService;
import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestTypeService;
import org.jeecg.modules.xslmes.vo.MesXslRubberQuickTestRecordBatchFromMaterialVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
@Service
public class MesXslRubberQuickTestRecordServiceImpl
extends ServiceImpl<MesXslRubberQuickTestRecordMapper, MesXslRubberQuickTestRecord>
implements IMesXslRubberQuickTestRecordService {
private static final String STD_ENABLE_IN_USE = "1";
@Autowired
private MesXslRubberQuickTestRecordLineMapper mesXslRubberQuickTestRecordLineMapper;
@Autowired
private IMesXslRubberQuickTestStdService mesXslRubberQuickTestStdService;
@Autowired
private IMesXslRubberQuickTestMethodService mesXslRubberQuickTestMethodService;
@Autowired
private IMesMaterialService mesMaterialService;
@Autowired
private IMesXslRubberQuickTestTypeService mesXslRubberQuickTestTypeService;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveMain(MesXslRubberQuickTestRecord main, List<MesXslRubberQuickTestRecordLine> lineList) {
if (oConvertUtils.isEmpty(main.getRecordNo())) {
main.setRecordNo(generateRecordNo(main));
}
this.save(main);
insertLines(main.getId(), lineList);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateMain(MesXslRubberQuickTestRecord main, List<MesXslRubberQuickTestRecordLine> lineList) {
if (oConvertUtils.isNotEmpty(main.getId())) {
MesXslRubberQuickTestRecord old = this.getById(main.getId());
if (old != null) {
main.setRecordNo(old.getRecordNo());
}
}
this.updateById(main);
mesXslRubberQuickTestRecordLineMapper.delete(
new LambdaQueryWrapper<MesXslRubberQuickTestRecordLine>()
.eq(MesXslRubberQuickTestRecordLine::getRecordId, main.getId()));
insertLines(main.getId(), lineList);
}
private void insertLines(String recordId, List<MesXslRubberQuickTestRecordLine> lineList) {
if (CollectionUtils.isEmpty(lineList)) {
return;
}
int sort = 0;
for (MesXslRubberQuickTestRecordLine line : lineList) {
line.setId(null);
line.setRecordId(recordId);
line.setSortNo(sort++);
mesXslRubberQuickTestRecordLineMapper.insert(line);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delMain(String id) {
mesXslRubberQuickTestRecordLineMapper.delete(
new LambdaQueryWrapper<MesXslRubberQuickTestRecordLine>().eq(MesXslRubberQuickTestRecordLine::getRecordId, id));
this.removeById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delBatchMain(Collection<? extends Serializable> idList) {
for (Serializable id : idList) {
delMain(id.toString());
}
}
@Override
public List<MesXslRubberQuickTestRecordLine> selectLinesByRecordId(String recordId) {
return mesXslRubberQuickTestRecordLineMapper.selectList(
new LambdaQueryWrapper<MesXslRubberQuickTestRecordLine>()
.eq(MesXslRubberQuickTestRecordLine::getRecordId, recordId)
.orderByAsc(MesXslRubberQuickTestRecordLine::getSortNo));
}
//update-begin---author:jiangxh ---date:20260528 for【MES】胶料快检记录单号JL+日期+4位流水自动生成-----------
@Override
public String generateRecordNo(MesXslRubberQuickTestRecord context) {
String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
String prefix = "JL" + dateStr;
Integer tenantId = resolveTenantId(context);
LambdaQueryWrapper<MesXslRubberQuickTestRecord> qw = new LambdaQueryWrapper<>();
qw.likeRight(MesXslRubberQuickTestRecord::getRecordNo, prefix);
qw.and(q -> q.eq(MesXslRubberQuickTestRecord::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslRubberQuickTestRecord::getDelFlag));
if (tenantId != null) {
qw.eq(MesXslRubberQuickTestRecord::getTenantId, tenantId);
}
qw.select(MesXslRubberQuickTestRecord::getRecordNo);
qw.orderByDesc(MesXslRubberQuickTestRecord::getRecordNo);
qw.last("LIMIT 1");
MesXslRubberQuickTestRecord last = this.getOne(qw, false);
int nextSeq = 1;
if (last != null
&& oConvertUtils.isNotEmpty(last.getRecordNo())
&& last.getRecordNo().length() >= prefix.length() + 4) {
String suffix = last.getRecordNo().substring(prefix.length());
try {
nextSeq = Integer.parseInt(suffix) + 1;
} catch (NumberFormatException ignored) {
nextSeq = (int) this.count(qw) + 1;
}
}
return prefix + String.format("%04d", nextSeq);
}
private static Integer resolveTenantId(MesXslRubberQuickTestRecord context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260528 for【MES】胶料快检记录单号JL+日期+4位流水自动生成-----------
//update-begin---author:jiangxh ---date:20260525 for【MES】胶料信息多选按实验标准批量生成快检记录-----------
@Override
@Transactional(rollbackFor = Exception.class)
public List<String> batchFromMaterial(MesXslRubberQuickTestRecordBatchFromMaterialVO vo) {
if (vo == null || CollectionUtils.isEmpty(vo.getMaterialIds())) {
throw new JeecgBootException("请至少选择一条胶料信息");
}
List<String> createdIds = new ArrayList<>();
for (String materialId : vo.getMaterialIds()) {
if (oConvertUtils.isEmpty(materialId)) {
continue;
}
String mid = materialId.trim();
MesMaterial material = mesMaterialService.getById(mid);
if (material == null) {
throw new JeecgBootException("胶料不存在:" + mid);
}
MesXslRubberQuickTestStd std = findApprovedStdForMaterial(mid);
if (std == null) {
throw new JeecgBootException(
"胶料【" + material.getMaterialName() + "】未找到已启用且已批准的实验标准,请先在胶料快检实验标准中维护");
}
List<MesXslRubberQuickTestStdLine> stdLines = mesXslRubberQuickTestStdService.selectLinesByStdId(std.getId());
if (CollectionUtils.isEmpty(stdLines)) {
throw new JeecgBootException("胶料【" + material.getMaterialName() + "】关联的实验标准无明细数据");
}
MesXslRubberQuickTestRecord main = buildMainFromMaterial(material, std);
List<MesXslRubberQuickTestRecordLine> recordLines = new ArrayList<>();
for (MesXslRubberQuickTestStdLine stdLine : stdLines) {
MesXslRubberQuickTestRecordLine rl = new MesXslRubberQuickTestRecordLine();
rl.setDataPointId(stdLine.getDataPointId());
rl.setInspectItem(stdLine.getPointName());
rl.setLowerLimit(stdLine.getLowerLimit());
rl.setUpperLimit(stdLine.getUpperLimit());
recordLines.add(rl);
}
saveMain(main, recordLines);
createdIds.add(main.getId());
}
if (createdIds.isEmpty()) {
throw new JeecgBootException("未生成任何快检记录");
}
return createdIds;
}
private MesXslRubberQuickTestStd findApprovedStdForMaterial(String materialId) {
LambdaQueryWrapper<MesXslRubberQuickTestStd> w = new LambdaQueryWrapper<>();
w.eq(MesXslRubberQuickTestStd::getRubberMaterialId, materialId);
w.eq(MesXslRubberQuickTestStd::getEnableStatus, STD_ENABLE_IN_USE);
w.eq(MesXslRubberQuickTestStd::getAuditStatus, XslMesBizConstants.RUBBER_QUICK_TEST_STD_AUDIT_APPROVED);
w.and(q -> q.eq(MesXslRubberQuickTestStd::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslRubberQuickTestStd::getDelFlag));
w.orderByDesc(MesXslRubberQuickTestStd::getCreateTime);
w.last("LIMIT 1");
return mesXslRubberQuickTestStdService.getOne(w, false);
}
private MesXslRubberQuickTestRecord buildMainFromMaterial(MesMaterial material, MesXslRubberQuickTestStd std) {
MesXslRubberQuickTestRecord main = new MesXslRubberQuickTestRecord();
main.setRubberMaterialId(material.getId());
main.setRubberMaterialName(material.getMaterialName());
main.setStdId(std.getId());
if (oConvertUtils.isNotEmpty(std.getTestMethodId())) {
MesXslRubberQuickTestMethod method = mesXslRubberQuickTestMethodService.getById(std.getTestMethodId());
if (method != null && oConvertUtils.isNotEmpty(method.getQuickTestTypeId())) {
fillQuickTestType(main, method.getQuickTestTypeId());
}
}
return main;
}
private void fillQuickTestType(MesXslRubberQuickTestRecord main, String typeId) {
MesXslRubberQuickTestType type = mesXslRubberQuickTestTypeService.getById(typeId);
if (type != null) {
main.setQuickTestTypeId(type.getId());
main.setQuickTestTypeName(type.getTypeName());
}
}
//update-end---author:jiangxh ---date:20260525 for【MES】胶料信息多选按实验标准批量生成快检记录-----------
}

View File

@@ -6,13 +6,11 @@ import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.XslMesBizConstants;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestStd;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestStdLine;
import org.jeecg.modules.xslmes.mapper.MesXslRubberQuickTestStdLineMapper;
@@ -37,7 +35,7 @@ public class MesXslRubberQuickTestStdServiceImpl
if (oConvertUtils.isEmpty(stdName)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslRubberQuickTestStd> w = new LambdaQueryWrapper<>();
w.eq(MesXslRubberQuickTestStd::getStdName, stdName.trim());
w.and(q -> q.eq(MesXslRubberQuickTestStd::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslRubberQuickTestStd::getDelFlag));
@@ -158,25 +156,5 @@ public class MesXslRubberQuickTestStdServiceImpl
}
//update-end---author:jiangxh ---date:20260525 for【MES】原材料检验标准密炼PS批准时关联实验标准置已批准-----------
private static Integer resolveTenantId(MesXslRubberQuickTestStd context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260525 for【MES】胶料快检实验标准名称同租户唯一、主子保存-----------
}

View File

@@ -2,12 +2,10 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestType;
import org.jeecg.modules.xslmes.mapper.MesXslRubberQuickTestTypeMapper;
import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestTypeService;
@@ -22,7 +20,7 @@ public class MesXslRubberQuickTestTypeServiceImpl
//update-begin---author:jiangxh ---date:20260522 for【MES】胶料快检实验类型编号001递增、名称同租户唯一-----------
@Override
public String generateNextTypeCode(MesXslRubberQuickTestType context) {
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
Integer max = baseMapper.selectMaxNumericTypeCode(tenantId);
int next = (max == null ? 0 : max) + 1;
if (next > 999) {
@@ -36,7 +34,7 @@ public class MesXslRubberQuickTestTypeServiceImpl
if (oConvertUtils.isEmpty(typeName)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslRubberQuickTestType> w = new LambdaQueryWrapper<>();
w.eq(MesXslRubberQuickTestType::getTypeName, typeName.trim());
w.and(
@@ -84,25 +82,5 @@ public class MesXslRubberQuickTestTypeServiceImpl
}
}
private static Integer resolveTenantId(MesXslRubberQuickTestType context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260522 for【MES】胶料快检实验类型编号001递增、名称同租户唯一-----------
}

View File

@@ -2,11 +2,9 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslSparePart;
import org.jeecg.modules.xslmes.mapper.MesXslSparePartMapper;
import org.jeecg.modules.xslmes.service.IMesXslSparePartService;
@@ -21,7 +19,7 @@ public class MesXslSparePartServiceImpl extends ServiceImpl<MesXslSparePartMappe
if (oConvertUtils.isEmpty(sparePartName)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslSparePart> w = new LambdaQueryWrapper<>();
w.eq(MesXslSparePart::getSparePartName, sparePartName.trim());
w.and(
@@ -38,25 +36,5 @@ public class MesXslSparePartServiceImpl extends ServiceImpl<MesXslSparePartMappe
return this.count(w) > 0;
}
private static Integer resolveTenantId(MesXslSparePart context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260515 for【MES】备品件名称同租户不可重复仅统计未删除del_flag=0 或 null-----------
}

View File

@@ -2,11 +2,9 @@ package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslSparePartsCategory;
import org.jeecg.modules.xslmes.mapper.MesXslSparePartsCategoryMapper;
import org.jeecg.modules.xslmes.service.IMesXslSparePartsCategoryService;
@@ -22,7 +20,7 @@ public class MesXslSparePartsCategoryServiceImpl extends ServiceImpl<MesXslSpare
if (oConvertUtils.isEmpty(categoryName)) {
return false;
}
Integer tenantId = resolveTenantId(context);
Integer tenantId = MesXslTenantUtils.resolveTenantId(context != null ? context.getTenantId() : null);
LambdaQueryWrapper<MesXslSparePartsCategory> w = new LambdaQueryWrapper<>();
w.eq(MesXslSparePartsCategory::getCategoryName, categoryName.trim());
w.and(
@@ -60,25 +58,5 @@ public class MesXslSparePartsCategoryServiceImpl extends ServiceImpl<MesXslSpare
}
//update-end---author:jiangxh ---date:20260515 for【MES】备品件类别按名称+租户查询单条,供备品件信息导入解析-----------
private static Integer resolveTenantId(MesXslSparePartsCategory context) {
if (context != null && context.getTenantId() != null) {
return context.getTenantId();
}
String ts = TenantContext.getTenant();
if (oConvertUtils.isEmpty(ts)) {
try {
ts = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
if (oConvertUtils.isEmpty(ts)) {
return null;
}
try {
return Integer.parseInt(ts.trim());
} catch (NumberFormatException e) {
return null;
}
}
//update-end---author:jiangxh ---date:20260515 for【MES】备品件类别名称同租户不可重复仅统计未删除del_flag=0 或 null-----------
}

View File

@@ -5,14 +5,12 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.system.service.ISysCategoryService;
import org.jeecg.modules.xslmes.config.XslMesWarehouseAreaCapacityProperties;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.jeecg.modules.xslmes.entity.MesXslWarehouseAreaCapacityCfg;
import org.jeecg.modules.xslmes.mapper.MesXslWarehouseAreaCapacityCfgMapper;
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaCapacityCfgService;
@@ -51,14 +49,7 @@ public class MesXslWarehouseAreaCapacityCfgServiceImpl extends ServiceImpl<MesXs
}
private int resolveTenantIdBestEffort() {
String t = TenantContext.getTenant();
if (oConvertUtils.isEmpty(t)) {
try {
t = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
} catch (Exception ignored) {
}
}
return oConvertUtils.getInt(t, 0);
return MesXslTenantUtils.resolveTenantIdBestEffort();
}
/** 页面保存必须用有效租户 */

View File

@@ -0,0 +1,30 @@
package org.jeecg.modules.xslmes.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 配方日志字段级对比项
*/
@Data
@Schema(description = "配方日志对比项")
public class MesXslFormulaSpecEditChangeItemVO {
@Schema(description = "所属区块 main/line/material/step/downStep/tcu")
private String section;
@Schema(description = "区块中文名")
private String sectionLabel;
@Schema(description = "字段键")
private String fieldKey;
@Schema(description = "字段中文名")
private String fieldLabel;
@Schema(description = "变更前值")
private String beforeValue;
@Schema(description = "变更后值")
private String afterValue;
}

View File

@@ -0,0 +1,25 @@
package org.jeecg.modules.xslmes.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.jeecg.modules.xslmes.entity.MesXslFormulaSpecEditLog;
/**
* 配方日志详情(含对比项)
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "配方日志详情")
public class MesXslFormulaSpecEditLogDetailVO extends MesXslFormulaSpecEditLog {
@Schema(description = "变更前快照对象")
private Object beforeData;
@Schema(description = "变更后快照对象")
private Object afterData;
@Schema(description = "字段级对比项")
private List<MesXslFormulaSpecEditChangeItemVO> changeItems;
}

View File

@@ -0,0 +1,58 @@
package org.jeecg.modules.xslmes.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 架子车数设定(数据来源:混炼示方,不落独立业务表)
*/
@Data
@Schema(description = "架子车数设定")
public class MesXslRackTrainCountSettingVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键同混炼示方ID")
private String id;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "示方编号")
private String specCode;
@Schema(description = "混炼示方ID")
private String mixingSpecId;
@Schema(description = "机台ID")
private String machineId;
@Schema(description = "机台名称")
private String machineName;
@Schema(description = "设定车数")
private Integer setTrainCount;
@Schema(description = "状态1已设定 0未设定")
private String status;
@Schema(description = "修改人")
private String updateBy;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "修改时间")
private Date updateTime;
@Schema(description = "创建人")
private String createBy;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private Date createTime;
}

View File

@@ -0,0 +1,51 @@
package org.jeecg.modules.xslmes.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Date;
import java.util.List;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 从胶料信息批量生成快检记录(共用主表字段)
*/
@Data
@Schema(description = "胶料快检记录-从胶料批量生成")
public class MesXslRubberQuickTestRecordBatchFromMaterialVO {
@Schema(description = "胶料ID列表 mes_material.id")
private List<String> materialIds;
private String prodEquipmentLedgerId;
private String prodEquipmentName;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date productionDate;
private String trainNo;
private String workShift;
private String workTeam;
private Integer inspectTimes;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date inspectTime;
private String inspectorUserId;
private String inspectorUsername;
private String inspectorRealname;
@Schema(description = "检验类型ID mes_xsl_rubber_quick_test_type.id")
private String quickTestTypeId;
private String inspectResult;
private String productionPlanNo;
private String inspectEquipmentLedgerId;
private String inspectEquipmentName;
private String rubberCardNo;
private String rubberBatchNo;
}

View File

@@ -0,0 +1,9 @@
package org.jeecg.modules.xslmes.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecord;
@Data
@EqualsAndHashCode(callSuper = true)
public class MesXslRubberQuickTestRecordPage extends MesXslRubberQuickTestRecord {}

View File

@@ -495,9 +495,48 @@ jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubber
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubberQuickTestMethodSelectModal.vue
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubberQuickTestStdMixerPsSelectModal.vue
-- author:jiangxh---date:20260525--for: 【MES】原材料检验标准密炼PS批准时关联胶料快检实验标准置已批准反审核不回退触发由审核改为批准---
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/common/XslMesBizConstants.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestStdService.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestStdServiceImpl.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerPsCompileServiceImpl.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestStdController.java
-- author:xsl---date:20260528--for: 【IM聊天-OA】IM群聊创建群聊、群列表、群消息收发与未读 ---
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_110__sys_im_group_chat.sql
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/dto/SysImCreateGroupDTO.java
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/entity/SysImConversation.java
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImConversationVO.java
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/SysImConversationMapper.java
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/xml/SysImConversationMapper.xml
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/controller/SysImChatController.java
jeecgboot-vue3/src/views/system/im/im.api.ts
jeecgboot-vue3/src/views/system/im/ImCreateGroupModal.vue
jeecgboot-vue3/src/views/system/im/ImChat.vue
-- author:jiangxh---date:20260525--for: 【MES】胶料快检记录主子表、质量管理菜单、胶料信息批量检验生成 ---
jeecg-boot/db/mes-xsl-rubber-quick-test-record-menu-permission.sql
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_108__mes_xsl_rubber_quick_test_record.sql
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecord.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecordLine.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslRubberQuickTestRecordPage.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslRubberQuickTestRecordBatchFromMaterialVO.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRubberQuickTestRecordMapper.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRubberQuickTestRecordLineMapper.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestRecordService.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestRecordServiceImpl.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestRecordController.java
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecordList.vue
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecord.data.ts
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecord.api.ts
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/components/MesXslRubberQuickTestRecordModal.vue
jeecgboot-vue3/src/views/mes/material/MesMaterialList.vue
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_109__mes_xsl_rubber_quick_test_record_no.sql
-- author:jiangxh---date:20260528--for: 【MES】胶料快检记录单号、无弹窗直接生成、列表去掉新增、明细不可增删 ---
jeecg-boot/db/mes-xsl-rubber-quick-test-record-menu-permission.sql
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecord.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestRecordService.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestRecordServiceImpl.java
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecordList.vue
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecord.data.ts
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/components/MesXslRubberQuickTestRecordModal.vue
jeecgboot-vue3/src/views/mes/material/MesMaterialList.vue

View File

@@ -0,0 +1,225 @@
package org.jeecg.modules.im.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.jeecg.modules.im.vo.SysImMessageVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.im.dto.SysImCreateGroupDTO;
import org.jeecg.modules.im.dto.SysImGroupMembersDTO;
import org.jeecg.modules.im.dto.SysImSendMessageDTO;
import org.jeecg.modules.im.service.ISysImChatService;
import org.jeecg.modules.im.vo.SysImContactVO;
import org.jeecg.modules.im.vo.SysImConversationVO;
import org.jeecg.modules.im.vo.SysImGroupDetailVO;
import org.jeecg.modules.im.vo.SysImMessageVO;
import org.jeecg.modules.system.entity.SysUser;
import org.jeecg.modules.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
/**
* 租户 IM 聊天
*/
@Tag(name = "IM聊天")
@RestController
@RequestMapping("/sys/im/chat")
@Slf4j
public class SysImChatController {
@Autowired
private ISysImChatService imChatService;
@Autowired
private ISysUserService sysUserService;
private LoginUser currentUser() {
return (LoginUser) SecurityUtils.getSubject().getPrincipal();
}
private Integer resolveTenantId(LoginUser user, HttpServletRequest request) {
Integer tenantId = oConvertUtils.getInt(TokenUtils.getTenantIdByRequest(request), 0);
if (tenantId != null && tenantId > 0) {
return tenantId;
}
SysUser sysUser = sysUserService.getById(user.getId());
if (sysUser != null && sysUser.getLoginTenantId() != null && sysUser.getLoginTenantId() > 0) {
return sysUser.getLoginTenantId();
}
return 0;
}
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】会话列表接口-----------
@Operation(summary = "IM聊天-会话列表")
@RequiresPermissions("sys:im:chat:list")
@GetMapping("/conversations")
public Result<List<SysImConversationVO>> conversations(HttpServletRequest request) {
LoginUser user = currentUser();
return Result.OK(imChatService.listConversations(user.getId(), resolveTenantId(user, request)));
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】会话列表接口-----------
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】打开单聊接口-----------
@Operation(summary = "IM聊天-打开单聊")
@RequiresPermissions("sys:im:chat:list")
@PostMapping("/open")
public Result<SysImConversationVO> open(@RequestParam(name = "targetUserId") String targetUserId, HttpServletRequest request) {
LoginUser user = currentUser();
return Result.OK(imChatService.openSingleConversation(user.getId(), resolveTenantId(user, request), user.getOrgCode(), targetUserId));
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】打开单聊接口-----------
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】消息列表接口-----------
@Operation(summary = "IM聊天-消息列表")
@RequiresPermissions("sys:im:chat:list")
@GetMapping("/messages")
public Result<IPage<SysImMessageVO>> messages(
@RequestParam(name = "conversationId") String conversationId,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "20") Integer pageSize,
@RequestParam(name = "startTime", required = false) String startTime,
@RequestParam(name = "beforeTime", required = false) String beforeTime) {
LoginUser user = currentUser();
return Result.OK(imChatService.listMessages(user.getId(), conversationId, pageNo, pageSize, startTime, beforeTime));
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】消息列表接口-----------
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】发送消息接口-----------
@Operation(summary = "IM聊天-发送消息")
@RequiresPermissions("sys:im:chat:send")
@PostMapping("/send")
public Result<SysImMessageVO> send(@RequestBody SysImSendMessageDTO dto, HttpServletRequest request) {
LoginUser user = currentUser();
return Result.OK(imChatService.sendMessage(user.getId(), resolveTenantId(user, request), dto));
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】发送消息接口-----------
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】标记已读接口-----------
@Operation(summary = "IM聊天-标记已读")
@RequiresPermissions("sys:im:chat:list")
@PutMapping("/read")
public Result<String> read(@RequestParam(name = "conversationId") String conversationId) {
LoginUser user = currentUser();
imChatService.markRead(user.getId(), conversationId);
return Result.OK();
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】标记已读接口-----------
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】本部门成员接口-----------
@Operation(summary = "IM聊天-本部门成员")
@RequiresPermissions("sys:im:chat:list")
@GetMapping("/deptMembers")
public Result<List<SysImContactVO>> deptMembers(@RequestParam(name = "keyword", required = false) String keyword, HttpServletRequest request) {
LoginUser user = currentUser();
return Result.OK(imChatService.listDeptMembers(user.getId(), resolveTenantId(user, request), user.getOrgCode(), keyword));
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】本部门成员接口-----------
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】联系人接口兼容保留同本部门-----------
@Operation(summary = "IM聊天-租户联系人")
@RequiresPermissions("sys:im:chat:list")
@GetMapping("/contacts")
public Result<List<SysImContactVO>> contacts(@RequestParam(name = "keyword", required = false) String keyword, HttpServletRequest request) {
LoginUser user = currentUser();
return Result.OK(imChatService.listDeptMembers(user.getId(), resolveTenantId(user, request), user.getOrgCode(), keyword));
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】联系人接口兼容保留同本部门-----------
//update-begin---author:xsl ---date:20260528 for【IM聊天-OA】群聊接口-----------
@Operation(summary = "IM聊天-群聊列表")
@RequiresPermissions("sys:im:chat:list")
@GetMapping("/groups")
public Result<List<SysImConversationVO>> groups(HttpServletRequest request) {
LoginUser user = currentUser();
return Result.OK(imChatService.listGroupConversations(user.getId(), resolveTenantId(user, request)));
}
@Operation(summary = "IM聊天-创建群聊")
@RequiresPermissions("sys:im:chat:group")
@PostMapping("/group/create")
public Result<SysImConversationVO> createGroup(@RequestBody SysImCreateGroupDTO dto, HttpServletRequest request) {
LoginUser user = currentUser();
return Result.OK(imChatService.createGroupConversation(user.getId(), resolveTenantId(user, request), user.getOrgCode(), dto));
}
//update-end---author:xsl ---date:20260528 for【IM聊天-OA】群聊接口-----------
//update-begin---author:cursor ---date:20260529 for【IM聊天-OA】群设置-群管理接口-----------
@Operation(summary = "IM聊天-群聊详情")
@RequiresPermissions("sys:im:chat:list")
@GetMapping("/group/detail")
public Result<SysImGroupDetailVO> groupDetail(@RequestParam(name = "conversationId") String conversationId) {
LoginUser user = currentUser();
return Result.OK(imChatService.getGroupDetail(user.getId(), conversationId));
}
@Operation(summary = "IM聊天-添加群成员")
@RequiresPermissions("sys:im:chat:list")
@PostMapping("/group/addMembers")
public Result<SysImConversationVO> addGroupMembers(@RequestBody SysImGroupMembersDTO dto, HttpServletRequest request) {
LoginUser user = currentUser();
return Result.OK(imChatService.addGroupMembers(user.getId(), resolveTenantId(user, request), user.getOrgCode(), dto));
}
@Operation(summary = "IM聊天-移除群成员")
@RequiresPermissions("sys:im:chat:list")
@PostMapping("/group/removeMember")
public Result<String> removeGroupMember(@RequestParam(name = "conversationId") String conversationId,
@RequestParam(name = "memberUserId") String memberUserId) {
LoginUser user = currentUser();
imChatService.removeGroupMember(user.getId(), conversationId, memberUserId);
return Result.OK("移除成功");
}
@Operation(summary = "IM聊天-修改群名称")
@RequiresPermissions("sys:im:chat:list")
@PostMapping("/group/rename")
public Result<SysImConversationVO> renameGroup(@RequestParam(name = "conversationId") String conversationId,
@RequestParam(name = "groupName") String groupName) {
LoginUser user = currentUser();
return Result.OK(imChatService.renameGroup(user.getId(), conversationId, groupName));
}
@Operation(summary = "IM聊天-转让群主")
@RequiresPermissions("sys:im:chat:list")
@PostMapping("/group/transfer")
public Result<String> transferGroupOwner(@RequestParam(name = "conversationId") String conversationId,
@RequestParam(name = "newOwnerId") String newOwnerId) {
LoginUser user = currentUser();
imChatService.transferGroupOwner(user.getId(), conversationId, newOwnerId);
return Result.OK("转让成功");
}
@Operation(summary = "IM聊天-退出群聊")
@RequiresPermissions("sys:im:chat:list")
@PostMapping("/group/quit")
public Result<String> quitGroup(@RequestParam(name = "conversationId") String conversationId) {
LoginUser user = currentUser();
imChatService.quitGroup(user.getId(), conversationId);
return Result.OK("已退出群聊");
}
@Operation(summary = "IM聊天-解散群聊")
@RequiresPermissions("sys:im:chat:list")
@PostMapping("/group/dismiss")
public Result<String> dismissGroup(@RequestParam(name = "conversationId") String conversationId) {
LoginUser user = currentUser();
imChatService.dismissGroup(user.getId(), conversationId);
return Result.OK("群聊已解散");
}
//update-end---author:cursor ---date:20260529 for【IM聊天-OA】群设置-群管理接口-----------
}

View File

@@ -0,0 +1,20 @@
package org.jeecg.modules.im.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 创建 IM 群聊
*/
@Data
@Schema(description = "创建IM群聊")
public class SysImCreateGroupDTO {
@Schema(description = "群名称")
private String groupName;
@Schema(description = "群成员用户ID不含本人时可自动加入创建人")
private List<String> memberUserIds;
}

View File

@@ -0,0 +1,22 @@
package org.jeecg.modules.im.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* IM 群聊添加成员
*/
//update-begin---author:cursor ---date:20260529 for【IM聊天-OA】群设置-添加群成员-----------
@Data
@Schema(description = "IM群聊添加成员")
public class SysImGroupMembersDTO {
@Schema(description = "会话ID")
private String conversationId;
@Schema(description = "新增成员用户ID")
private List<String> memberUserIds;
}
//update-end---author:cursor ---date:20260529 for【IM聊天-OA】群设置-添加群成员-----------

View File

@@ -0,0 +1,19 @@
package org.jeecg.modules.im.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 发送 IM 消息 DTO
*/
@Data
@Schema(description = "发送IM消息")
public class SysImSendMessageDTO {
@Schema(description = "会话ID")
private String conversationId;
@Schema(description = "消息内容")
private String content;
@Schema(description = "消息类型 text/image/file")
private String msgType;
}

View File

@@ -0,0 +1,47 @@
package org.jeecg.modules.im.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* IM 会话
*/
@Data
@TableName("sys_im_conversation")
public class SysImConversation implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private String id;
/** 会话类型 single单聊 group群聊 */
private String convType;
/** 单聊唯一键 */
private String userPairKey;
/** 群名称 */
private String groupName;
/** 群主用户ID */
private String ownerId;
/** 租户ID */
private Integer tenantId;
/** 最后一条消息摘要 */
private String lastContent;
/** 最后消息时间 */
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastTime;
private String createBy;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String updateBy;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}

View File

@@ -0,0 +1,32 @@
package org.jeecg.modules.im.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* IM 会话成员
*/
@Data
@TableName("sys_im_conversation_member")
public class SysImConversationMember implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private String id;
private String conversationId;
private String userId;
private Integer unreadCount;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastReadTime;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}

View File

@@ -0,0 +1,32 @@
package org.jeecg.modules.im.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* IM 消息
*/
@Data
@TableName("sys_im_message")
public class SysImMessage implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private String id;
private String conversationId;
private String senderId;
private String content;
/** 消息类型 text/image/file */
private String msgType;
private Integer tenantId;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}

View File

@@ -0,0 +1,24 @@
package org.jeecg.modules.im.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.jeecg.modules.im.entity.SysImConversation;
import org.jeecg.modules.im.vo.SysImConversationVO;
import java.util.List;
/**
* IM 会话 Mapper
*/
public interface SysImConversationMapper extends BaseMapper<SysImConversation> {
/**
* 查询当前用户的会话列表
*/
List<SysImConversationVO> listMyConversations(@Param("userId") String userId, @Param("tenantId") Integer tenantId);
/**
* 查询当前用户的群聊列表
*/
List<SysImConversationVO> listMyGroupConversations(@Param("userId") String userId, @Param("tenantId") Integer tenantId);
}

View File

@@ -0,0 +1,16 @@
package org.jeecg.modules.im.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.jeecg.modules.im.entity.SysImConversationMember;
/**
* IM 会话成员 Mapper
*/
public interface SysImConversationMemberMapper extends BaseMapper<SysImConversationMember> {
/**
* 未读数 +1排除发送人
*/
int incrementUnreadExceptSender(@Param("conversationId") String conversationId, @Param("senderId") String senderId);
}

View File

@@ -0,0 +1,10 @@
package org.jeecg.modules.im.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.im.entity.SysImMessage;
/**
* IM 消息 Mapper
*/
public interface SysImMessageMapper extends BaseMapper<SysImMessage> {
}

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.im.mapper.SysImConversationMapper">
<select id="listMyConversations" resultType="org.jeecg.modules.im.vo.SysImConversationVO">
SELECT
c.id AS conversationId,
c.conv_type AS convType,
c.last_content AS lastContent,
c.last_time AS lastTime,
m.unread_count AS unreadCount,
u.id AS targetUserId,
u.realname AS targetRealname,
u.username AS targetUsername,
u.avatar AS targetAvatar
FROM sys_im_conversation_member m
INNER JOIN sys_im_conversation c ON c.id = m.conversation_id
INNER JOIN sys_im_conversation_member om ON om.conversation_id = c.id AND om.user_id != m.user_id
INNER JOIN sys_user u ON u.id = om.user_id
WHERE m.user_id = #{userId}
AND c.tenant_id = #{tenantId}
AND c.conv_type = 'single'
ORDER BY c.last_time DESC
</select>
<select id="listMyGroupConversations" resultType="org.jeecg.modules.im.vo.SysImConversationVO">
SELECT
c.id AS conversationId,
c.conv_type AS convType,
c.group_name AS groupName,
c.owner_id AS ownerId,
c.last_content AS lastContent,
c.last_time AS lastTime,
m.unread_count AS unreadCount,
(
SELECT COUNT(1)
FROM sys_im_conversation_member gm
WHERE gm.conversation_id = c.id
) AS memberCount
FROM sys_im_conversation_member m
INNER JOIN sys_im_conversation c ON c.id = m.conversation_id
WHERE m.user_id = #{userId}
AND c.tenant_id = #{tenantId}
AND c.conv_type = 'group'
ORDER BY c.last_time DESC
</select>
</mapper>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.im.mapper.SysImConversationMemberMapper">
<update id="incrementUnreadExceptSender">
UPDATE sys_im_conversation_member
SET unread_count = unread_count + 1
WHERE conversation_id = #{conversationId}
AND user_id != #{senderId}
</update>
</mapper>

View File

@@ -0,0 +1,83 @@
package org.jeecg.modules.im.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.jeecg.modules.im.dto.SysImCreateGroupDTO;
import org.jeecg.modules.im.dto.SysImGroupMembersDTO;
import org.jeecg.modules.im.dto.SysImSendMessageDTO;
import org.jeecg.modules.im.vo.SysImContactVO;
import org.jeecg.modules.im.vo.SysImConversationVO;
import org.jeecg.modules.im.vo.SysImGroupDetailVO;
import org.jeecg.modules.im.vo.SysImMessageVO;
import java.util.List;
/**
* IM 聊天服务
*/
public interface ISysImChatService {
List<SysImConversationVO> listConversations(String userId, Integer tenantId);
SysImConversationVO openSingleConversation(String userId, Integer tenantId, String orgCode, String targetUserId);
List<SysImConversationVO> listGroupConversations(String userId, Integer tenantId);
SysImConversationVO createGroupConversation(String userId, Integer tenantId, String orgCode, SysImCreateGroupDTO dto);
IPage<SysImMessageVO> listMessages(String userId, String conversationId, Integer pageNo, Integer pageSize, String startTime, String beforeTime);
SysImMessageVO sendMessage(String userId, Integer tenantId, SysImSendMessageDTO dto);
void markRead(String userId, String conversationId);
List<SysImContactVO> listDeptMembers(String userId, Integer tenantId, String orgCode, String keyword);
//update-begin---author:cursor ---date:20260529 for【IM聊天-OA】群设置-群管理接口-----------
/** 群聊详情(含成员列表,区分群主) */
SysImGroupDetailVO getGroupDetail(String userId, String conversationId);
/** 添加群成员(所有群成员可操作) */
SysImConversationVO addGroupMembers(String userId, Integer tenantId, String orgCode, SysImGroupMembersDTO dto);
/** 移除群成员(仅群主) */
void removeGroupMember(String userId, String conversationId, String memberUserId);
/** 修改群名称(仅群主) */
SysImConversationVO renameGroup(String userId, String conversationId, String groupName);
/** 转让群主(仅群主) */
void transferGroupOwner(String userId, String conversationId, String newOwnerId);
/** 退出群聊(非群主成员) */
void quitGroup(String userId, String conversationId);
/** 解散群聊(仅群主) */
void dismissGroup(String userId, String conversationId);
//update-end---author:cursor ---date:20260529 for【IM聊天-OA】群设置-群管理接口-----------
}

View File

@@ -0,0 +1,859 @@
package org.jeecg.modules.im.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.WebsocketConst;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.im.dto.SysImCreateGroupDTO;
import org.jeecg.modules.im.dto.SysImGroupMembersDTO;
import org.jeecg.modules.im.dto.SysImSendMessageDTO;
import org.jeecg.modules.im.entity.SysImConversation;
import org.jeecg.modules.im.entity.SysImConversationMember;
import org.jeecg.modules.im.entity.SysImMessage;
import org.jeecg.modules.im.mapper.SysImConversationMapper;
import org.jeecg.modules.im.mapper.SysImConversationMemberMapper;
import org.jeecg.modules.im.mapper.SysImMessageMapper;
import org.jeecg.modules.im.service.ISysImChatService;
import org.jeecg.modules.im.vo.SysImContactVO;
import org.jeecg.modules.im.vo.SysImConversationVO;
import org.jeecg.modules.im.vo.SysImGroupDetailVO;
import org.jeecg.modules.im.vo.SysImGroupMemberVO;
import org.jeecg.modules.im.vo.SysImMessageVO;
import org.jeecg.modules.message.websocket.WebSocket;
import org.jeecg.modules.system.entity.SysDepart;
import org.jeecg.modules.system.entity.SysPermission;
import org.jeecg.modules.system.entity.SysUser;
import org.jeecg.modules.system.entity.SysUserDepart;
import org.jeecg.modules.system.mapper.SysDepartMapper;
import org.jeecg.modules.system.mapper.SysUserDepartMapper;
import org.jeecg.modules.system.mapper.SysUserMapper;
import org.jeecg.modules.system.mapper.SysUserTenantMapper;
import org.jeecg.modules.system.service.ISysPermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
/**
* IM 聊天服务实现
*/
@Slf4j
@Service
public class SysImChatServiceImpl implements ISysImChatService {
private static final String CONV_TYPE_SINGLE = "single";
private static final String CONV_TYPE_GROUP = "group";
private static final String MSG_TYPE_TEXT = "text";
private static final String MSG_TYPE_IMAGE = "image";
private static final String MSG_TYPE_BIZ_RECORD = "biz_record";
private static final String MSG_IMAGE_PREVIEW = "[图片]";
private static final String MSG_BIZ_RECORD_PREVIEW = "[业务数据]";
private static final String IM_RECORD_QUERY_KEY = "imRecordId";
@Autowired
private ISysPermissionService sysPermissionService;
@Autowired
private SysImConversationMapper conversationMapper;
@Autowired
private SysImConversationMemberMapper memberMapper;
@Autowired
private SysImMessageMapper messageMapper;
@Autowired
private SysUserMapper userMapper;
@Autowired
private SysUserTenantMapper userTenantMapper;
@Autowired
private SysUserDepartMapper userDepartMapper;
@Autowired
private SysDepartMapper departMapper;
@Autowired
private WebSocket webSocket;
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】会话列表-----------
@Override
public List<SysImConversationVO> listConversations(String userId, Integer tenantId) {
if (oConvertUtils.isEmpty(userId) || tenantId == null || tenantId <= 0) {
return Collections.emptyList();
}
return conversationMapper.listMyConversations(userId, tenantId);
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】会话列表-----------
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】打开单聊会话-----------
@Override
@Transactional(rollbackFor = Exception.class)
public SysImConversationVO openSingleConversation(String userId, Integer tenantId, String orgCode, String targetUserId) {
String pairKey = buildPairKey(userId, targetUserId);
SysImConversation conversation = conversationMapper.selectOne(new LambdaQueryWrapper<SysImConversation>()
.eq(SysImConversation::getTenantId, tenantId)
.eq(SysImConversation::getUserPairKey, pairKey));
Date now = new Date();
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】已有会话快速打开-----------
if (conversation != null) {
return buildConversationVo(conversation, userId, targetUserId);
}
validateTenantChat(userId, tenantId, orgCode, targetUserId);
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】已有会话快速打开-----------
conversation = new SysImConversation();
conversation.setConvType(CONV_TYPE_SINGLE);
conversation.setUserPairKey(pairKey);
conversation.setTenantId(tenantId);
conversation.setCreateBy(userId);
conversation.setCreateTime(now);
conversation.setUpdateTime(now);
conversationMapper.insert(conversation);
createMember(conversation.getId(), userId, now);
createMember(conversation.getId(), targetUserId, now);
return buildConversationVo(conversation.getId(), userId, tenantId, targetUserId);
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】打开单聊会话-----------
//update-begin---author:xsl ---date:20260528 for【IM聊天-OA】群聊会话列表与创建-----------
@Override
public List<SysImConversationVO> listGroupConversations(String userId, Integer tenantId) {
if (oConvertUtils.isEmpty(userId) || tenantId == null || tenantId <= 0) {
return Collections.emptyList();
}
return conversationMapper.listMyGroupConversations(userId, tenantId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public SysImConversationVO createGroupConversation(String userId, Integer tenantId, String orgCode, SysImCreateGroupDTO dto) {
if (dto == null || oConvertUtils.isEmpty(dto.getGroupName())) {
throw new JeecgBootException("群名称不能为空");
}
String groupName = dto.getGroupName().trim();
if (groupName.length() > 30) {
throw new JeecgBootException("群名称不能超过30字");
}
List<String> memberIds = normalizeGroupMemberIds(userId, dto.getMemberUserIds());
if (memberIds.size() < 2) {
throw new JeecgBootException("群聊至少需要2名成员");
}
if (memberIds.size() > 50) {
throw new JeecgBootException("群成员不能超过50人");
}
for (String memberId : memberIds) {
if (userId.equals(memberId)) {
continue;
}
validateTenantChat(userId, tenantId, orgCode, memberId);
}
Date now = new Date();
SysImConversation conversation = new SysImConversation();
conversation.setConvType(CONV_TYPE_GROUP);
conversation.setGroupName(groupName);
conversation.setOwnerId(userId);
conversation.setTenantId(tenantId);
conversation.setCreateBy(userId);
conversation.setCreateTime(now);
conversation.setUpdateTime(now);
conversationMapper.insert(conversation);
for (String memberId : memberIds) {
createMember(conversation.getId(), memberId, now);
}
return buildGroupConversationVo(conversation, userId);
}
private List<String> normalizeGroupMemberIds(String creatorId, List<String> memberUserIds) {
Set<String> memberIds = new LinkedHashSet<>();
memberIds.add(creatorId);
if (memberUserIds != null) {
for (String memberId : memberUserIds) {
if (oConvertUtils.isNotEmpty(memberId)) {
memberIds.add(memberId);
}
}
}
return new ArrayList<>(memberIds);
}
private SysImConversationVO buildGroupConversationVo(SysImConversation conversation, String userId) {
SysImConversationMember member = getMember(userId, conversation.getId());
SysImConversationVO vo = new SysImConversationVO();
vo.setConversationId(conversation.getId());
vo.setConvType(CONV_TYPE_GROUP);
vo.setGroupName(conversation.getGroupName());
vo.setOwnerId(conversation.getOwnerId());
vo.setLastContent(conversation.getLastContent());
vo.setLastTime(conversation.getLastTime());
vo.setUnreadCount(member == null ? 0 : member.getUnreadCount());
Long memberCount = memberMapper.selectCount(new LambdaQueryWrapper<SysImConversationMember>()
.eq(SysImConversationMember::getConversationId, conversation.getId()));
vo.setMemberCount(memberCount == null ? 0 : memberCount.intValue());
return vo;
}
//update-end---author:xsl ---date:20260528 for【IM聊天-OA】群聊会话列表与创建-----------
//update-begin---author:cursor ---date:20260529 for【IM聊天-OA】群设置-群管理-----------
private static final int GROUP_MEMBER_MAX = 50;
@Override
public SysImGroupDetailVO getGroupDetail(String userId, String conversationId) {
SysImConversation conversation = assertGroupConversation(userId, conversationId);
List<SysImConversationMember> members = memberMapper.selectList(new LambdaQueryWrapper<SysImConversationMember>()
.eq(SysImConversationMember::getConversationId, conversationId)
.orderByAsc(SysImConversationMember::getCreateTime));
List<String> memberUserIds = members.stream()
.map(SysImConversationMember::getUserId)
.filter(oConvertUtils::isNotEmpty)
.distinct()
.collect(Collectors.toList());
Map<String, SysUser> userMap = new HashMap<>(memberUserIds.size());
if (!memberUserIds.isEmpty()) {
List<SysUser> users = userMapper.selectBatchIds(memberUserIds);
if (users != null) {
for (SysUser user : users) {
userMap.put(user.getId(), user);
}
}
}
String ownerId = conversation.getOwnerId();
List<SysImGroupMemberVO> memberVoList = new ArrayList<>(members.size());
for (SysImConversationMember member : members) {
SysUser user = userMap.get(member.getUserId());
SysImGroupMemberVO memberVo = new SysImGroupMemberVO();
memberVo.setUserId(member.getUserId());
memberVo.setOwner(member.getUserId() != null && member.getUserId().equals(ownerId));
if (user != null) {
memberVo.setRealname(user.getRealname());
memberVo.setUsername(user.getUsername());
memberVo.setAvatar(user.getAvatar());
}
memberVoList.add(memberVo);
}
// 群主排在最前
memberVoList.sort(Comparator.comparingInt(item -> Boolean.TRUE.equals(item.getOwner()) ? 0 : 1));
SysImGroupDetailVO detail = new SysImGroupDetailVO();
detail.setConversationId(conversation.getId());
detail.setGroupName(conversation.getGroupName());
detail.setOwnerId(ownerId);
detail.setMemberCount(memberVoList.size());
detail.setOwner(userId.equals(ownerId));
detail.setMembers(memberVoList);
return detail;
}
@Override
@Transactional(rollbackFor = Exception.class)
public SysImConversationVO addGroupMembers(String userId, Integer tenantId, String orgCode, SysImGroupMembersDTO dto) {
if (dto == null || oConvertUtils.isEmpty(dto.getConversationId())) {
throw new JeecgBootException("会话不存在");
}
SysImConversation conversation = assertGroupConversation(userId, dto.getConversationId());
if (dto.getMemberUserIds() == null || dto.getMemberUserIds().isEmpty()) {
throw new JeecgBootException("请选择要添加的成员");
}
// 已在群成员
Set<String> existIds = memberMapper.selectList(new LambdaQueryWrapper<SysImConversationMember>()
.eq(SysImConversationMember::getConversationId, conversation.getId()))
.stream().map(SysImConversationMember::getUserId).collect(Collectors.toSet());
// 去重并过滤已存在成员
List<String> toAdd = new ArrayList<>();
Set<String> seen = new LinkedHashSet<>();
for (String memberId : dto.getMemberUserIds()) {
if (oConvertUtils.isEmpty(memberId) || existIds.contains(memberId) || !seen.add(memberId)) {
continue;
}
toAdd.add(memberId);
}
if (toAdd.isEmpty()) {
throw new JeecgBootException("所选成员已在群内");
}
if (existIds.size() + toAdd.size() > GROUP_MEMBER_MAX) {
throw new JeecgBootException("群成员不能超过" + GROUP_MEMBER_MAX + "");
}
// 校验同租户、同部门
for (String memberId : toAdd) {
validateTenantChat(userId, tenantId, orgCode, memberId);
}
Date now = new Date();
for (String memberId : toAdd) {
createMember(conversation.getId(), memberId, now);
}
return buildGroupConversationVo(conversation, userId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeGroupMember(String userId, String conversationId, String memberUserId) {
SysImConversation conversation = assertGroupConversation(userId, conversationId);
assertGroupOwner(userId, conversation);
if (oConvertUtils.isEmpty(memberUserId)) {
throw new JeecgBootException("请选择要移除的成员");
}
if (memberUserId.equals(conversation.getOwnerId())) {
throw new JeecgBootException("群主不能被移除");
}
memberMapper.delete(new LambdaQueryWrapper<SysImConversationMember>()
.eq(SysImConversationMember::getConversationId, conversationId)
.eq(SysImConversationMember::getUserId, memberUserId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public SysImConversationVO renameGroup(String userId, String conversationId, String groupName) {
SysImConversation conversation = assertGroupConversation(userId, conversationId);
assertGroupOwner(userId, conversation);
if (oConvertUtils.isEmpty(groupName)) {
throw new JeecgBootException("群名称不能为空");
}
String name = groupName.trim();
if (name.length() > 30) {
throw new JeecgBootException("群名称不能超过30字");
}
conversation.setGroupName(name);
conversation.setUpdateBy(userId);
conversation.setUpdateTime(new Date());
conversationMapper.updateById(conversation);
return buildGroupConversationVo(conversation, userId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void transferGroupOwner(String userId, String conversationId, String newOwnerId) {
SysImConversation conversation = assertGroupConversation(userId, conversationId);
assertGroupOwner(userId, conversation);
if (oConvertUtils.isEmpty(newOwnerId)) {
throw new JeecgBootException("请选择新群主");
}
if (newOwnerId.equals(userId)) {
throw new JeecgBootException("不能转让给自己");
}
if (getMember(newOwnerId, conversationId) == null) {
throw new JeecgBootException("新群主必须是群成员");
}
conversation.setOwnerId(newOwnerId);
conversation.setUpdateBy(userId);
conversation.setUpdateTime(new Date());
conversationMapper.updateById(conversation);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void quitGroup(String userId, String conversationId) {
SysImConversation conversation = assertGroupConversation(userId, conversationId);
if (userId.equals(conversation.getOwnerId())) {
throw new JeecgBootException("群主不能退出群聊,请先转让群主或解散群聊");
}
memberMapper.delete(new LambdaQueryWrapper<SysImConversationMember>()
.eq(SysImConversationMember::getConversationId, conversationId)
.eq(SysImConversationMember::getUserId, userId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void dismissGroup(String userId, String conversationId) {
SysImConversation conversation = assertGroupConversation(userId, conversationId);
assertGroupOwner(userId, conversation);
memberMapper.delete(new LambdaQueryWrapper<SysImConversationMember>()
.eq(SysImConversationMember::getConversationId, conversationId));
conversationMapper.deleteById(conversationId);
}
/** 校验会话存在、为群聊且当前用户是群成员 */
private SysImConversation assertGroupConversation(String userId, String conversationId) {
if (oConvertUtils.isEmpty(conversationId)) {
throw new JeecgBootException("会话不存在");
}
SysImConversation conversation = conversationMapper.selectById(conversationId);
if (conversation == null || !CONV_TYPE_GROUP.equals(conversation.getConvType())) {
throw new JeecgBootException("群聊不存在");
}
assertMember(userId, conversationId);
return conversation;
}
/** 校验当前用户是群主 */
private void assertGroupOwner(String userId, SysImConversation conversation) {
if (!userId.equals(conversation.getOwnerId())) {
throw new JeecgBootException("仅群主可执行该操作");
}
}
//update-end---author:cursor ---date:20260529 for【IM聊天-OA】群设置-群管理-----------
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】消息分页-----------
@Override
public IPage<SysImMessageVO> listMessages(String userId, String conversationId, Integer pageNo, Integer pageSize, String startTime, String beforeTime) {
assertMember(userId, conversationId);
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】默认本周消息+向上翻页加载-----------
Date start = parseMessageTime(startTime);
Date before = parseMessageTime(beforeTime);
int currentPage = (start != null || before != null) ? 1 : (pageNo == null || pageNo < 1 ? 1 : pageNo);
int size = pageSize == null || pageSize < 1 ? 20 : pageSize;
Page<SysImMessage> page = new Page<>(currentPage, size);
if (currentPage == 1) {
page.setSearchCount(false);
}
LambdaQueryWrapper<SysImMessage> wrapper = new LambdaQueryWrapper<SysImMessage>()
.eq(SysImMessage::getConversationId, conversationId);
if (start != null) {
wrapper.ge(SysImMessage::getCreateTime, start);
}
if (before != null) {
wrapper.lt(SysImMessage::getCreateTime, before);
}
wrapper.orderByDesc(SysImMessage::getCreateTime);
IPage<SysImMessage> messagePage = messageMapper.selectPage(page, wrapper);
Page<SysImMessageVO> voPage = new Page<>(currentPage, size, messagePage.getTotal());
List<SysImMessageVO> records = toMessageVoList(messagePage.getRecords(), userId);
Collections.reverse(records);
voPage.setRecords(records);
return voPage;
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】默认本周消息+向上翻页加载-----------
}
private Date parseMessageTime(String timeText) {
if (oConvertUtils.isEmpty(timeText)) {
return null;
}
Date date = DateUtils.parseDatetime(timeText);
if (date == null) {
date = DateUtils.str2Date(timeText, DateUtils.date_sdf.get());
}
return date;
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】消息分页-----------
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】发送消息-----------
@Override
@Transactional(rollbackFor = Exception.class)
public SysImMessageVO sendMessage(String userId, Integer tenantId, SysImSendMessageDTO dto) {
if (dto == null || oConvertUtils.isEmpty(dto.getConversationId()) || oConvertUtils.isEmpty(dto.getContent())) {
throw new JeecgBootException("消息内容不能为空");
}
assertMember(userId, dto.getConversationId());
Date now = new Date();
SysImMessage message = new SysImMessage();
message.setConversationId(dto.getConversationId());
message.setSenderId(userId);
message.setContent(dto.getContent().trim());
message.setMsgType(oConvertUtils.isEmpty(dto.getMsgType()) ? MSG_TYPE_TEXT : dto.getMsgType());
message.setTenantId(tenantId);
message.setCreateTime(now);
messageMapper.insert(message);
SysImConversation conversation = conversationMapper.selectById(dto.getConversationId());
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】图片消息会话摘要-----------
String lastContent = resolveLastContent(message.getMsgType(), message.getContent());
if (CONV_TYPE_GROUP.equals(conversation.getConvType())) {
SysUser sender = userMapper.selectById(userId);
String senderName = sender != null ? oConvertUtils.getString(sender.getRealname(), sender.getUsername()) : "";
if (oConvertUtils.isNotEmpty(senderName)) {
lastContent = senderName + ": " + lastContent;
}
}
conversation.setLastContent(truncate(lastContent, 200));
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】图片消息会话摘要-----------
conversation.setLastTime(now);
conversation.setUpdateTime(now);
conversationMapper.updateById(conversation);
memberMapper.incrementUnreadExceptSender(dto.getConversationId(), userId);
SysImMessageVO messageVo = toMessageVo(message, userId);
fillBizRecordReceiverPermission(messageVo, message, userId, new HashMap<>(4));
pushChatMessage(dto.getConversationId(), userId, messageVo, conversation.getConvType());
return messageVo;
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】发送消息-----------
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】标记已读-----------
@Override
@Transactional(rollbackFor = Exception.class)
public void markRead(String userId, String conversationId) {
SysImConversationMember member = getMember(userId, conversationId);
if (member == null) {
return;
}
member.setUnreadCount(0);
member.setLastReadTime(new Date());
memberMapper.updateById(member);
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】标记已读-----------
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】本部门成员列表含会话摘要-----------
@Override
public List<SysImContactVO> listDeptMembers(String userId, Integer tenantId, String orgCode, String keyword) {
String resolvedOrgCode = resolveOrgCode(userId, tenantId, orgCode);
if (oConvertUtils.isEmpty(resolvedOrgCode)) {
throw new JeecgBootException("未获取到当前部门,请切换部门后重试");
}
List<SysUser> users = userDepartMapper.querySameDepartUserList(resolvedOrgCode, userId, tenantId, keyword);
if (users == null || users.isEmpty()) {
return Collections.emptyList();
}
Map<String, SysImConversationVO> convMap = new HashMap<>(16);
if (tenantId != null && tenantId > 0) {
for (SysImConversationVO conv : conversationMapper.listMyConversations(userId, tenantId)) {
if (oConvertUtils.isNotEmpty(conv.getTargetUserId())) {
convMap.put(conv.getTargetUserId(), conv);
}
}
}
List<SysImContactVO> result = users.stream().map(user -> {
SysImContactVO vo = toContactVo(user);
SysImConversationVO conv = convMap.get(user.getId());
if (conv != null) {
vo.setConversationId(conv.getConversationId());
vo.setLastContent(conv.getLastContent());
vo.setLastTime(conv.getLastTime());
vo.setUnreadCount(conv.getUnreadCount());
}
return vo;
}).collect(Collectors.toList());
result.sort(Comparator
.comparing(SysImContactVO::getLastTime, Comparator.nullsLast(Comparator.reverseOrder()))
.thenComparing(item -> oConvertUtils.getString(item.getRealname(), item.getUsername())));
return result;
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】本部门成员列表含会话摘要-----------
private SysImContactVO toContactVo(SysUser user) {
SysImContactVO vo = new SysImContactVO();
vo.setId(user.getId());
vo.setUsername(user.getUsername());
vo.setRealname(user.getRealname());
vo.setAvatar(user.getAvatar());
vo.setOrgCodeTxt(user.getOrgCodeTxt());
vo.setUnreadCount(0);
return vo;
}
private String resolveOrgCode(String userId, Integer tenantId, String orgCode) {
if (oConvertUtils.isNotEmpty(orgCode)) {
return orgCode;
}
if (tenantId != null && tenantId > 0) {
List<SysUserDepart> departs = userDepartMapper.getTenantUserDepart(userId, String.valueOf(tenantId));
if (departs != null && !departs.isEmpty()) {
SysDepart depart = departMapper.selectById(departs.get(0).getDepId());
if (depart != null && oConvertUtils.isNotEmpty(depart.getOrgCode())) {
return depart.getOrgCode();
}
}
}
List<SysUserDepart> departs = userDepartMapper.getUserDepartByUid(userId);
if (departs != null && !departs.isEmpty()) {
SysDepart depart = departMapper.selectById(departs.get(0).getDepId());
if (depart != null) {
return depart.getOrgCode();
}
}
return null;
}
private void createMember(String conversationId, String userId, Date now) {
SysImConversationMember member = new SysImConversationMember();
member.setConversationId(conversationId);
member.setUserId(userId);
member.setUnreadCount(0);
member.setCreateTime(now);
memberMapper.insert(member);
}
private SysImConversationVO buildConversationVo(String conversationId, String userId, Integer tenantId, String targetUserId) {
SysImConversation conversation = conversationMapper.selectById(conversationId);
if (conversation == null) {
return new SysImConversationVO();
}
return buildConversationVo(conversation, userId, targetUserId);
}
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】已有会话快速打开-----------
private SysImConversationVO buildConversationVo(SysImConversation conversation, String userId, String targetUserId) {
SysUser target = userMapper.selectById(targetUserId);
SysImConversationMember member = getMember(userId, conversation.getId());
SysImConversationVO vo = new SysImConversationVO();
vo.setConversationId(conversation.getId());
vo.setConvType(CONV_TYPE_SINGLE);
vo.setLastContent(conversation.getLastContent());
vo.setLastTime(conversation.getLastTime());
vo.setUnreadCount(member == null ? 0 : member.getUnreadCount());
if (target != null) {
vo.setTargetUserId(target.getId());
vo.setTargetRealname(target.getRealname());
vo.setTargetUsername(target.getUsername());
vo.setTargetAvatar(target.getAvatar());
}
return vo;
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】已有会话快速打开-----------
private void validateTenantChat(String userId, Integer tenantId, String orgCode, String targetUserId) {
if (oConvertUtils.isEmpty(targetUserId)) {
throw new JeecgBootException("请选择聊天对象");
}
if (userId.equals(targetUserId)) {
throw new JeecgBootException("不能与自己聊天");
}
if (tenantId == null || tenantId <= 0) {
throw new JeecgBootException("请先选择租户");
}
Integer selfExist = userTenantMapper.userTenantIzExist(userId, tenantId);
Integer targetExist = userTenantMapper.userTenantIzExist(targetUserId, tenantId);
if (selfExist == null || selfExist <= 0 || targetExist == null || targetExist <= 0) {
throw new JeecgBootException("仅支持与当前租户内用户聊天");
}
SysUser target = userMapper.selectById(targetUserId);
if (target == null) {
throw new JeecgBootException("聊天对象不存在");
}
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】限制同部门聊天-----------
String resolvedOrgCode = resolveOrgCode(userId, tenantId, orgCode);
if (oConvertUtils.isEmpty(resolvedOrgCode)) {
throw new JeecgBootException("未获取到当前部门,请切换部门后重试");
}
if (userDepartMapper.countUserInDepartOrgCode(targetUserId, resolvedOrgCode, tenantId) <= 0) {
throw new JeecgBootException("仅支持与同部门用户聊天");
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】限制同部门聊天-----------
}
private String buildPairKey(String userId1, String userId2) {
if (userId1.compareTo(userId2) <= 0) {
return userId1 + "_" + userId2;
}
return userId2 + "_" + userId1;
}
private SysImConversationMember getMember(String userId, String conversationId) {
return memberMapper.selectOne(new LambdaQueryWrapper<SysImConversationMember>()
.eq(SysImConversationMember::getConversationId, conversationId)
.eq(SysImConversationMember::getUserId, userId));
}
private void assertMember(String userId, String conversationId) {
if (getMember(userId, conversationId) == null) {
throw new JeecgBootException("无权访问该会话");
}
}
private SysImMessageVO toMessageVo(SysImMessage message, String currentUserId) {
return toMessageVo(message, currentUserId, null);
}
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】消息列表批量填充发送人-----------
private List<SysImMessageVO> toMessageVoList(List<SysImMessage> messages, String currentUserId) {
if (messages == null || messages.isEmpty()) {
return Collections.emptyList();
}
List<String> senderIds = messages.stream()
.map(SysImMessage::getSenderId)
.filter(oConvertUtils::isNotEmpty)
.distinct()
.collect(Collectors.toList());
Map<String, SysUser> userMap = new HashMap<>(senderIds.size());
if (!senderIds.isEmpty()) {
List<SysUser> users = userMapper.selectBatchIds(senderIds);
if (users != null) {
for (SysUser user : users) {
userMap.put(user.getId(), user);
}
}
}
List<SysImMessageVO> result = new ArrayList<>(messages.size());
Map<String, Boolean> receiverPermissionCache = new HashMap<>(16);
for (SysImMessage message : messages) {
SysImMessageVO vo = toMessageVo(message, currentUserId, userMap);
fillBizRecordReceiverPermission(vo, message, currentUserId, receiverPermissionCache);
result.add(vo);
}
return result;
}
private SysImMessageVO toMessageVo(SysImMessage message, String currentUserId, Map<String, SysUser> userMap) {
SysImMessageVO vo = new SysImMessageVO();
vo.setId(message.getId());
vo.setConversationId(message.getConversationId());
vo.setSenderId(message.getSenderId());
vo.setContent(message.getContent());
vo.setMsgType(message.getMsgType());
vo.setCreateTime(message.getCreateTime());
vo.setMine(currentUserId.equals(message.getSenderId()));
SysUser sender = userMap == null ? null : userMap.get(message.getSenderId());
if (sender == null && userMap == null) {
sender = userMapper.selectById(message.getSenderId());
}
if (sender != null) {
vo.setSenderName(sender.getRealname());
vo.setSenderAvatar(sender.getAvatar());
}
return vo;
}
//update-begin---author:xsl ---date:20260528 for【IM聊天-OA】发送方提示接收方无功能权限-----------
private void fillBizRecordReceiverPermission(SysImMessageVO vo, SysImMessage message, String currentUserId, Map<String, Boolean> cache) {
if (!Boolean.TRUE.equals(vo.getMine()) || !MSG_TYPE_BIZ_RECORD.equals(vo.getMsgType())) {
return;
}
SysImConversation conversation = conversationMapper.selectById(message.getConversationId());
if (conversation == null || !CONV_TYPE_SINGLE.equals(conversation.getConvType())) {
return;
}
String pagePath = extractBizRecordPagePath(vo.getContent());
if (oConvertUtils.isEmpty(pagePath)) {
return;
}
String peerUserId = resolvePeerUserId(message.getConversationId(), currentUserId);
if (oConvertUtils.isEmpty(peerUserId)) {
return;
}
String cacheKey = peerUserId + "|" + normalizeBizPagePath(pagePath);
Boolean hasPermission = cache.get(cacheKey);
if (hasPermission == null) {
hasPermission = hasUserPagePathPermission(peerUserId, pagePath);
cache.put(cacheKey, hasPermission);
}
vo.setReceiverHasBizPagePermission(hasPermission);
}
private String resolvePeerUserId(String conversationId, String currentUserId) {
List<SysImConversationMember> members = memberMapper.selectList(new LambdaQueryWrapper<SysImConversationMember>()
.eq(SysImConversationMember::getConversationId, conversationId));
if (members == null || members.isEmpty()) {
return null;
}
for (SysImConversationMember member : members) {
if (!currentUserId.equals(member.getUserId())) {
return member.getUserId();
}
}
return null;
}
private String extractBizRecordPagePath(String content) {
if (oConvertUtils.isEmpty(content)) {
return null;
}
try {
JSONObject obj = JSONObject.parseObject(content);
return obj.getString("pagePath");
} catch (Exception e) {
log.debug("解析业务明细 pagePath 失败: {}", e.getMessage());
return null;
}
}
private String normalizeBizPagePath(String pagePath) {
if (oConvertUtils.isEmpty(pagePath)) {
return "";
}
String path = pagePath.split("\\?")[0];
if (path.contains("&" + IM_RECORD_QUERY_KEY + "=") || path.contains("?" + IM_RECORD_QUERY_KEY + "=")) {
path = path.replaceAll("[?&]" + IM_RECORD_QUERY_KEY + "=[^&]*", "");
if (path.endsWith("?") || path.endsWith("&")) {
path = path.substring(0, path.length() - 1);
}
}
if (path.endsWith("/") && path.length() > 1) {
path = path.substring(0, path.length() - 1);
}
return path;
}
private boolean hasUserPagePathPermission(String userId, String pagePath) {
String targetPath = normalizeBizPagePath(pagePath);
if (oConvertUtils.isEmpty(userId) || oConvertUtils.isEmpty(targetPath)) {
return false;
}
List<SysPermission> permissions = sysPermissionService.queryByUser(userId);
if (permissions == null || permissions.isEmpty()) {
return false;
}
for (SysPermission permission : permissions) {
if (permission == null || oConvertUtils.isEmpty(permission.getUrl())) {
continue;
}
if (permission.getMenuType() != null && permission.getMenuType() == 2) {
continue;
}
String url = normalizeBizPagePath(permission.getUrl());
if (targetPath.equals(url)) {
return true;
}
}
return false;
}
//update-end---author:xsl ---date:20260528 for【IM聊天-OA】发送方提示接收方无功能权限-----------
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】消息列表批量填充发送人-----------
private void pushChatMessage(String conversationId, String senderId, SysImMessageVO messageVo, String convType) {
List<SysImConversationMember> members = memberMapper.selectList(new LambdaQueryWrapper<SysImConversationMember>()
.eq(SysImConversationMember::getConversationId, conversationId));
for (SysImConversationMember member : members) {
if (senderId.equals(member.getUserId())) {
continue;
}
JSONObject obj = new JSONObject();
obj.put(WebsocketConst.MSG_CMD, WebsocketConst.MSG_CHAT);
obj.put(WebsocketConst.MSG_USER_ID, member.getUserId());
obj.put("conversationId", conversationId);
//update-begin---author:xsl ---date:20260528 for【IM聊天-OA】WebSocket推送会话类型区分群聊-----------
obj.put("convType", convType);
//update-end---author:xsl ---date:20260528 for【IM聊天-OA】WebSocket推送会话类型区分群聊-----------
obj.put("messageId", messageVo.getId());
obj.put("senderId", messageVo.getSenderId());
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】WebSocket推送补全头像字段-----------
obj.put("senderName", messageVo.getSenderName());
obj.put("senderAvatar", messageVo.getSenderAvatar());
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】WebSocket推送补全头像字段-----------
obj.put("content", messageVo.getContent());
obj.put("msgType", messageVo.getMsgType());
obj.put("createTime", messageVo.getCreateTime());
webSocket.sendMessage(member.getUserId(), obj.toJSONString());
}
}
private String truncate(String content, int maxLen) {
if (content == null) {
return null;
}
return content.length() <= maxLen ? content : content.substring(0, maxLen);
}
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】图片消息会话摘要-----------
private String resolveLastContent(String msgType, String content) {
if (MSG_TYPE_IMAGE.equals(msgType)) {
return MSG_IMAGE_PREVIEW;
}
//update-begin---author:xsl ---date:20260528 for【IM聊天-OA】业务明细消息会话摘要-----------
if (MSG_TYPE_BIZ_RECORD.equals(msgType)) {
return resolveBizRecordPreview(content);
}
//update-end---author:xsl ---date:20260528 for【IM聊天-OA】业务明细消息会话摘要-----------
return content;
}
private String resolveBizRecordPreview(String content) {
if (oConvertUtils.isEmpty(content)) {
return MSG_BIZ_RECORD_PREVIEW;
}
try {
JSONObject obj = JSONObject.parseObject(content);
String pageTitle = obj.getString("pageTitle");
if (oConvertUtils.isNotEmpty(pageTitle)) {
return MSG_BIZ_RECORD_PREVIEW + pageTitle;
}
} catch (Exception e) {
log.debug("解析业务明细消息摘要失败: {}", e.getMessage());
}
return MSG_BIZ_RECORD_PREVIEW;
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】图片消息会话摘要-----------
}

View File

@@ -0,0 +1,31 @@
package org.jeecg.modules.im.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* IM 联系人 VO
*/
@Data
@Schema(description = "IM联系人")
public class SysImContactVO {
@Schema(description = "用户ID")
private String id;
@Schema(description = "账号")
private String username;
@Schema(description = "姓名")
private String realname;
@Schema(description = "头像")
private String avatar;
@Schema(description = "部门")
private String orgCodeTxt;
@Schema(description = "会话ID")
private String conversationId;
@Schema(description = "最后消息摘要")
private String lastContent;
@Schema(description = "最后消息时间")
private java.util.Date lastTime;
@Schema(description = "未读数")
private Integer unreadCount;
}

View File

@@ -0,0 +1,43 @@
package org.jeecg.modules.im.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* IM 会话列表 VO
*/
@Data
@Schema(description = "IM会话")
public class SysImConversationVO {
@Schema(description = "会话ID")
private String conversationId;
@Schema(description = "会话类型")
private String convType;
@Schema(description = "最后消息摘要")
private String lastContent;
@Schema(description = "最后消息时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastTime;
@Schema(description = "未读数")
private Integer unreadCount;
@Schema(description = "对方用户ID")
private String targetUserId;
@Schema(description = "对方姓名")
private String targetRealname;
@Schema(description = "对方账号")
private String targetUsername;
@Schema(description = "对方头像")
private String targetAvatar;
@Schema(description = "群名称")
private String groupName;
@Schema(description = "群成员数")
private Integer memberCount;
@Schema(description = "群主用户ID")
private String ownerId;
}

View File

@@ -0,0 +1,29 @@
package org.jeecg.modules.im.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* IM 群聊详情(群设置)
*/
//update-begin---author:cursor ---date:20260529 for【IM聊天-OA】群设置-群详情-----------
@Data
@Schema(description = "IM群聊详情")
public class SysImGroupDetailVO {
@Schema(description = "会话ID")
private String conversationId;
@Schema(description = "群名称")
private String groupName;
@Schema(description = "群主用户ID")
private String ownerId;
@Schema(description = "群成员数")
private Integer memberCount;
@Schema(description = "当前用户是否群主")
private Boolean owner;
@Schema(description = "群成员列表")
private List<SysImGroupMemberVO> members;
}
//update-end---author:cursor ---date:20260529 for【IM聊天-OA】群设置-群详情-----------

View File

@@ -0,0 +1,25 @@
package org.jeecg.modules.im.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* IM 群成员
*/
//update-begin---author:cursor ---date:20260529 for【IM聊天-OA】群设置-群成员展示-----------
@Data
@Schema(description = "IM群成员")
public class SysImGroupMemberVO {
@Schema(description = "用户ID")
private String userId;
@Schema(description = "姓名")
private String realname;
@Schema(description = "账号")
private String username;
@Schema(description = "头像")
private String avatar;
@Schema(description = "是否群主")
private Boolean owner;
}
//update-end---author:cursor ---date:20260529 for【IM聊天-OA】群设置-群成员展示-----------

View File

@@ -0,0 +1,39 @@
package org.jeecg.modules.im.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* IM 消息 VO
*/
@Data
@Schema(description = "IM消息")
public class SysImMessageVO {
@Schema(description = "消息ID")
private String id;
@Schema(description = "会话ID")
private String conversationId;
@Schema(description = "发送人ID")
private String senderId;
@Schema(description = "发送人姓名")
private String senderName;
@Schema(description = "发送人头像")
private String senderAvatar;
@Schema(description = "消息内容")
private String content;
@Schema(description = "消息类型")
private String msgType;
@Schema(description = "是否本人发送")
private Boolean mine;
@Schema(description = "业务明细接收方是否有对应功能权限(仅发送方可见)")
private Boolean receiverHasBizPagePermission;
@Schema(description = "发送时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}

View File

@@ -102,9 +102,17 @@ public interface SysUserDepartMapper extends BaseMapper<SysUserDepart>{
/**
* 通过用户id集合获取用户id和部门code
*
* @param userIdList
* @return
*/
List<SysUserSysDepPostModel> getUserDepPostByUserIds(@Param("userIdList") List<String> userIdList);
/**
* 查询同部门用户(精确 orgCode不含下级部门
*/
List<SysUser> querySameDepartUserList(@Param("orgCode") String orgCode, @Param("excludeUserId") String excludeUserId,
@Param("tenantId") Integer tenantId, @Param("keyword") String keyword);
/**
* 用户是否属于指定 orgCode 部门
*/
int countUserInDepartOrgCode(@Param("userId") String userId, @Param("orgCode") String orgCode, @Param("tenantId") Integer tenantId);
}

View File

@@ -142,4 +142,40 @@
#{userId}
</foreach>
</select>
<!-- IM聊天同部门用户精确 orgCode -->
<select id="querySameDepartUserList" resultType="org.jeecg.modules.system.entity.SysUser">
select distinct a.* from sys_user a
inner join sys_user_depart b on b.user_id = a.id
inner join sys_depart c on b.dep_id = c.id
where a.del_flag = 0 and a.status = 1
and c.org_code = #{orgCode}
and a.username != '_reserve_user_external'
<if test="excludeUserId != null and excludeUserId != ''">
and a.id != #{excludeUserId}
</if>
<if test="tenantId != null and tenantId > 0">
and a.id in (
select user_id from sys_user_tenant where tenant_id = #{tenantId} and status = '1'
)
</if>
<if test="keyword != null and keyword != ''">
<bind name="bindKeyword" value="'%' + keyword + '%'"/>
and (a.username like #{bindKeyword} or a.realname like #{bindKeyword})
</if>
order by a.sort, a.realname
</select>
<select id="countUserInDepartOrgCode" resultType="int">
select count(1) from sys_user a
inner join sys_user_depart b on b.user_id = a.id
inner join sys_depart c on b.dep_id = c.id
where a.del_flag = 0 and a.id = #{userId}
and c.org_code = #{orgCode}
<if test="tenantId != null and tenantId > 0">
and a.id in (
select user_id from sys_user_tenant where tenant_id = #{tenantId} and status = '1'
)
</if>
</select>
</mapper>

View File

@@ -157,6 +157,7 @@ spring:
datasource:
master:
url: jdbc:mysql://xsl.qdxsl.top:50768/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
# url: jdbc:mysql://localhost:3307/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver

View File

@@ -0,0 +1,10 @@
-- 分类字典统一租户ID为1002幂等
SET NAMES utf8mb4;
SET @mes_tenant_id = 1002;
UPDATE `sys_category`
SET `tenant_id` = @mes_tenant_id,
`update_by` = 'admin',
`update_time` = NOW()
WHERE IFNULL(`tenant_id`, 0) <> @mes_tenant_id;

View File

@@ -0,0 +1,75 @@
-- 配方日志查询配合示方/混炼示方修改日志表 + 字典 + 菜单
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `mes_xsl_formula_spec_edit_log` (
`id` varchar(32) NOT NULL COMMENT '主键',
`spec_type` varchar(20) NOT NULL COMMENT '示方分类 formula=配合示方 mixing=混炼示方',
`spec_id` varchar(32) NOT NULL COMMENT '示方主表ID',
`spec_title` varchar(200) DEFAULT NULL COMMENT '示方标识胶料代号/规格名',
`issue_number` varchar(64) DEFAULT NULL COMMENT '发行编号',
`action_type` varchar(20) NOT NULL COMMENT '操作类型 create=新增 update=修改 delete=删除',
`change_summary` varchar(2000) DEFAULT NULL COMMENT '修改内容摘要',
`before_snapshot` longtext COMMENT '变更前快照JSON',
`after_snapshot` longtext COMMENT '变更后快照JSON',
`modify_time` datetime DEFAULT NULL COMMENT '修改时间',
`modify_by` varchar(64) DEFAULT NULL COMMENT '修改人账号',
`modify_by_name` varchar(100) DEFAULT NULL COMMENT '修改人姓名',
`tenant_id` int DEFAULT NULL COMMENT '租户ID',
PRIMARY KEY (`id`),
KEY `idx_fsel_spec_type` (`spec_type`),
KEY `idx_fsel_spec_id` (`spec_id`),
KEY `idx_fsel_issue_no` (`issue_number`),
KEY `idx_fsel_modify_time` (`modify_time`),
KEY `idx_fsel_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES配方示方修改日志';
INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
VALUES ('1995000000000000107', '配方日志示方分类', 'xslmes_formula_spec_edit_log_type', '配方日志查询-示方分类', 0, 'admin', NOW(), 0, 1002);
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
VALUES ('1995000000000001071', '1995000000000000107', '配合示方', 'formula', 1, 1, 'admin', NOW());
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
VALUES ('1995000000000001072', '1995000000000000107', '混炼示方', 'mixing', 2, 1, 'admin', NOW());
INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
VALUES ('1995000000000000108', '配方日志操作类型', 'xslmes_formula_spec_edit_log_action', '配方日志查询-操作类型', 0, 'admin', NOW(), 0, 1002);
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
VALUES ('1995000000000001081', '1995000000000000108', '新增', 'create', 1, 1, 'admin', NOW());
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
VALUES ('1995000000000001082', '1995000000000000108', '修改', 'update', 2, 1, 'admin', NOW());
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
VALUES ('1995000000000001083', '1995000000000000108', '删除', 'delete', 3, 1, 'admin', NOW());
INSERT IGNORE INTO `sys_permission` (
`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`,
`menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`,
`hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`,
`del_flag`, `rule_flag`, `status`, `internal_or_external`
) VALUES (
'177925970995560', '1900000000000000810', '配方日志查询', '/xslmes/mesXslFormulaSpecEditLog',
'xslmes/mesXslFormulaSpecEditLog/MesXslFormulaSpecEditLogList', 1, 'MesXslFormulaSpecEditLogList', NULL,
1, NULL, '0', 5.50, 0, 'ant-design:file-search-outlined', 0, 1,
0, 0, '配合示方/混炼示方修改日志查询与对比', 'admin', NOW(), 'admin', NOW(),
0, 0, '1', 0
);
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES ('177925970995561', '177925970995560', '查询', 2, 'xslmes:mes_xsl_formula_spec_edit_log:list', '1', 1.00, 0, 1, 0, '1', 0, 'admin', NOW());
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES ('177925970995562', '177925970995560', '导出', 2, 'xslmes:mes_xsl_formula_spec_edit_log:exportXls', '1', 2.00, 0, 1, 0, '1', 0, 'admin', NOW());
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1'
FROM `sys_role` r
CROSS JOIN `sys_permission` p
WHERE r.`role_code` = 'admin'
AND p.`id` IN ('177925970995560', '177925970995561', '177925970995562')
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp
WHERE rp.`role_id` = r.id AND rp.`permission_id` = p.id
);

View File

@@ -0,0 +1,40 @@
-- 架子车数设定菜单与字典数据来源于混炼示方不建业务表
SET NAMES utf8mb4;
INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
VALUES ('1995000000000000109', '架子车数设定状态', 'xslmes_rack_train_count_setting_status', '架子车数设定-是否已设定车数', 0, 'admin', NOW(), 0, 1002);
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
VALUES ('1995000000000001091', '1995000000000000109', '已设定', '1', 1, 1, 'admin', NOW());
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
VALUES ('1995000000000001092', '1995000000000000109', '未设定', '0', 2, 1, 'admin', NOW());
INSERT IGNORE INTO `sys_permission` (
`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`,
`menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`,
`hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`,
`del_flag`, `rule_flag`, `status`, `internal_or_external`
) VALUES (
'177925970995570', '1900000000000000810', '架子车数设定', '/xslmes/mesXslRackTrainCountSetting',
'xslmes/mesXslRackTrainCountSetting/MesXslRackTrainCountSettingList', 1, 'MesXslRackTrainCountSettingList', NULL,
1, NULL, '0', 2.50, 0, 'ant-design:car-outlined', 0, 1,
0, 0, '基于混炼示方查询与维护设定车数', 'admin', NOW(), 'admin', NOW(),
0, 0, '1', 0
);
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES ('177925970995571', '177925970995570', '查询', 2, 'xslmes:mes_xsl_rack_train_count_setting:list', '1', 1.00, 0, 1, 0, '1', 0, 'admin', NOW());
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES ('177925970995572', '177925970995570', '编辑', 2, 'xslmes:mes_xsl_rack_train_count_setting:edit', '1', 2.00, 0, 1, 0, '1', 0, 'admin', NOW());
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1'
FROM `sys_role` r
CROSS JOIN `sys_permission` p
WHERE r.`role_code` = 'admin'
AND p.`id` IN ('177925970995570', '177925970995571', '177925970995572')
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);

View File

@@ -0,0 +1,149 @@
-- 胶料快检记录主子表字典 + 建表 + 菜单质量管理下+ 按钮 + 胶料信息检验按钮 + 租户 admin 授权
-- 权限前缀mes:mes_xsl_rubber_quick_test_record:*
-- 菜单 ID 1860000000000000192
-- SET @mes_tenant_id多租户 admin 授权目标租户
SET NAMES utf8mb4;
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检记录检验结果', 'xslmes_rubber_quick_test_record_result', '1合格0不合格', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_record_result' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '合格', '1', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_record_result' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '1');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '不合格', '0', 2, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_record_result' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '0');
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检班次', 'xslmes_rubber_quick_test_work_shift', '班次', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_work_shift' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, v.txt, v.val, v.ord, 1, 'admin', NOW()
FROM `sys_dict` d
CROSS JOIN (
SELECT '早班' AS txt, '1' AS val, 1 AS ord UNION ALL
SELECT '中班', '2', 2 UNION ALL
SELECT '晚班', '3', 3
) v
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_work_shift'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = v.val);
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检班组', 'xslmes_rubber_quick_test_work_team', '班组', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_work_team' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, v.txt, v.val, v.ord, 1, 'admin', NOW()
FROM `sys_dict` d
CROSS JOIN (
SELECT '甲班' AS txt, '1' AS val, 1 AS ord UNION ALL
SELECT '乙班', '2', 2 UNION ALL
SELECT '丙班', '3', 3
) v
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_work_team'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = v.val);
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_record` (
`id` varchar(32) NOT NULL COMMENT '主键',
`rubber_material_id` varchar(32) DEFAULT NULL COMMENT '胶料 mes_material.id',
`rubber_material_name` varchar(128) DEFAULT NULL COMMENT '胶料名称冗余',
`std_id` varchar(32) DEFAULT NULL COMMENT '引用的实验标准 mes_xsl_rubber_quick_test_std.id',
`prod_equipment_ledger_id` varchar(32) DEFAULT NULL COMMENT '生产机台 mes_xsl_equipment_ledger.id',
`prod_equipment_name` varchar(128) DEFAULT NULL COMMENT '生产机台名称冗余',
`production_date` date DEFAULT NULL COMMENT '生产日期',
`train_no` varchar(64) DEFAULT NULL COMMENT '车次编号',
`work_shift` varchar(8) DEFAULT NULL COMMENT '班次字典xslmes_rubber_quick_test_work_shift',
`work_team` varchar(8) DEFAULT NULL COMMENT '班组字典xslmes_rubber_quick_test_work_team',
`inspect_times` int DEFAULT NULL COMMENT '检验次数',
`inspect_time` datetime DEFAULT NULL COMMENT '检验时间',
`inspector_user_id` varchar(32) DEFAULT NULL COMMENT '检验人用户ID',
`inspector_username` varchar(64) DEFAULT NULL COMMENT '检验人账号冗余',
`inspector_realname` varchar(64) DEFAULT NULL COMMENT '检验人姓名冗余',
`quick_test_type_id` varchar(32) DEFAULT NULL COMMENT '检验类型 mes_xsl_rubber_quick_test_type.id',
`quick_test_type_name` varchar(128) DEFAULT NULL COMMENT '检验类型名称冗余',
`inspect_result` varchar(2) DEFAULT NULL COMMENT '检验结果字典xslmes_rubber_quick_test_record_result1合格0不合格',
`production_plan_no` varchar(100) DEFAULT NULL COMMENT '生产计划号',
`inspect_equipment_ledger_id` varchar(32) DEFAULT NULL COMMENT '检验机台 mes_xsl_equipment_ledger.id',
`inspect_equipment_name` varchar(128) DEFAULT NULL COMMENT '检验机台名称冗余',
`rubber_card_no` varchar(100) DEFAULT NULL COMMENT '胶料卡片号',
`rubber_batch_no` varchar(100) DEFAULT NULL COMMENT '胶料批次',
`tenant_id` int DEFAULT NULL COMMENT '租户',
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '部门',
`create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(32) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int DEFAULT '0' COMMENT '删除标记0正常1删除',
PRIMARY KEY (`id`),
KEY `idx_mrqtr_material` (`rubber_material_id`),
KEY `idx_mrqtr_std` (`std_id`),
KEY `idx_mrqtr_tenant` (`tenant_id`),
KEY `idx_mrqtr_inspect_time` (`inspect_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES胶料快检记录';
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_record_line` (
`id` varchar(32) NOT NULL COMMENT '主键',
`record_id` varchar(32) NOT NULL COMMENT '主表 mes_xsl_rubber_quick_test_record.id',
`data_point_id` varchar(32) DEFAULT NULL COMMENT '数据点 mes_xsl_rubber_quick_test_data_point.id',
`inspect_item` varchar(128) DEFAULT NULL COMMENT '检验项目数据点名称只读带出',
`lower_limit` decimal(18,6) DEFAULT NULL COMMENT '检验下限只读带出',
`inspect_value` decimal(18,6) DEFAULT NULL COMMENT '检验值',
`upper_limit` decimal(18,6) DEFAULT NULL COMMENT '检验上限只读带出',
`sort_no` int DEFAULT NULL COMMENT '排序号',
`create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(32) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_mrqtrl_record` (`record_id`),
UNIQUE KEY `uk_mrqtrl_record_point` (`record_id`, `data_point_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES胶料快检记录明细';
SET @mes_tenant_id = 1002;
SET @mes_quality_pid = IFNULL(
(SELECT `id` FROM `sys_permission` WHERE `del_flag` = 0 AND `menu_type` = 0 AND `name` = '质量管理' LIMIT 1),
'1860000000000000162'
);
INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `component_name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `keep_alive`, `internal_or_external`, `create_by`, `create_time`)
VALUES ('1860000000000000192', @mes_quality_pid, '胶料快检记录', '/xslmes/mesXslRubberQuickTestRecord', 'xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecordList', 'MesXslRubberQuickTestRecordList', 1, NULL, '1', 5, 1, 0, 0, '1', 0, 1, 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE
`parent_id` = VALUES(`parent_id`), `name` = VALUES(`name`), `url` = VALUES(`url`), `component` = VALUES(`component`),
`component_name` = VALUES(`component_name`), `sort_no` = VALUES(`sort_no`), `is_leaf` = VALUES(`is_leaf`), `keep_alive` = VALUES(`keep_alive`);
UPDATE `sys_permission` SET `icon` = 'ant-design:file-search-outlined' WHERE `id` = '1860000000000000192' AND `del_flag` = 0;
INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `status`, `del_flag`, `create_by`, `create_time`) VALUES
('1860000000000000193', '1860000000000000192', '新增', 2, 'mes:mes_xsl_rubber_quick_test_record:add', '1', '1', 0, 'admin', NOW()),
('1860000000000000194', '1860000000000000192', '编辑', 2, 'mes:mes_xsl_rubber_quick_test_record:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000000195', '1860000000000000192', '删除', 2, 'mes:mes_xsl_rubber_quick_test_record:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000000196', '1860000000000000192', '批量删除', 2, 'mes:mes_xsl_rubber_quick_test_record:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000000197', '1860000000000000192', '导出', 2, 'mes:mes_xsl_rubber_quick_test_record:exportXls', '1', '1', 0, 'admin', NOW()),
('1860000000000000198', '1860000000000000192', '导入', 2, 'mes:mes_xsl_rubber_quick_test_record:importExcel', '1', '1', 0, 'admin', NOW()),
('1860000000000000199', '1860000000000000192', '从胶料生成', 2, 'mes:mes_xsl_rubber_quick_test_record:batchFromMaterial', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`), `name` = VALUES(`name`);
INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `status`, `del_flag`, `create_by`, `create_time`) VALUES
('1860000000000000200', '1860000000000000011', '胶料快检', 2, 'mes:mes_material:rubberQuickTestInspect', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`), `name` = VALUES(`name`);
INSERT INTO `sys_role_permission`(`id`, `role_id`, `permission_id`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NOW(), '127.0.0.1'
FROM sys_role r
CROSS JOIN sys_permission p
WHERE r.tenant_id = @mes_tenant_id
AND r.role_code = 'admin'
AND p.id IN (
'1860000000000000192',
'1860000000000000193', '1860000000000000194', '1860000000000000195', '1860000000000000196',
'1860000000000000197', '1860000000000000198', '1860000000000000199',
'1860000000000000200'
)
AND NOT EXISTS (
SELECT 1 FROM sys_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
);

View File

@@ -0,0 +1,6 @@
-- 胶料快检记录单号 record_noJL+yyyyMMdd+4位流水
ALTER TABLE `mes_xsl_rubber_quick_test_record`
ADD COLUMN `record_no` varchar(32) DEFAULT NULL COMMENT '单号JL+日期+4位流水如JL202605280001' AFTER `id`;
ALTER TABLE `mes_xsl_rubber_quick_test_record`
ADD UNIQUE KEY `uk_mrqtr_record_no` (`record_no`);

View File

@@ -0,0 +1,70 @@
-- 租户 IM 聊天表结构 + 我的租户菜单
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `sys_im_conversation` (
`id` varchar(32) NOT NULL COMMENT '主键',
`conv_type` varchar(10) NOT NULL DEFAULT 'single' COMMENT '会话类型 single单聊',
`user_pair_key` varchar(80) DEFAULT NULL COMMENT '单聊唯一键(较小userId_较大userId)',
`tenant_id` int DEFAULT NULL COMMENT '租户ID',
`last_content` varchar(500) DEFAULT NULL COMMENT '最后一条消息摘要',
`last_time` datetime DEFAULT NULL COMMENT '最后消息时间',
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_conv_pair` (`tenant_id`, `user_pair_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IM会话表';
CREATE TABLE IF NOT EXISTS `sys_im_conversation_member` (
`id` varchar(32) NOT NULL COMMENT '主键',
`conversation_id` varchar(32) NOT NULL COMMENT '会话ID',
`user_id` varchar(32) NOT NULL COMMENT '用户ID',
`unread_count` int NOT NULL DEFAULT 0 COMMENT '未读数',
`last_read_time` datetime DEFAULT NULL COMMENT '最后已读时间',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_member` (`conversation_id`, `user_id`),
KEY `idx_im_member_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IM会话成员表';
CREATE TABLE IF NOT EXISTS `sys_im_message` (
`id` varchar(32) NOT NULL COMMENT '主键',
`conversation_id` varchar(32) NOT NULL COMMENT '会话ID',
`sender_id` varchar(32) NOT NULL COMMENT '发送人ID',
`content` text COMMENT '消息内容',
`msg_type` varchar(20) NOT NULL DEFAULT 'text' COMMENT '消息类型 text/image/file',
`tenant_id` int DEFAULT NULL COMMENT '租户ID',
`create_time` datetime DEFAULT NULL COMMENT '发送时间',
PRIMARY KEY (`id`),
KEY `idx_im_msg_conv_time` (`conversation_id`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IM消息表';
INSERT IGNORE INTO `sys_permission` (
`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`,
`menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`,
`hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`,
`del_flag`, `rule_flag`, `status`, `internal_or_external`
) VALUES (
'1995000000000000110', '1674708136602542082', 'IM聊天', '/my/ImChat',
'system/im/ImChat', 1, 'ImChat', NULL,
1, NULL, '0', 1.10, 0, 'ant-design:message-outlined', 0, 1,
0, 0, '租户内用户即时聊天', 'admin', NOW(), 'admin', NOW(),
0, 0, '1', 0
);
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES ('1995000000000000111', '1995000000000000110', '查询', 2, 'sys:im:chat:list', '1', 1.00, 0, 1, 0, '1', 0, 'admin', NOW());
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES ('1995000000000000112', '1995000000000000110', '发送', 2, 'sys:im:chat:send', '1', 2.00, 0, 1, 0, '1', 0, 'admin', NOW());
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1'
FROM `sys_role` r
CROSS JOIN `sys_permission` p
WHERE r.`role_code` = 'admin'
AND p.`id` IN ('1995000000000000110', '1995000000000000111', '1995000000000000112')
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);

View File

@@ -0,0 +1,22 @@
-- IM 群聊扩展会话表字段
SET NAMES utf8mb4;
ALTER TABLE `sys_im_conversation`
ADD COLUMN `group_name` varchar(100) DEFAULT NULL COMMENT '群名称' AFTER `user_pair_key`,
ADD COLUMN `owner_id` varchar(32) DEFAULT NULL COMMENT '群主用户ID' AFTER `group_name`;
ALTER TABLE `sys_im_conversation`
MODIFY COLUMN `conv_type` varchar(10) NOT NULL DEFAULT 'single' COMMENT '会话类型 single单聊 group群聊';
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES ('1995000000000000113', '1995000000000000110', '创建群聊', 2, 'sys:im:chat:group', '1', 3.00, 0, 1, 0, '1', 0, 'admin', NOW());
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1'
FROM `sys_role` r
CROSS JOIN `sys_permission` p
WHERE r.`role_code` = 'admin'
AND p.`id` = '1995000000000000113'
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);

View File

@@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
"""生成分类字典「行政区划」Flyway SQL国家-省-市-县)"""
import json
from pathlib import Path
MYSQL_DIR = Path(__file__).resolve().parents[1]
JEECG_BOOT = Path(__file__).resolve().parents[9]
PCA_PATH = JEECG_BOOT / 'jeecg-boot-base-core/src/main/resources/static/pca.json'
OUT_PATH = MYSQL_DIR / 'V3.9.2_105__sys_region_category_dict.sql'
MES_TENANT_ID = 1002
pca = json.loads(PCA_PATH.read_text(encoding='utf-8'))
OTHER_COUNTRIES = [
('阿富汗', 'AF'), ('阿尔巴尼亚', 'AL'), ('阿尔及利亚', 'DZ'), ('安道尔', 'AD'), ('安哥拉', 'AO'),
('安提瓜和巴布达', 'AG'), ('阿根廷', 'AR'), ('亚美尼亚', 'AM'), ('澳大利亚', 'AU'), ('奥地利', 'AT'),
('阿塞拜疆', 'AZ'), ('巴哈马', 'BS'), ('巴林', 'BH'), ('孟加拉国', 'BD'), ('巴巴多斯', 'BB'),
('白俄罗斯', 'BY'), ('比利时', 'BE'), ('伯利兹', 'BZ'), ('贝宁', 'BJ'), ('不丹', 'BT'),
('玻利维亚', 'BO'), ('波黑', 'BA'), ('博茨瓦纳', 'BW'), ('巴西', 'BR'), ('文莱', 'BN'),
('保加利亚', 'BG'), ('布基纳法索', 'BF'), ('布隆迪', 'BI'), ('佛得角', 'CV'), ('柬埔寨', 'KH'),
('喀麦隆', 'CM'), ('加拿大', 'CA'), ('中非', 'CF'), ('乍得', 'TD'), ('智利', 'CL'),
('哥伦比亚', 'CO'), ('科摩罗', 'KM'), ('刚果(布)', 'CG'), ('刚果(金)', 'CD'), ('哥斯达黎加', 'CR'),
('科特迪瓦', 'CI'), ('克罗地亚', 'HR'), ('古巴', 'CU'), ('塞浦路斯', 'CY'), ('捷克', 'CZ'),
('丹麦', 'DK'), ('吉布提', 'DJ'), ('多米尼克', 'DM'), ('多米尼加', 'DO'), ('厄瓜多尔', 'EC'),
('埃及', 'EG'), ('萨尔瓦多', 'SV'), ('赤道几内亚', 'GQ'), ('厄立特里亚', 'ER'), ('爱沙尼亚', 'EE'),
('斯威士兰', 'SZ'), ('埃塞俄比亚', 'ET'), ('斐济', 'FJ'), ('芬兰', 'FI'), ('法国', 'FR'),
('加蓬', 'GA'), ('冈比亚', 'GM'), ('格鲁吉亚', 'GE'), ('德国', 'DE'), ('加纳', 'GH'),
('希腊', 'GR'), ('格林纳达', 'GD'), ('危地马拉', 'GT'), ('几内亚', 'GN'), ('几内亚比绍', 'GW'),
('圭亚那', 'GY'), ('海地', 'HT'), ('洪都拉斯', 'HN'), ('匈牙利', 'HU'), ('冰岛', 'IS'),
('印度', 'IN'), ('印度尼西亚', 'ID'), ('伊朗', 'IR'), ('伊拉克', 'IQ'), ('爱尔兰', 'IE'),
('以色列', 'IL'), ('意大利', 'IT'), ('牙买加', 'JM'), ('日本', 'JP'), ('约旦', 'JO'),
('哈萨克斯坦', 'KZ'), ('肯尼亚', 'KE'), ('基里巴斯', 'KI'), ('科威特', 'KW'), ('吉尔吉斯斯坦', 'KG'),
('老挝', 'LA'), ('拉脱维亚', 'LV'), ('黎巴嫩', 'LB'), ('莱索托', 'LS'), ('利比里亚', 'LR'),
('利比亚', 'LY'), ('列支敦士登', 'LI'), ('立陶宛', 'LT'), ('卢森堡', 'LU'), ('马达加斯加', 'MG'),
('马拉维', 'MW'), ('马来西亚', 'MY'), ('马尔代夫', 'MV'), ('马里', 'ML'), ('马耳他', 'MT'),
('马绍尔群岛', 'MH'), ('毛里塔尼亚', 'MR'), ('毛里求斯', 'MU'), ('墨西哥', 'MX'), ('密克罗尼西亚', 'FM'),
('摩尔多瓦', 'MD'), ('摩纳哥', 'MC'), ('蒙古', 'MN'), ('黑山', 'ME'), ('摩洛哥', 'MA'),
('莫桑比克', 'MZ'), ('缅甸', 'MM'), ('纳米比亚', 'NA'), ('瑙鲁', 'NR'), ('尼泊尔', 'NP'),
('荷兰', 'NL'), ('新西兰', 'NZ'), ('尼加拉瓜', 'NI'), ('尼日尔', 'NE'), ('尼日利亚', 'NG'),
('朝鲜', 'KP'), ('北马其顿', 'MK'), ('挪威', 'NO'), ('阿曼', 'OM'), ('巴基斯坦', 'PK'),
('帕劳', 'PW'), ('巴拿马', 'PA'), ('巴布亚新几内亚', 'PG'), ('巴拉圭', 'PY'), ('秘鲁', 'PE'),
('菲律宾', 'PH'), ('波兰', 'PL'), ('葡萄牙', 'PT'), ('卡塔尔', 'QA'), ('罗马尼亚', 'RO'),
('俄罗斯', 'RU'), ('卢旺达', 'RW'), ('圣基茨和尼维斯', 'KN'), ('圣卢西亚', 'LC'), ('圣文森特和格林纳丁斯', 'VC'),
('萨摩亚', 'WS'), ('圣马力诺', 'SM'), ('圣多美和普林西比', 'ST'), ('沙特阿拉伯', 'SA'), ('塞内加尔', 'SN'),
('塞尔维亚', 'RS'), ('塞舌尔', 'SC'), ('塞拉利昂', 'SL'), ('新加坡', 'SG'), ('斯洛伐克', 'SK'),
('斯洛文尼亚', 'SI'), ('所罗门群岛', 'SB'), ('索马里', 'SO'), ('南非', 'ZA'), ('韩国', 'KR'),
('南苏丹', 'SS'), ('西班牙', 'ES'), ('斯里兰卡', 'LK'), ('苏丹', 'SD'), ('苏里南', 'SR'),
('瑞典', 'SE'), ('瑞士', 'CH'), ('叙利亚', 'SY'), ('塔吉克斯坦', 'TJ'), ('坦桑尼亚', 'TZ'),
('泰国', 'TH'), ('东帝汶', 'TL'), ('多哥', 'TG'), ('汤加', 'TO'), ('特立尼达和多巴哥', 'TT'),
('突尼斯', 'TN'), ('土耳其', 'TR'), ('土库曼斯坦', 'TM'), ('图瓦卢', 'TV'), ('乌干达', 'UG'),
('乌克兰', 'UA'), ('阿联酋', 'AE'), ('英国', 'GB'), ('美国', 'US'), ('乌拉圭', 'UY'),
('乌兹别克斯坦', 'UZ'), ('瓦努阿图', 'VU'), ('梵蒂冈', 'VA'), ('委内瑞拉', 'VE'), ('越南', 'VN'),
('也门', 'YE'), ('赞比亚', 'ZM'), ('津巴布韦', 'ZW'),
]
seq = 0
def make_id(code: str) -> str:
global seq
if code.isdigit():
return f'1995{int(code):015d}'
seq += 1
return f'1995900000000{seq:06d}'
def esc(s: str) -> str:
return s.replace('\\', '\\\\').replace("'", "''")
nodes = []
root_id = '1995000000000000001'
nodes.append({'id': root_id, 'pid': '0', 'name': '行政区划', 'code': 'SYS_REGION', 'has_child': '1'})
cn_id = make_id('86')
nodes.append({'id': cn_id, 'pid': root_id, 'name': '中国', 'code': 'SYS_REGION_86', 'has_child': '1'})
for prov_code, prov_name in pca['86'].items():
prov_id = make_id(prov_code)
cities = pca.get(prov_code, {})
nodes.append({'id': prov_id, 'pid': cn_id, 'name': prov_name, 'code': prov_code, 'has_child': '1' if cities else '0'})
for city_code, city_name in cities.items():
city_id = make_id(city_code)
counties = pca.get(city_code, {})
nodes.append({'id': city_id, 'pid': prov_id, 'name': city_name, 'code': city_code, 'has_child': '1' if counties else '0'})
for county_code, county_name in counties.items():
nodes.append({'id': make_id(county_code), 'pid': city_id, 'name': county_name, 'code': county_code, 'has_child': '0'})
for name, iso in OTHER_COUNTRIES:
nodes.append({'id': make_id(f'C{iso}'), 'pid': root_id, 'name': name, 'code': f'SYS_REGION_{iso}', 'has_child': '0'})
lines = [
'-- 分类字典:国家-省-市-县 四级行政区划(幂等)',
'-- 数据来源JeecgBoot pca.json + ISO3166 国家列表',
'SET NAMES utf8mb4;',
'',
]
batch_size = 50
for i in range(0, len(nodes), batch_size):
batch = nodes[i : i + batch_size]
lines.append('INSERT INTO `sys_category` (`id`, `pid`, `name`, `code`, `has_child`, `is_rubber`, `tenant_id`, `create_by`, `create_time`)')
values = []
for n in batch:
values.append(
"SELECT '{id}', '{pid}', '{name}', '{code}', '{has_child}', '0', {tenant_id}, 'admin', NOW() FROM DUAL "
"WHERE NOT EXISTS (SELECT 1 FROM `sys_category` WHERE `code` = '{code}')".format(
id=n['id'], pid=n['pid'], name=esc(n['name']), code=n['code'], has_child=n['has_child'], tenant_id=MES_TENANT_ID
)
)
lines.append('\nUNION ALL\n'.join(values))
lines.append(';')
lines.append('')
OUT_PATH.write_text('\n'.join(lines), encoding='utf-8')
print(f'Generated {len(nodes)} nodes -> {OUT_PATH}')
print(f'File size: {OUT_PATH.stat().st_size / 1024:.1f} KB')

View File

@@ -0,0 +1,16 @@
.im-record-locate-row {
> td {
background: #fff7e6 !important;
animation: im-record-locate-flash 1.2s ease-in-out 2;
}
}
@keyframes im-record-locate-flash {
0%,
100% {
box-shadow: inset 0 0 0 9999px rgba(250, 173, 20, 0.08);
}
50% {
box-shadow: inset 0 0 0 9999px rgba(250, 173, 20, 0.22);
}
}

View File

@@ -3,6 +3,7 @@
@import 'public.less';
@import 'ant/index.less';
@import './theme.less';
@import './im-record-locate.less';
@import './entry.css';
input:-webkit-autofill {

View File

@@ -1,4 +1,5 @@
import { reactive, ref, Ref, unref } from 'vue';
import { reactive, ref, Ref, unref, onUnmounted, watch, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { merge } from 'lodash-es';
import { DynamicProps } from '/#/utils';
import { BasicTableProps, TableActionType, useTable } from '/@/components/Table';
@@ -9,6 +10,16 @@ import { useMethods } from '/@/hooks/system/useMethods';
import { useDesign } from '/@/hooks/web/useDesign';
import { filterObj } from '/@/utils/common/compUtils';
import { isFunction } from '@/utils/is';
import { registerImPageListProvider } from '/@/views/system/im/imPageListRegistry';
import { buildImPageListSnapshot } from '/@/views/system/im/imPageListUtil';
import { IM_RECORD_QUERY_KEY } from '/@/views/system/im/imBizRecordMessage';
import {
IM_RECORD_LOCATE_CLEAR_EVENT,
IM_RECORD_LOCATE_EVENT,
removeImRecordQueryFromRoute,
resolveImLocateRecordId,
scrollToImRecordRowWithRetry,
} from '/@/views/system/im/imRecordLocate';
const { handleExportXls, handleImportXls } = useMethods();
// 定义 useListPage 方法所需参数
@@ -59,7 +70,168 @@ export function useListPage(options: ListPageOptions) {
const tableContext = useListTable(options.tableProps);
const [, { getForm, reload, setLoading, getColumns }, { selectedRowKeys }] = tableContext;
const route = useRoute();
const [, tableMethods, { selectedRowKeys }] = tableContext;
const { getForm, reload, setLoading, getColumns } = tableMethods;
const imHighlightRecordId = ref('');
let clearHighlightTimer: ReturnType<typeof setTimeout> | null = null;
let locatingRecordId = '';
onUnmounted(() => {
if (clearHighlightTimer) {
clearTimeout(clearHighlightTimer);
clearHighlightTimer = null;
}
});
//update-begin---author:xsl ---date:20260528 for【IM聊天-OA】列表页注册 IM 明细快照提供器-----------
onUnmounted(
registerImPageListProvider(() => {
const sourceColumns = tableMethods.getColumns?.() || options.tableProps?.columns || [];
return buildImPageListSnapshot({
title: (options.tableProps?.title as string) || '',
pagePath: route.fullPath,
rowKey: (options.tableProps?.rowKey as string) || 'id',
sourceColumns,
records: tableMethods.getDataSource?.() || [],
});
}),
);
//update-end---author:xsl ---date:20260528 for【IM聊天-OA】列表页注册 IM 明细快照提供器-----------
//update-begin---author:xsl ---date:20260528 for【IM聊天-OA】IM 消息链接跳转后定位列表行-----------
function isTableReady() {
try {
tableMethods.getDataSource?.();
return true;
} catch {
return false;
}
}
function applyImRecordRowClassName() {
if (!isTableReady()) {
return;
}
const rowKey = (options.tableProps?.rowKey as string) || 'id';
tableMethods.setProps?.({
rowClassName: (record: Recordable) => {
if (imHighlightRecordId.value && String(record[rowKey]) === imHighlightRecordId.value) {
return 'im-record-locate-row';
}
return '';
},
});
}
/** 等待列表首屏数据加载(兼容 immediate:false + 左侧树页面) */
async function waitForLocateContext(maxWaitMs = 3500) {
const start = Date.now();
while (Date.now() - start < maxWaitMs) {
if (!isTableReady()) {
await new Promise((resolve) => setTimeout(resolve, 50));
continue;
}
const data = tableMethods.getDataSource?.() || [];
if (data.length > 0) {
return true;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
return isTableReady();
}
function findRecordInTable(recordId: string) {
const rowKey = (options.tableProps?.rowKey as string) || 'id';
const data = tableMethods.getDataSource?.() || [];
return data.some((item) => String(item[rowKey]) === recordId);
}
function clearImRecordHighlight() {
imHighlightRecordId.value = '';
applyImRecordRowClassName();
}
function scheduleClearImRecordHighlight(delayMs = 3500) {
if (clearHighlightTimer) {
clearTimeout(clearHighlightTimer);
}
clearHighlightTimer = setTimeout(() => {
clearImRecordHighlight();
clearHighlightTimer = null;
}, delayMs);
}
async function applyImRecordHighlight(recordId: string) {
imHighlightRecordId.value = recordId;
applyImRecordRowClassName();
await nextTick();
await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
applyImRecordRowClassName();
await scrollToImRecordRowWithRetry(recordId);
}
async function locateImRecordRow(recordId: string) {
if (locatingRecordId === recordId) {
return;
}
locatingRecordId = recordId;
try {
if (!(await waitForLocateContext())) {
return;
}
if (!findRecordInTable(recordId)) {
$message.createMessage.warning('当前列表中未找到对应数据');
removeImRecordQueryFromRoute();
return;
}
await applyImRecordHighlight(recordId);
scheduleClearImRecordHighlight();
// 直链 URL 场景:仅改地址栏,避免 router.replace 导致 fullPath 变化 remount
removeImRecordQueryFromRoute();
} finally {
locatingRecordId = '';
}
}
watch(
() => [route.path, route.query[IM_RECORD_QUERY_KEY]] as const,
([path, queryRecordId]) => {
const recordId = resolveImLocateRecordId(path, queryRecordId);
if (!recordId) {
return;
}
nextTick(() => locateImRecordRow(recordId));
},
{ immediate: true },
);
function handleImRecordLocateEvent(e: Event) {
const detail = (e as CustomEvent<{ path: string; recordId: string }>).detail;
if (!detail?.path || detail.path !== route.path || !detail.recordId) {
return;
}
nextTick(() => locateImRecordRow(detail.recordId));
}
function handleImRecordLocateClearEvent() {
if (clearHighlightTimer) {
clearTimeout(clearHighlightTimer);
clearHighlightTimer = null;
}
locatingRecordId = '';
clearImRecordHighlight();
}
onUnmounted(() => {
window.removeEventListener(IM_RECORD_LOCATE_EVENT, handleImRecordLocateEvent);
window.removeEventListener(IM_RECORD_LOCATE_CLEAR_EVENT, handleImRecordLocateClearEvent);
});
window.addEventListener(IM_RECORD_LOCATE_EVENT, handleImRecordLocateEvent);
window.addEventListener(IM_RECORD_LOCATE_CLEAR_EVENT, handleImRecordLocateClearEvent);
//update-end---author:xsl ---date:20260528 for【IM聊天-OA】IM 消息链接跳转后定位列表行-----------
// 导出 excel
async function onExportXls() {

View File

@@ -8,6 +8,7 @@ import { useRouter } from 'vue-router';
import { REDIRECT_NAME } from '/@/router/constant';
import { useUserStore } from '/@/store/modules/user';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
import { clearImRecordLocateState, stripImRecordQuery } from '/@/views/system/im/imRecordLocate';
export type RouteLocationRawEx = Omit<RouteLocationRaw, 'path'> & { path: PageEnum };
@@ -43,10 +44,17 @@ export function useGo(_router?: Router) {
* @description: redo current page
*/
export const useRedo = (_router?: Router, otherQuery?: Recordable) => {
const { push, currentRoute } = _router || useRouter();
const { query, params = {}, name, fullPath } = unref(currentRoute.value);
const router = _router || useRouter();
const { push, currentRoute, resolve: resolveRoute } = router;
function redo(): Promise<boolean> {
return new Promise((resolve) => {
//update-begin---author:xsl ---date:20260528 for【IM聊天-OA】标签页刷新时取消 IM 定位-----------
clearImRecordLocateState();
const rawRoute = unref(currentRoute.value);
let { query, params = {}, name, fullPath } = rawRoute;
query = stripImRecordQuery(query as Recordable);
fullPath = resolveRoute({ path: rawRoute.path, query, hash: rawRoute.hash }).fullPath;
//update-end---author:xsl ---date:20260528 for【IM聊天-OA】标签页刷新时取消 IM 定位-----------
if (name === REDIRECT_NAME) {
resolve(false);
return;

View File

@@ -2,16 +2,64 @@
import { unref } from 'vue';
import { useWebSocket, WebSocketResult } from '@vueuse/core';
import md5 from 'crypto-js/md5';
import { getToken } from '/@/utils/auth';
import { useGlobSetting } from '/@/hooks/setting';
import { useUserStore } from '/@/store/modules/user';
let result: WebSocketResult<any>;
const listeners = new Map();
let connectedUrl = '';
//update-begin---author:xsl ---date:20260528 for【IM聊天】WS 重连后通知监听方重新拉取消息-----------
let wsConnectionCount = 0;
const reconnectListeners = new Set<() => void>();
export function onWebSocketReconnect(callback: () => void) {
reconnectListeners.add(callback);
return () => reconnectListeners.delete(callback);
}
//update-end---author:xsl ---date:20260528 for【IM聊天】WS 重连后通知监听方重新拉取消息-----------
/**
* 构建系统 WebSocket 地址(含 context-path如 /jeecg-boot
*/
export function buildSystemWebSocketUrl(): string {
const glob = useGlobSetting();
const userStore = useUserStore();
const userInfo = unref(userStore.getUserInfo);
if (!userInfo?.id) {
return '';
}
const token = getToken() || '';
const wsClientId = md5(token).toString();
const wsUserId = `${userInfo.id}_${wsClientId}`;
let base = (glob.domainUrl || '').replace('https://', 'wss://').replace('http://', 'ws://');
base = base.replace(/\/$/, '');
const apiPath = (glob.apiUrl || '/jeecg-boot').replace(/\/$/, '');
const prefix = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
return `${base}${prefix}/websocket/${wsUserId}`;
}
/**
* 确保 WebSocket 已连接(聊天页等场景可主动调用)
*/
export function ensureWebSocketConnected(): void {
const url = buildSystemWebSocketUrl();
if (!url) {
return;
}
if (result?.status?.value === 'OPEN' && connectedUrl === url) {
return;
}
connectWebSocket(url);
}
/**
* 开启 WebSocket 链接,全局只需执行一次
* @param url
*/
export function connectWebSocket(url: string) {
connectedUrl = url;
// 代码逻辑说明: v2.4.6 的 websocket 服务端,存在性能和安全问题。 #3278
const token = (getToken() || '') as string;
result = useWebSocket(url, {
@@ -28,7 +76,15 @@ export function connectWebSocket(url: string) {
protocols: [token],
// 代码逻辑说明: [issues/6662] 演示系统socket总断换一个写法
onConnected: function (ws) {
wsConnectionCount++;
console.log('[WebSocket] 连接成功', ws);
//update-begin---author:xsl ---date:20260528 for【IM聊天】WS 重连后通知监听方重新拉取消息-----------
if (wsConnectionCount > 1) {
reconnectListeners.forEach((cb) => {
try { cb(); } catch (err) { console.error(err); }
});
}
//update-end---author:xsl ---date:20260528 for【IM聊天】WS 重连后通知监听方重新拉取消息-----------
},
onDisconnected: function (ws, event) {
console.log('[WebSocket] 连接断开:', ws, event);

View File

@@ -0,0 +1,42 @@
<template>
<Tooltip :title="tooltipTitle" placement="bottom" :mouseEnterDelay="0.5">
<span :class="`${prefixCls}-action__item refresh-cache-item`" class="refresh-cache-btn" @click="clearCache">
<SyncOutlined :spin="loading" />
</span>
</Tooltip>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Tooltip } from 'ant-design-vue';
import { SyncOutlined } from '@ant-design/icons-vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useRefreshCache } from './useRefreshCache';
export default defineComponent({
name: 'RefreshCache',
components: { Tooltip, SyncOutlined },
setup() {
const { prefixCls } = useDesign('layout-header');
const { loading, clearCache, tooltipTitle } = useRefreshCache();
return {
prefixCls,
loading,
clearCache,
tooltipTitle,
};
},
});
</script>
<style lang="less" scoped>
.refresh-cache-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 12px;
font-size: 16px;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<div :class="[prefixCls, { 'is-disabled': imPageActive }]" @click="openChat">
<MessageOutlined />
<ImChatModal @register="registerModal" />
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted } from 'vue';
import { MessageOutlined } from '@ant-design/icons-vue';
import { useModal } from '/@/components/Modal';
import { useDesign } from '/@/hooks/web/useDesign';
import ImChatModal from '/@/views/system/im/ImChatModal.vue';
import { prefetchImChatData } from '/@/views/system/im/imCache';
import { useImChat } from '/@/views/system/im/useImChat';
import { refreshImUnread } from '/@/views/system/im/useImUnread';
import { useImChatPageActive } from '/@/views/system/im/imSession';
export default defineComponent({
name: 'HeaderImChat',
components: {
MessageOutlined,
ImChatModal,
},
setup() {
const { prefixCls } = useDesign('header-im-chat');
const [registerModal, { openModal }] = useModal();
const imPageActive = useImChatPageActive();
const { openChatModal } = useImChat();
function openChat() {
//update-begin---author:xsl ---date:20260528 for【IM聊天-OA】头部打开 IM 时快照当前功能页名称-----------
openChatModal(openModal);
//update-end---author:xsl ---date:20260528 for【IM聊天-OA】头部打开 IM 时快照当前功能页名称-----------
}
onMounted(() => {
prefetchImChatData();
refreshImUnread(true);
});
return {
prefixCls,
imPageActive,
openChat,
registerModal,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-header-im-chat';
.@{prefix-cls} {
cursor: pointer;
padding: 0 10px;
font-size: 18px;
display: inline-flex;
align-items: center;
svg {
width: 0.9em;
}
&.is-disabled {
cursor: not-allowed;
opacity: 0.45;
}
}
</style>

View File

@@ -14,3 +14,7 @@ export const ErrorAction = createAsyncComponent(() => import('./ErrorAction.vue'
export const LockScreen = createAsyncComponent(() => import('./LockScreen.vue'));
export { FullScreen };
export { default as RefreshCache } from './RefreshCache.vue';
export const ImChat = createAsyncComponent(() => import('./im-chat/index.vue'));

View File

@@ -1,19 +1,19 @@
<template>
<div :class="prefixCls">
<Badge :count="messageCount" :overflowCount="9" :offset="[-4, 18]" :numberStyle="numberStyle" @click="clickBadge('')">
<Badge :count="messageCount" :overflowCount="99" :offset="[-4, 18]" :numberStyle="numberStyle" @click="clickBadge('')">
<BellOutlined />
</Badge>
<DynamicNotice ref="dynamicNoticeRef" v-bind="dynamicNoticeProps" />
<DetailModal @register="registerDetail" />
<sys-message-modal @register="registerMessageModal" @refresh="reloadCount" :messageCount="messageCount"></sys-message-modal>
<sys-message-modal @register="registerMessageModal" @refresh="reloadCount" :systemMessageCount="systemMessageCount"></sys-message-modal>
<!-- 修改密码弹窗 -->
<ChangePasswordModal @register="changePwdModal"></ChangePasswordModal>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, unref, reactive, onMounted, getCurrentInstance } from 'vue';
import { computed, defineComponent, ref, unref, reactive, onMounted, onUnmounted, getCurrentInstance } from 'vue';
import { Popover, Tabs, Badge } from 'ant-design-vue';
import { BellOutlined } from '@ant-design/icons-vue';
// import { tabListData } from './data';
@@ -25,16 +25,16 @@
import { useDesign } from '/@/hooks/web/useDesign';
import { useGlobSetting } from '/@/hooks/setting';
import { useUserStore } from '/@/store/modules/user';
import { connectWebSocket, onWebSocket } from '/@/hooks/web/useWebSocket';
import { connectWebSocket, onWebSocket, offWebSocket, buildSystemWebSocketUrl } from '/@/hooks/web/useWebSocket';
import { readAllMsg } from '/@/views/monitor/mynews/mynews.api';
import { getToken } from '/@/utils/auth';
import md5 from 'crypto-js/md5';
import { useRouter } from 'vue-router';
import SysMessageModal from '/@/views/system/message/components/SysMessageModal.vue'
import ChangePasswordModal from './ChangePasswordModal.vue'
import { ElectronEnum } from '/@/enums/jeecgEnum';
import { defHttp } from "@/utils/http/axios";
import { handleImChatSocket } from '/@/views/system/im/imCache';
import { useImUnread } from '/@/views/system/im/useImUnread';
export default defineComponent({
components: {
@@ -85,11 +85,20 @@
}
const popoverVisible = ref<boolean>(false);
const systemMessageCount = ref(0);
const { totalUnread: imUnreadCount, conversationUnreadCount: imConversationUnreadCount, refreshImUnread: refreshImUnreadCount } = useImUnread();
const messageCount = computed(() => (systemMessageCount.value || 0) + (imConversationUnreadCount.value || 0));
onMounted(() => {
initWebSocket();
refreshImUnreadCount(true);
loadData();
});
onUnmounted(() => {
offWebSocket(onWebSocketMessage);
});
const messageCount = ref<number>(0)
function mapAnnouncement(item) {
return {
...item,
@@ -113,7 +122,7 @@
let msgCount = await getUnreadMessageCount();
// 代码逻辑说明: 【QQYUN-12162】OA项目改造系统重消息拆分目前消息都在一起 需按分类进行拆分---
unReadNum.value = msgCount;
messageCount.value = msgCount.count?msgCount.count:0;
systemMessageCount.value = msgCount.count ? msgCount.count : 0;
// 代码逻辑说明: 【JHHB-13】桌面应用消息通知
if (glob.isElectronPlatform) {
window[ElectronEnum.ELECTRON_API].sendNotifyFlash(messageCount.value);
@@ -124,7 +133,21 @@
}
}
function onWebSocketMessage(data) {
if (data.cmd === 'chat') {
handleImChatSocket(data);
refreshImUnreadCount(false);
return;
}
if (data.cmd === 'topic' || data.cmd === 'user') {
if (data.noticeType) {
noticeType.value = data.noticeType;
}
notification(data);
loadData();
window.setTimeout(() => loadData(), 800);
}
}
function onNoticeClick(record) {
try {
@@ -148,35 +171,17 @@
// 初始化 WebSocket
function initWebSocket() {
let token = getToken();
//将登录token生成一个短的标识
let wsClientId = md5(token);
let userId = unref(userStore.getUserInfo).id + "_" + wsClientId;
// WebSocket与普通的请求所用协议有所不同ws等同于httpwss等同于https
let url = glob.domainUrl?.replace('https://', 'wss://').replace('http://', 'ws://') + '/websocket/' + userId;
const url = buildSystemWebSocketUrl();
if (!url) {
return;
}
connectWebSocket(url);
onWebSocket(onWebSocketMessage);
}
function onWebSocketMessage(data) {
if (data.cmd === 'topic' || data.cmd === 'user') {
// 代码逻辑说明: VUEN-1674【严重bug】系统通知为什么必须刷新右上角才提示
if(data.noticeType){
noticeType.value = data.noticeType;
}
//后台保存数据太慢 前端延迟刷新消息
setTimeout(()=>{
// 代码逻辑说明: 【JHHB-13】桌面应用消息通知
notification(data);
loadData();
}, 1000)
}
}
// 桌面应用通知
function notification(data) {
if (glob.isElectronPlatform && (data.noticeType || data.cmd == 'email')) {
// 流程、文件、日程、系统、会议
// flow、file、plan、system、meeting
let title = '';
let msgTxt = '';
let path = '';
@@ -203,6 +208,7 @@
window[ElectronEnum.ELECTRON_API].sendNotification(`有新的${title}消息`, msgTxt, path);
}
}
// 清空消息
function onEmptyNotify() {
popoverVisible.value = false;
@@ -212,6 +218,7 @@
try {
await editCementSend(id);
await loadData();
refreshImUnreadCount(true);
} catch (e) {
console.error(e);
}

View File

@@ -0,0 +1,46 @@
import { ref } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { useUserStore } from '/@/store/modules/user';
import { refreshCache, queryAllDictItems } from '/@/views/system/dict/dict.api';
import { refreshDragCache } from '/@/api/common/api';
import { DB_DICT_DATA_KEY } from '/@/enums/cacheEnum';
import { removeAuthCache, setAuthCache } from '/@/utils/auth';
/** 顶部/用户菜单共用的刷新缓存逻辑 */
export function useRefreshCache() {
const { t } = useI18n();
const { createMessage } = useMessage();
const userStore = useUserStore();
const loading = ref(false);
async function clearCache() {
if (loading.value) {
return;
}
loading.value = true;
try {
const result = await refreshCache();
await refreshDragCache();
if (result.success) {
const res = await queryAllDictItems();
removeAuthCache(DB_DICT_DATA_KEY);
setAuthCache(DB_DICT_DATA_KEY, res.result);
createMessage.success(t('layout.header.refreshCacheComplete'));
userStore.setAllDictItems(res.result);
} else {
createMessage.error(t('layout.header.refreshCacheFailure'));
}
} catch {
createMessage.error(t('layout.header.refreshCacheFailure'));
} finally {
loading.value = false;
}
}
return {
loading,
clearCache,
tooltipTitle: t('layout.header.dropdownItemRefreshCache'),
};
}

View File

@@ -44,7 +44,6 @@
import { useI18n } from '/@/hooks/web/useI18n';
import { useDesign } from '/@/hooks/web/useDesign';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/src/hooks/web/useMessage';
import { useGo } from '/@/hooks/web/usePage';
import headerImg from '/@/assets/images/header.jpg';
import { propTypes } from '/@/utils/propTypes';
@@ -52,15 +51,11 @@
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { refreshCache, queryAllDictItems } from '/@/views/system/dict/dict.api';
import { DB_DICT_DATA_KEY } from '/src/enums/cacheEnum';
import { removeAuthCache, setAuthCache } from '/src/utils/auth';
import { useRefreshCache } from '../useRefreshCache';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { getRefPromise } from '/@/utils/index';
import { refreshDragCache } from "@/api/common/api";
type MenuEvent = 'logout' | 'doc' | 'lock' | 'cache' | 'depart' | 'defaultHomePage' | 'password' | 'account';
const { createMessage } = useMessage();
export default defineComponent({
name: 'UserDropdown',
components: {
@@ -84,6 +79,7 @@
const passwordVisible = ref(false);
const lockActionVisible = ref(false);
const lockActionRef = ref(null);
const { clearCache } = useRefreshCache();
const getUserInfo = computed(() => {
const { realname = '', avatar, desc } = userStore.getUserInfo || {};
@@ -119,22 +115,6 @@
openWindow(SITE_URL);
}
// 清除缓存
async function clearCache() {
const result = await refreshCache();
const dragRes = await refreshDragCache();
console.log('dragRes', dragRes);
if (result.success) {
const res = await queryAllDictItems();
removeAuthCache(DB_DICT_DATA_KEY);
setAuthCache(DB_DICT_DATA_KEY, res.result);
createMessage.success(t('layout.header.refreshCacheComplete'));
// 代码逻辑说明: 【issues/7433】vue3 数据字典优化建议
userStore.setAllDictItems(res.result);
} else {
createMessage.error(t('layout.header.refreshCacheFailure'));
}
}
// 切换部门
function updateCurrentDepart() {
loginSelectRef.value.show();

View File

@@ -29,10 +29,14 @@
<Notify v-if="getShowNotice" :class="`${prefixCls}-action__item notify-item`" />
<ImChat :class="`${prefixCls}-action__item im-chat-item`" />
<FullScreen v-if="getShowFullScreen" :class="`${prefixCls}-action__item fullscreen-item`" />
<LockScreen v-if="getUseLockPage" />
<RefreshCache />
<AppLocalePicker v-if="getShowLocalePicker" :reload="true" :showText="false" :class="`${prefixCls}-action__item`" />
<UserDropDown :theme="getHeaderTheme" />
@@ -64,7 +68,7 @@
import { SettingButtonPositionEnum } from '/@/enums/appEnum';
import { AppLocalePicker } from '/@/components/Application';
import { UserDropDown, LayoutBreadcrumb, FullScreen, Notify, ErrorAction, LockScreen } from './components';
import { UserDropDown, LayoutBreadcrumb, FullScreen, Notify, ImChat, ErrorAction, LockScreen, RefreshCache } from './components';
import { useAppInject } from '/@/hooks/web/useAppInject';
import { useDesign } from '/@/hooks/web/useDesign';
@@ -86,9 +90,11 @@
LayoutBreadcrumb,
LayoutMenu,
UserDropDown,
RefreshCache,
AppLocalePicker,
FullScreen,
Notify,
ImChat,
AppSearch,
ErrorAction,
LockScreen,

View File

@@ -198,6 +198,8 @@ export const useUserStore = defineStore({
await this.setLoginInfo({ ...data, isLogin: true });
// 代码逻辑说明: 登录成功后缓存拖拽模块的接口前缀
localStorage.setItem(JDragConfigEnum.DRAG_BASE_URL, useGlobSetting().domainUrl);
// 登录后异步预取 IM 聊天数据,减少打开聊天时的等待
import('/@/views/system/im/imCache').then(({ prefetchImChatData }) => prefetchImChatData());
// 代码逻辑说明: 修复登录成功后,没有正确重定向的问题
let redirect = router.currentRoute.value?.query?.redirect as string;
@@ -284,6 +286,9 @@ export const useUserStore = defineStore({
}
}
// 退出登录前清除 IM 聊天缓存
import('/@/views/system/im/imCache').then(({ clearImCache }) => clearImCache());
// let username:any = this.userInfo && this.userInfo.username;
// if(username){
// removeAuthCache(username)

View File

@@ -5,6 +5,15 @@
<a-button type="primary" v-auth="'mes:mes_material:add'" @click="handleAdd" preIcon="ant-design:plus-outlined">新增</a-button>
<a-button type="primary" v-auth="'mes:mes_material:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
<j-upload-button type="primary" v-auth="'mes:mes_material:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-button
type="primary"
v-auth="'mes:mes_material:rubberQuickTestInspect'"
preIcon="ant-design:experiment-outlined"
:disabled="selectedRowKeys.length === 0"
@click="handleRubberQuickTest"
>
检验
</a-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
@@ -29,7 +38,10 @@ import { useListPage } from '/@/hooks/system/useListPage';
import MesMaterialModal from './modules/MesMaterialModal.vue';
import { columns, searchFormSchema } from './MesMaterial.data';
import { batchDelete, deleteOne, getExportUrl, getImportUrl, list } from './MesMaterial.api';
import { batchFromMaterial } from '/@/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecord.api';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
const [registerModal, { openModal }] = useModal();
const { tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
@@ -63,6 +75,18 @@ async function batchHandleDelete() {
function handleSuccess() {
reload();
}
async function handleRubberQuickTest() {
if (!selectedRowKeys.value?.length) {
createMessage.warning('请至少选择一条胶料');
return;
}
try {
await batchFromMaterial({ materialIds: [...selectedRowKeys.value] });
createMessage.success('快检记录已生成,请到「胶料快检记录」中编辑');
} catch (e: any) {
createMessage.error(e?.message || '生成失败');
}
}
function getTableAction(record) {
return [{ label: '编辑', onClick: handleEdit.bind(null, record), auth: 'mes:mes_material:edit' }];
}

View File

@@ -0,0 +1,442 @@
<template>
<div class="im-biz-record-message">
<div v-if="showNoPermission" class="im-biz-record-no-permission">暂无当前消息权限</div>
<template v-else>
<template v-if="isSingleItem">
<div class="im-biz-record-item">
<div class="im-biz-record-table-wrap">
<table class="im-biz-record-table im-biz-record-table--detail">
<tbody>
<tr v-for="field in resolveItemFields(singleItem)" :key="field.label">
<th>{{ field.label }}</th>
<td>{{ field.value }}</td>
</tr>
</tbody>
</table>
</div>
<a class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
<Icon icon="ant-design:link-outlined" />
<span>查看并定位到此数据</span>
</a>
</div>
</template>
<!-- 多条列表表第一列为定位链接 -->
<template v-else>
<div class="im-biz-record-table-wrap im-biz-record-table-wrap--list">
<table class="im-biz-record-table im-biz-record-table--list">
<thead>
<tr>
<th class="im-biz-record-link-col">链接</th>
<th v-for="columnLabel in listColumnLabels" :key="columnLabel">{{ columnLabel }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in payload.items" :key="item.recordId || index">
<td class="im-biz-record-link-col">
<a class="im-biz-record-link" @click.prevent="handleLinkClick(item.linkPath)">
<Icon icon="ant-design:link-outlined" />
<span>定位</span>
</a>
</td>
<td v-for="columnLabel in listColumnLabels" :key="columnLabel">
{{ getFieldValue(item, columnLabel) }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
<div v-if="showPeerNoPermissionTip" class="im-biz-record-peer-tip">对方无此功能权限</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import type { ImBizRecordItem, ImBizRecordPayload } from './imBizRecordMessage';
import {
getImBizRecordFieldValueByLabel,
resolveImBizRecordItemFields,
resolveImBizRecordListColumnLabels,
} from './imBizRecordMessage';
import { navigateImBizRecordLink } from './imRecordLocate';
import { hasImBizRecordPagePermission } from './imBizRecordPermission';
defineOptions({ name: 'ImBizRecordMessageContent' });
const props = defineProps<{
payload: ImBizRecordPayload;
mine?: boolean;
receiverHasBizPagePermission?: boolean;
}>();
const isSingleItem = computed(() => props.payload.items.length === 1);
const singleItem = computed(() => props.payload.items[0]);
const listColumnLabels = computed(() => resolveImBizRecordListColumnLabels(props.payload.items));
const hasPagePermission = computed(() => hasImBizRecordPagePermission(props.payload.pagePath));
const showNoPermission = computed(() => !props.mine && !hasPagePermission.value);
const showPeerNoPermissionTip = computed(
() => !!props.mine && props.receiverHasBizPagePermission === false,
);
function resolveItemFields(item: ImBizRecordItem) {
return resolveImBizRecordItemFields(item);
}
function getFieldValue(item: ImBizRecordItem, label: string) {
return getImBizRecordFieldValueByLabel(item, label);
}
async function handleLinkClick(linkPath: string) {
if (!linkPath || showNoPermission.value) {
return;
}
await navigateImBizRecordLink(linkPath);
}
</script>
<style lang="less" scoped>
.im-biz-record-message {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 280px;
max-width: 420px;
}
.im-biz-record-no-permission {
padding: 12px 10px;
font-size: 13px;
line-height: 1.5;
color: #8c8c8c;
text-align: center;
}
.im-biz-record-peer-tip {
display: inline-flex;
align-items: center;
align-self: flex-start;
margin-top: 4px;
padding: 2px 8px;
border-radius: 10px;
background: #fff7e6;
border: 1px solid #ffd591;
font-size: 12px;
line-height: 1.5;
color: #d46b08;
}
.im-biz-record-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.im-biz-record-table-wrap {
overflow: hidden;
border: 1px solid #f0f0f0;
border-radius: 6px;
background: #fff;
&--list {
overflow-x: auto;
}
}
.im-biz-record-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
line-height: 1.5;
th,
td {
padding: 8px 10px;
border-bottom: 1px solid #f0f0f0;
vertical-align: top;
word-break: break-word;
}
tr:last-child {
th,
td {
border-bottom: none;
}
}
&--detail {
table-layout: fixed;
th {
width: 38%;
background: #fafafa;
color: #595959;
font-weight: 500;
text-align: left;
}
td {
color: #262626;
background: #fff;
}
}
&--list {
min-width: 100%;
table-layout: auto;
thead th {
background: #fafafa;
color: #595959;
font-weight: 500;
text-align: left;
white-space: nowrap;
}
tbody td {
color: #262626;
background: #fff;
}
}
}
.im-biz-record-link-col {
width: 72px;
min-width: 72px;
white-space: nowrap;
}
.im-biz-record-link {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #1677ff;
text-decoration: underline;
cursor: pointer;
&:hover {
color: #0958d9;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,413 @@
<template>
<div class="im-chat-input-wrap">
<div class="im-chat-input" :class="{ focused: inputFocused, disabled: disabled }">
<a-textarea
ref="textareaRef"
:value="modelValue"
:auto-size="{ minRows: 3, maxRows: 8 }"
placeholder="请输入消息"
:bordered="false"
:disabled="disabled"
class="im-chat-input-textarea"
@update:value="handleInput"
@focus="inputFocused = true"
@blur="handleBlur"
@keydown="handleKeydown"
/>
<div class="im-chat-input-toolbar">
<div class="toolbar-left">
<a-popover
v-model:open="emojiVisible"
trigger="click"
placement="topLeft"
overlay-class-name="im-emoji-popover"
:overlay-style="{ padding: 0 }"
:get-popup-container="getPopupContainer"
>
<template #content>
<Picker
:picker-styles="pickerStyles"
:i18n="emojiI18n"
:data="emojiIndex"
emoji="grinning"
:native="true"
:show-preview="false"
:infinite-scroll="false"
:show-search="true"
:show-skin-tones="false"
set="native"
@select="handleSelectEmoji"
/>
</template>
<button type="button" class="toolbar-btn" title="表情" :disabled="disabled">
<Icon icon="ant-design:smile-outlined" />
</button>
</a-popover>
<a-upload
:show-upload-list="false"
:action="uploadUrl"
:headers="uploadHeaders"
:data="{ biz: 'im/chat' }"
:accept="IM_IMAGE_ACCEPT"
:before-upload="beforeImageUpload"
:disabled="disabled || uploadingImage"
@change="handleImageUploadChange"
>
<button type="button" class="toolbar-btn" title="发送图片" :disabled="disabled || uploadingImage">
<Icon v-if="!uploadingImage" icon="ant-design:picture-outlined" />
<LoadingOutlined v-else spin />
</button>
</a-upload>
</div>
<div class="toolbar-right">
<div class="send-btn-group">
<a-button type="primary" class="send-btn" :disabled="!canSend" :loading="sending" @click="emitSend">
发送(S)
</a-button>
<a-dropdown :trigger="['click']" placement="topRight">
<a-button type="primary" class="send-btn-arrow" :disabled="sending">
<Icon icon="ant-design:down-outlined" />
</a-button>
<template #overlay>
<a-menu :selected-keys="[sendMode]" @click="handleSendModeChange">
<a-menu-item key="enter"> Enter 发送</a-menu-item>
<a-menu-item key="ctrlEnter"> Ctrl+Enter 发送</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</div>
</div>
<div class="im-chat-input-hint">{{ sendModeHint }}</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import type { UploadChangeParam, UploadProps } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { LoadingOutlined } from '@ant-design/icons-vue';
import 'emoji-mart-vue-fast/css/emoji-mart.css';
import { getGloablEmojiIndex } from '/@/components/jeecg/comment/useComment';
import { uploadUrl } from '/@/api/common/api';
import { getHeaders } from '/@/utils/common/compUtils';
import { getEmojiInsertText, IM_IMAGE_ACCEPT, IM_IMAGE_MAX_SIZE } from './imMessageUtil';
type SendMode = 'enter' | 'ctrlEnter';
const SEND_MODE_KEY = 'im-chat-send-mode';
defineOptions({ name: 'ImChatInput' });
const props = withDefaults(
defineProps<{
modelValue: string;
disabled?: boolean;
sending?: boolean;
}>(),
{
disabled: false,
sending: false,
},
);
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
(e: 'send'): void;
(e: 'image-uploaded', imagePath: string): void;
}>();
const emojiI18n = {
categories: {
recent: '最近使用',
smileys: '全部表情',
people: '人物',
nature: '自然',
foods: '食物',
activity: '活动',
places: '地点',
objects: '物品',
symbols: '符号',
flags: '旗帜',
},
search: '搜索表情',
notfound: '未找到表情',
};
const pickerStyles = { width: '360px', border: 'none' };
const emojiIndex = getGloablEmojiIndex();
const uploadHeaders = getHeaders();
const textareaRef = ref<{ resizableTextArea?: { textArea: HTMLTextAreaElement } }>();
const inputFocused = ref(false);
const emojiVisible = ref(false);
const uploadingImage = ref(false);
const sendMode = ref<SendMode>(loadSendMode());
const canSend = computed(() => !props.disabled && !!props.modelValue.trim() && !props.sending);
const sendModeHint = computed(() =>
sendMode.value === 'enter' ? 'Enter / Alt+S 发送Ctrl+Enter 换行' : 'Ctrl+Enter / Alt+S 发送Enter 换行',
);
function loadSendMode(): SendMode {
const saved = localStorage.getItem(SEND_MODE_KEY);
return saved === 'ctrlEnter' ? 'ctrlEnter' : 'enter';
}
function handleSendModeChange({ key }: { key: string }) {
sendMode.value = key as SendMode;
localStorage.setItem(SEND_MODE_KEY, key);
}
function getPopupContainer(triggerNode: HTMLElement) {
return triggerNode.parentElement || document.body;
}
function getTextareaEl() {
return textareaRef.value?.resizableTextArea?.textArea;
}
function handleInput(value: string) {
emit('update:modelValue', value);
}
function handleBlur() {
window.setTimeout(() => {
inputFocused.value = false;
}, 120);
}
function insertAtCursor(text: string) {
const el = getTextareaEl();
const current = props.modelValue || '';
if (!el) {
emit('update:modelValue', current + text);
return;
}
const start = el.selectionStart ?? current.length;
const end = el.selectionEnd ?? start;
const nextValue = current.slice(0, start) + text + current.slice(end);
emit('update:modelValue', nextValue);
const cursor = start + text.length;
window.requestAnimationFrame(() => {
el.focus();
el.setSelectionRange(cursor, cursor);
});
}
function handleSelectEmoji(item: Record<string, any>) {
const text = getEmojiInsertText(item);
if (!text) {
return;
}
insertAtCursor(text);
emojiVisible.value = false;
inputFocused.value = true;
window.requestAnimationFrame(() => getTextareaEl()?.focus());
}
function emitSend() {
if (!canSend.value) {
return;
}
emit('send');
}
function handleKeydown(e: KeyboardEvent) {
if (e.altKey && !e.ctrlKey && !e.shiftKey && (e.key === 's' || e.key === 'S')) {
e.preventDefault();
emitSend();
return;
}
if (e.key !== 'Enter') {
return;
}
const enterSend = sendMode.value === 'enter';
const shouldSend = enterSend ? !e.ctrlKey && !e.shiftKey : e.ctrlKey && !e.shiftKey;
const shouldNewLine = enterSend ? e.ctrlKey || e.shiftKey : !e.ctrlKey || e.shiftKey;
if (shouldSend) {
e.preventDefault();
emitSend();
return;
}
if (shouldNewLine && e.ctrlKey && !e.shiftKey) {
e.preventDefault();
insertAtCursor('\n');
}
}
const beforeImageUpload: UploadProps['beforeUpload'] = (file) => {
if (!file.type?.startsWith('image/')) {
message.warning('仅支持发送图片文件');
return false;
}
if (file.size > IM_IMAGE_MAX_SIZE) {
message.warning('图片大小不能超过10MB');
return false;
}
if (props.disabled) {
message.warning('请先选择聊天对象');
return false;
}
uploadingImage.value = true;
return true;
};
function handleImageUploadChange(info: UploadChangeParam) {
const { file } = info;
if (file.status === 'uploading') {
return;
}
uploadingImage.value = false;
if (file.status === 'error') {
message.error(`${file.name} 上传失败`);
return;
}
if (file.status !== 'done') {
return;
}
const response = file.response as { success?: boolean; message?: string } | undefined;
if (!response?.success) {
message.warning(response?.message || '图片上传失败');
return;
}
const imagePath = response.message;
if (imagePath) {
emit('image-uploaded', imagePath);
}
}
</script>
<style lang="less" scoped>
.im-chat-input-wrap {
width: 100%;
}
.im-chat-input {
border: 1px solid #e8e8e8;
border-radius: 12px;
background: #fff;
overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s;
&.focused {
border-color: #91caff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.08);
}
&.disabled {
background: #fafafa;
opacity: 0.85;
}
}
.im-chat-input-textarea {
padding: 12px 14px 4px !important;
font-size: 14px;
line-height: 1.6;
resize: none;
background: transparent !important;
box-shadow: none !important;
:deep(textarea) {
padding: 0 !important;
}
}
.im-chat-input-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 10px 8px;
gap: 8px;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 2px;
}
.toolbar-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
border: none;
border-radius: 6px;
background: transparent;
color: #666;
font-size: 18px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
&:hover:not(:disabled) {
background: #f5f5f5;
color: #1677ff;
}
&:disabled {
cursor: not-allowed;
opacity: 0.45;
}
}
.toolbar-right {
display: flex;
align-items: center;
}
.send-btn-group {
display: inline-flex;
align-items: stretch;
border-radius: 8px;
overflow: hidden;
.send-btn {
min-width: 80px;
border-radius: 8px 0 0 8px;
font-weight: 500;
border-right: 1px solid rgba(255, 255, 255, 0.25);
}
.send-btn-arrow {
width: 32px;
padding: 0;
border-radius: 0 8px 8px 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
}
.im-chat-input-hint {
margin-top: 6px;
padding: 0 4px;
text-align: right;
font-size: 12px;
color: #bfbfbf;
line-height: 1.4;
user-select: none;
}
</style>
<style lang="less">
.im-emoji-popover {
.ant-popover-inner-content {
padding: 0;
}
.emoji-mart-bar {
border-bottom: 1px solid #f0f0f0;
}
.emoji-mart-emoji span {
line-height: 1;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More