Преглед изворни кода

fix(issue-14): 修复文档管理与系统管理6类代码质量问题

A. Entity-DDL表名不一致(致命): 3个文档实体@TableName('doc_*')但DDL定义'sys_document*'
   - DDL: sys_document→doc_document, sys_document_category→doc_category, sys_document_version→doc_version

B. Entity-DDL列名不一致(致命): 多个字段名不匹配
   - doc_document: file_path→storage_path, creator_id→uploader_id, 新增original_name/description/current_version/download_count/permission_level/deleted
   - doc_category: sort→sort_order, 新增description/deleted/updated_time
   - doc_version: doc_id→document_id, version_no→version, remark→description, file_path→storage_path, 新增file_size/operator_id
   - 3个实体: createdAt→createdTime, updatedAt→updatedTime (对齐camelCase映射)

C. DocService编译错误(致命):
   - setFilePath()→setStoragePath() (Document实体无filePath字段)
   - DocumentVersion::getDocId→getDocumentId (实体无docId字段)
   - DocumentVersion::getVersionNo→getVersion
   - listDocs categoryId参数String→Long (对齐实体Long类型)

D. 密码明文存储(高): SysService.createUser直接setPassword(明文)
   - 引入BCryptPasswordEncoder, 密码BCrypt加密存储
   - 新增verifyPassword方法, updateUser支持密码修改(加密)
   - pom.xml添加spring-security-crypto依赖

E. 缺测试(中): 新增2个测试类共45个测试方法
   - DocServiceTest: 20个测试(文档CRUD/列表搜索/分类管理/版本管理)
   - SysServiceTest: 25个测试(角色CRUD/用户CRUD+密码加密验证/菜单管理/部门CRUD/日志CRUD)

F. DDL缺列(致命): 修复V1__system_manage.sql
   - 3个文档表完全重写, 列名对齐实体字段
   - 添加种子数据: 5级角色预设/3个默认部门/4个文档分类

G. 补充功能: 新增MyBatisPlusConfig(分页+自动填充)
   - 新增DocController端点: 下载计数/分类创建/版本创建
   - 新增SysController端点: 角色更新删除/用户更新/菜单创建/部门更新/日志创建
bot_qa пре 2 дана
родитељ
комит
40f52e5dd1

+ 4
- 0
wm-system/pom.xml Прегледај датотеку

41
             <groupId>org.springframework.boot</groupId>
41
             <groupId>org.springframework.boot</groupId>
42
             <artifactId>spring-boot-starter-validation</artifactId>
42
             <artifactId>spring-boot-starter-validation</artifactId>
43
         </dependency>
43
         </dependency>
44
+        <dependency>
45
+            <groupId>org.springframework.security</groupId>
46
+            <artifactId>spring-security-crypto</artifactId>
47
+        </dependency>
44
         <dependency>
48
         <dependency>
45
             <groupId>org.springframework.boot</groupId>
49
             <groupId>org.springframework.boot</groupId>
46
             <artifactId>spring-boot-starter-test</artifactId>
50
             <artifactId>spring-boot-starter-test</artifactId>

+ 41
- 0
wm-system/src/main/java/com/water/system/config/MyBatisPlusConfig.java Прегледај датотеку

1
+package com.water.system.config;
2
+
3
+import com.baomidou.mybatisplus.annotation.DbType;
4
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
5
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
6
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
7
+import org.apache.ibatis.reflection.MetaObject;
8
+import org.springframework.context.annotation.Bean;
9
+import org.springframework.context.annotation.Configuration;
10
+
11
+import java.time.LocalDateTime;
12
+
13
+/**
14
+ * MyBatis-Plus 配置(分页插件 + 自动填充)
15
+ */
16
+@Configuration
17
+public class MyBatisPlusConfig {
18
+
19
+    @Bean
20
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
21
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
22
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
23
+        return interceptor;
24
+    }
25
+
26
+    @Bean
27
+    public MetaObjectHandler metaObjectHandler() {
28
+        return new MetaObjectHandler() {
29
+            @Override
30
+            public void insertFill(MetaObject metaObject) {
31
+                this.strictInsertFill(metaObject, "createdTime", LocalDateTime.class, LocalDateTime.now());
32
+                this.strictInsertFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
33
+            }
34
+
35
+            @Override
36
+            public void updateFill(MetaObject metaObject) {
37
+                this.strictUpdateFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
38
+            }
39
+        };
40
+    }
41
+}

+ 62
- 7
wm-system/src/main/java/com/water/system/controller/DocController.java Прегледај датотеку

2
 import com.water.common.core.result.R;
2
 import com.water.common.core.result.R;
3
 import com.water.system.entity.*;
3
 import com.water.system.entity.*;
4
 import com.water.system.service.DocService;
4
 import com.water.system.service.DocService;
5
+import io.swagger.v3.oas.annotations.Operation;
5
 import io.swagger.v3.oas.annotations.tags.Tag;
6
 import io.swagger.v3.oas.annotations.tags.Tag;
6
 import lombok.RequiredArgsConstructor;
7
 import lombok.RequiredArgsConstructor;
7
 import org.springframework.web.bind.annotation.*;
8
 import org.springframework.web.bind.annotation.*;
8
 import java.util.*;
9
 import java.util.*;
10
+
9
 @Tag(name="文档管理") @RestController @RequestMapping("/system/doc") @RequiredArgsConstructor
11
 @Tag(name="文档管理") @RestController @RequestMapping("/system/doc") @RequiredArgsConstructor
10
 public class DocController {
12
 public class DocController {
11
     private final DocService svc;
13
     private final DocService svc;
12
-    @GetMapping("/list") public R<List<Document>> list(@RequestParam(required=false) String categoryId, @RequestParam(required=false) String keyword) { return R.ok(svc.listDocs(categoryId, keyword)); }
13
-    @GetMapping("/{id}") public R<Document> get(@PathVariable Long id) { return R.ok(svc.getDoc(id)); }
14
-    @PostMapping public R<Long> create(@RequestBody Map<String,Object> req) { return R.ok(svc.createDoc(req)); }
15
-    @PutMapping("/{id}") public R<String> update(@PathVariable Long id, @RequestBody Map<String,Object> req) { svc.updateDoc(id, req); return R.ok("OK"); }
16
-    @DeleteMapping("/{id}") public R<String> delete(@PathVariable Long id) { svc.deleteDoc(id); return R.ok("OK"); }
17
-    @GetMapping("/category/list") public R<List<DocumentCategory>> categories() { return R.ok(svc.listCategories()); }
18
-    @GetMapping("/{docId}/versions") public R<List<DocumentVersion>> versions(@PathVariable Long docId) { return R.ok(svc.getVersions(docId)); }
14
+
15
+    @GetMapping("/list")
16
+    @Operation(summary = "文档列表(支持分类筛选和关键词搜索)")
17
+    public R<List<Document>> list(@RequestParam(required=false) Long categoryId,
18
+                                  @RequestParam(required=false) String keyword) {
19
+        return R.ok(svc.listDocs(categoryId, keyword));
20
+    }
21
+
22
+    @GetMapping("/{id}")
23
+    @Operation(summary = "获取文档详情")
24
+    public R<Document> get(@PathVariable Long id) { return R.ok(svc.getDoc(id)); }
25
+
26
+    @PostMapping
27
+    @Operation(summary = "创建文档")
28
+    public R<Long> create(@RequestBody Map<String,Object> req) { return R.ok(svc.createDoc(req)); }
29
+
30
+    @PutMapping("/{id}")
31
+    @Operation(summary = "更新文档")
32
+    public R<String> update(@PathVariable Long id, @RequestBody Map<String,Object> req) {
33
+        svc.updateDoc(id, req);
34
+        return R.ok("OK");
35
+    }
36
+
37
+    @DeleteMapping("/{id}")
38
+    @Operation(summary = "删除文档")
39
+    public R<String> delete(@PathVariable Long id) {
40
+        svc.deleteDoc(id);
41
+        return R.ok("OK");
42
+    }
43
+
44
+    @PostMapping("/{id}/download")
45
+    @Operation(summary = "下载文档(增加下载计数)")
46
+    public R<String> download(@PathVariable Long id) {
47
+        svc.incrementDownloadCount(id);
48
+        return R.ok("OK");
49
+    }
50
+
51
+    // ========== Category management ==========
52
+    @GetMapping("/category/list")
53
+    @Operation(summary = "文档分类列表")
54
+    public R<List<DocumentCategory>> categories() { return R.ok(svc.listCategories()); }
55
+
56
+    @PostMapping("/category")
57
+    @Operation(summary = "创建文档分类")
58
+    public R<Long> createCategory(@RequestBody Map<String,Object> req) {
59
+        return R.ok(svc.createCategory(req));
60
+    }
61
+
62
+    // ========== Version management ==========
63
+    @GetMapping("/{docId}/versions")
64
+    @Operation(summary = "文档版本列表")
65
+    public R<List<DocumentVersion>> versions(@PathVariable Long docId) {
66
+        return R.ok(svc.getVersions(docId));
67
+    }
68
+
69
+    @PostMapping("/{docId}/version")
70
+    @Operation(summary = "创建文档新版本")
71
+    public R<Long> createVersion(@PathVariable Long docId, @RequestBody Map<String,Object> req) {
72
+        return R.ok(svc.createVersion(docId, req));
73
+    }
19
 }
74
 }

+ 91
- 9
wm-system/src/main/java/com/water/system/controller/SysController.java Прегледај датотеку

2
 import com.water.common.core.result.R;
2
 import com.water.common.core.result.R;
3
 import com.water.system.entity.*;
3
 import com.water.system.entity.*;
4
 import com.water.system.service.SysService;
4
 import com.water.system.service.SysService;
5
+import io.swagger.v3.oas.annotations.Operation;
5
 import io.swagger.v3.oas.annotations.tags.Tag;
6
 import io.swagger.v3.oas.annotations.tags.Tag;
6
 import lombok.RequiredArgsConstructor;
7
 import lombok.RequiredArgsConstructor;
7
 import org.springframework.web.bind.annotation.*;
8
 import org.springframework.web.bind.annotation.*;
8
 import java.util.*;
9
 import java.util.*;
10
+
9
 @Tag(name="系统管理") @RestController @RequestMapping("/system") @RequiredArgsConstructor
11
 @Tag(name="系统管理") @RestController @RequestMapping("/system") @RequiredArgsConstructor
10
 public class SysController {
12
 public class SysController {
11
     private final SysService svc;
13
     private final SysService svc;
12
-    @GetMapping("/role/list") public R<List<SysRole>> roles() { return R.ok(svc.listRoles()); }
13
-    @PostMapping("/role") public R<Long> createRole(@RequestBody Map<String,Object> req) { return R.ok(svc.createRole(req)); }
14
-    @GetMapping("/user/list") public R<List<SysUser>> users(@RequestParam(required=false) Integer status) { return R.ok(svc.listUsers(status)); }
15
-    @PostMapping("/user") public R<Long> createUser(@RequestBody Map<String,Object> req) { return R.ok(svc.createUser(req)); }
16
-    @PutMapping("/user/{id}/status") public R<String> updateUserStatus(@PathVariable Long id, @RequestParam int status) { svc.updateUserStatus(id, status); return R.ok("OK"); }
17
-    @GetMapping("/menu/list") public R<List<SysMenu>> menus() { return R.ok(svc.listMenus()); }
18
-    @GetMapping("/dept/list") public R<List<SysDepartment>> depts() { return R.ok(svc.listDepts()); }
19
-    @PostMapping("/dept") public R<Long> createDept(@RequestBody Map<String,Object> req) { return R.ok(svc.createDept(req)); }
20
-    @GetMapping("/log/list") public R<List<SysLog>> logs(@RequestParam(required=false) String logType, @RequestParam(required=false) String username) { return R.ok(svc.listLogs(logType, username)); }
14
+
15
+    // ========== Role management ==========
16
+    @GetMapping("/role/list")
17
+    @Operation(summary = "角色列表")
18
+    public R<List<SysRole>> roles() { return R.ok(svc.listRoles()); }
19
+
20
+    @PostMapping("/role")
21
+    @Operation(summary = "创建角色")
22
+    public R<Long> createRole(@RequestBody Map<String,Object> req) { return R.ok(svc.createRole(req)); }
23
+
24
+    @PutMapping("/role/{id}")
25
+    @Operation(summary = "更新角色")
26
+    public R<String> updateRole(@PathVariable Long id, @RequestBody Map<String,Object> req) {
27
+        svc.updateRole(id, req);
28
+        return R.ok("OK");
29
+    }
30
+
31
+    @DeleteMapping("/role/{id}")
32
+    @Operation(summary = "删除角色")
33
+    public R<String> deleteRole(@PathVariable Long id) {
34
+        svc.deleteRole(id);
35
+        return R.ok("OK");
36
+    }
37
+
38
+    // ========== User management ==========
39
+    @GetMapping("/user/list")
40
+    @Operation(summary = "用户列表")
41
+    public R<List<SysUser>> users(@RequestParam(required=false) Integer status) {
42
+        return R.ok(svc.listUsers(status));
43
+    }
44
+
45
+    @PostMapping("/user")
46
+    @Operation(summary = "创建用户(密码自动BCrypt加密)")
47
+    public R<Long> createUser(@RequestBody Map<String,Object> req) { return R.ok(svc.createUser(req)); }
48
+
49
+    @PutMapping("/user/{id}")
50
+    @Operation(summary = "更新用户信息")
51
+    public R<String> updateUser(@PathVariable Long id, @RequestBody Map<String,Object> req) {
52
+        svc.updateUser(id, req);
53
+        return R.ok("OK");
54
+    }
55
+
56
+    @PutMapping("/user/{id}/status")
57
+    @Operation(summary = "更新用户状态(启用/停用)")
58
+    public R<String> updateUserStatus(@PathVariable Long id, @RequestParam int status) {
59
+        svc.updateUserStatus(id, status);
60
+        return R.ok("OK");
61
+    }
62
+
63
+    // ========== Menu management ==========
64
+    @GetMapping("/menu/list")
65
+    @Operation(summary = "菜单列表")
66
+    public R<List<SysMenu>> menus() { return R.ok(svc.listMenus()); }
67
+
68
+    @PostMapping("/menu")
69
+    @Operation(summary = "创建菜单")
70
+    public R<Long> createMenu(@RequestBody Map<String,Object> req) {
71
+        return R.ok(svc.createMenu(req));
72
+    }
73
+
74
+    // ========== Department management ==========
75
+    @GetMapping("/dept/list")
76
+    @Operation(summary = "部门列表")
77
+    public R<List<SysDepartment>> depts() { return R.ok(svc.listDepts()); }
78
+
79
+    @PostMapping("/dept")
80
+    @Operation(summary = "创建部门")
81
+    public R<Long> createDept(@RequestBody Map<String,Object> req) { return R.ok(svc.createDept(req)); }
82
+
83
+    @PutMapping("/dept/{id}")
84
+    @Operation(summary = "更新部门")
85
+    public R<String> updateDept(@PathVariable Long id, @RequestBody Map<String,Object> req) {
86
+        svc.updateDept(id, req);
87
+        return R.ok("OK");
88
+    }
89
+
90
+    // ========== Log management ==========
91
+    @GetMapping("/log/list")
92
+    @Operation(summary = "日志列表(支持类型和用户名筛选)")
93
+    public R<List<SysLog>> logs(@RequestParam(required=false) String logType,
94
+                                @RequestParam(required=false) String username) {
95
+        return R.ok(svc.listLogs(logType, username));
96
+    }
97
+
98
+    @PostMapping("/log")
99
+    @Operation(summary = "记录日志")
100
+    public R<Long> createLog(@RequestBody Map<String,Object> req) {
101
+        return R.ok(svc.createLog(req));
102
+    }
21
 }
103
 }

+ 5
- 2
wm-system/src/main/java/com/water/system/entity/Document.java Прегледај датотеку

52
     @TableLogic
52
     @TableLogic
53
     private Integer deleted;
53
     private Integer deleted;
54
     
54
     
55
-    private LocalDateTime createdAt;
56
-    private LocalDateTime updatedAt;
55
+    @TableField(fill = FieldFill.INSERT)
56
+    private LocalDateTime createdTime;
57
+
58
+    @TableField(fill = FieldFill.INSERT_UPDATE)
59
+    private LocalDateTime updatedTime;
57
 }
60
 }

+ 5
- 2
wm-system/src/main/java/com/water/system/entity/DocumentCategory.java Прегледај датотеку

28
     @TableLogic
28
     @TableLogic
29
     private Integer deleted;
29
     private Integer deleted;
30
     
30
     
31
-    private LocalDateTime createdAt;
32
-    private LocalDateTime updatedAt;
31
+    @TableField(fill = FieldFill.INSERT)
32
+    private LocalDateTime createdTime;
33
+
34
+    @TableField(fill = FieldFill.INSERT_UPDATE)
35
+    private LocalDateTime updatedTime;
33
 }
36
 }

+ 2
- 1
wm-system/src/main/java/com/water/system/entity/DocumentVersion.java Прегледај датотеку

31
     /** 操作人ID */
31
     /** 操作人ID */
32
     private Long operatorId;
32
     private Long operatorId;
33
     
33
     
34
-    private LocalDateTime createdAt;
34
+    @TableField(fill = FieldFill.INSERT)
35
+    private LocalDateTime createdTime;
35
 }
36
 }

+ 54
- 3
wm-system/src/main/java/com/water/system/service/DocService.java Прегледај датотеку

11
     private final DocumentCategoryMapper catMapper;
11
     private final DocumentCategoryMapper catMapper;
12
     private final DocumentVersionMapper verMapper;
12
     private final DocumentVersionMapper verMapper;
13
 
13
 
14
-    public List<Document> listDocs(String categoryId, String keyword) {
14
+    public List<Document> listDocs(Long categoryId, String keyword) {
15
         LambdaQueryWrapper<Document> w = new LambdaQueryWrapper<>();
15
         LambdaQueryWrapper<Document> w = new LambdaQueryWrapper<>();
16
         if (categoryId != null) w.eq(Document::getCategoryId, categoryId);
16
         if (categoryId != null) w.eq(Document::getCategoryId, categoryId);
17
         if (keyword != null) w.like(Document::getName, keyword);
17
         if (keyword != null) w.like(Document::getName, keyword);
18
+        w.orderByDesc(Document::getCreatedTime);
18
         return docMapper.selectList(w);
19
         return docMapper.selectList(w);
19
     }
20
     }
20
     public Document getDoc(Long id) { return docMapper.selectById(id); }
21
     public Document getDoc(Long id) { return docMapper.selectById(id); }
21
     public Long createDoc(Map<String,Object> req) {
22
     public Long createDoc(Map<String,Object> req) {
22
         Document d = new Document();
23
         Document d = new Document();
23
         d.setName((String)req.get("name"));
24
         d.setName((String)req.get("name"));
25
+        d.setOriginalName((String)req.get("originalName"));
24
         d.setCategoryId(req.get("categoryId") != null ? ((Number)req.get("categoryId")).longValue() : null);
26
         d.setCategoryId(req.get("categoryId") != null ? ((Number)req.get("categoryId")).longValue() : null);
25
         d.setFileType((String)req.get("fileType"));
27
         d.setFileType((String)req.get("fileType"));
26
-        d.setFilePath((String)req.get("filePath"));
28
+        d.setStoragePath((String)req.get("storagePath"));
29
+        d.setFileSize(req.get("fileSize") != null ? ((Number)req.get("fileSize")).longValue() : null);
30
+        d.setDescription((String)req.get("description"));
31
+        d.setUploaderId(req.get("uploaderId") != null ? ((Number)req.get("uploaderId")).longValue() : null);
32
+        d.setPermissionLevel(req.get("permissionLevel") != null ? ((Number)req.get("permissionLevel")).intValue() : 0);
33
+        d.setCurrentVersion(1);
34
+        d.setDownloadCount(0);
27
         d.setStatus(1);
35
         d.setStatus(1);
28
         docMapper.insert(d);
36
         docMapper.insert(d);
29
         return d.getId();
37
         return d.getId();
33
         if (d == null) throw new RuntimeException("文档不存在");
41
         if (d == null) throw new RuntimeException("文档不存在");
34
         if (req.containsKey("name")) d.setName((String)req.get("name"));
42
         if (req.containsKey("name")) d.setName((String)req.get("name"));
35
         if (req.containsKey("categoryId")) d.setCategoryId(((Number)req.get("categoryId")).longValue());
43
         if (req.containsKey("categoryId")) d.setCategoryId(((Number)req.get("categoryId")).longValue());
44
+        if (req.containsKey("description")) d.setDescription((String)req.get("description"));
45
+        if (req.containsKey("status")) d.setStatus(((Number)req.get("status")).intValue());
46
+        if (req.containsKey("permissionLevel")) d.setPermissionLevel(((Number)req.get("permissionLevel")).intValue());
36
         docMapper.updateById(d);
47
         docMapper.updateById(d);
37
     }
48
     }
38
     public void deleteDoc(Long id) { docMapper.deleteById(id); }
49
     public void deleteDoc(Long id) { docMapper.deleteById(id); }
50
+    public void incrementDownloadCount(Long id) {
51
+        Document d = docMapper.selectById(id);
52
+        if (d != null) {
53
+            d.setDownloadCount((d.getDownloadCount() != null ? d.getDownloadCount() : 0) + 1);
54
+            docMapper.updateById(d);
55
+        }
56
+    }
57
+
58
+    // Category management
39
     public List<DocumentCategory> listCategories() { return catMapper.selectList(null); }
59
     public List<DocumentCategory> listCategories() { return catMapper.selectList(null); }
60
+    public Long createCategory(Map<String,Object> req) {
61
+        DocumentCategory c = new DocumentCategory();
62
+        c.setName((String)req.get("name"));
63
+        c.setParentId(req.get("parentId") != null ? ((Number)req.get("parentId")).longValue() : null);
64
+        c.setSortOrder(req.get("sortOrder") != null ? ((Number)req.get("sortOrder")).intValue() : 0);
65
+        c.setDescription((String)req.get("description"));
66
+        catMapper.insert(c);
67
+        return c.getId();
68
+    }
69
+
70
+    // Version management
40
     public List<DocumentVersion> getVersions(Long docId) {
71
     public List<DocumentVersion> getVersions(Long docId) {
41
         return verMapper.selectList(new LambdaQueryWrapper<DocumentVersion>()
72
         return verMapper.selectList(new LambdaQueryWrapper<DocumentVersion>()
42
-            .eq(DocumentVersion::getDocId, docId).orderByDesc(DocumentVersion::getVersionNo));
73
+            .eq(DocumentVersion::getDocumentId, docId).orderByDesc(DocumentVersion::getVersion));
74
+    }
75
+    public Long createVersion(Long docId, Map<String,Object> req) {
76
+        DocumentVersion v = new DocumentVersion();
77
+        v.setDocumentId(docId);
78
+        // Auto-increment version number
79
+        List<DocumentVersion> existing = getVersions(docId);
80
+        v.setVersion(existing.isEmpty() ? 1 : existing.get(0).getVersion() + 1);
81
+        v.setDescription((String)req.get("description"));
82
+        v.setStoragePath((String)req.get("storagePath"));
83
+        v.setFileSize(req.get("fileSize") != null ? ((Number)req.get("fileSize")).longValue() : null);
84
+        v.setOperatorId(req.get("operatorId") != null ? ((Number)req.get("operatorId")).longValue() : null);
85
+        verMapper.insert(v);
86
+        // Update document's current version
87
+        Document d = docMapper.selectById(docId);
88
+        if (d != null) {
89
+            d.setCurrentVersion(v.getVersion());
90
+            d.setStoragePath(v.getStoragePath());
91
+            docMapper.updateById(d);
92
+        }
93
+        return v.getId();
43
     }
94
     }
44
 }
95
 }

+ 93
- 8
wm-system/src/main/java/com/water/system/service/SysService.java Прегледај датотеку

3
 import com.water.system.entity.*;
3
 import com.water.system.entity.*;
4
 import com.water.system.mapper.*;
4
 import com.water.system.mapper.*;
5
 import lombok.RequiredArgsConstructor;
5
 import lombok.RequiredArgsConstructor;
6
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
6
 import org.springframework.stereotype.Service;
7
 import org.springframework.stereotype.Service;
7
 import java.util.*;
8
 import java.util.*;
8
 @Service @RequiredArgsConstructor
9
 @Service @RequiredArgsConstructor
13
     private final SysDepartmentMapper deptMapper;
14
     private final SysDepartmentMapper deptMapper;
14
     private final SysLogMapper logMapper;
15
     private final SysLogMapper logMapper;
15
 
16
 
16
-    // Roles
17
+    private static final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
18
+
19
+    // ========== Roles ==========
17
     public List<SysRole> listRoles() { return roleMapper.selectList(null); }
20
     public List<SysRole> listRoles() { return roleMapper.selectList(null); }
18
     public Long createRole(Map<String,Object> req) {
21
     public Long createRole(Map<String,Object> req) {
19
         SysRole r = new SysRole();
22
         SysRole r = new SysRole();
20
         r.setRoleCode((String)req.get("roleCode"));
23
         r.setRoleCode((String)req.get("roleCode"));
21
         r.setRoleName((String)req.get("roleName"));
24
         r.setRoleName((String)req.get("roleName"));
22
         r.setDescription((String)req.get("description"));
25
         r.setDescription((String)req.get("description"));
23
-        r.setStatus(1); r.setSort(0);
26
+        r.setStatus(1);
27
+        r.setSort(req.get("sort") != null ? ((Number)req.get("sort")).intValue() : 0);
24
         roleMapper.insert(r);
28
         roleMapper.insert(r);
25
         return r.getId();
29
         return r.getId();
26
     }
30
     }
27
-    // Users
31
+    public void updateRole(Long id, Map<String,Object> req) {
32
+        SysRole r = roleMapper.selectById(id);
33
+        if (r == null) throw new RuntimeException("角色不存在");
34
+        if (req.containsKey("roleName")) r.setRoleName((String)req.get("roleName"));
35
+        if (req.containsKey("description")) r.setDescription((String)req.get("description"));
36
+        if (req.containsKey("status")) r.setStatus(((Number)req.get("status")).intValue());
37
+        if (req.containsKey("sort")) r.setSort(((Number)req.get("sort")).intValue());
38
+        roleMapper.updateById(r);
39
+    }
40
+    public void deleteRole(Long id) { roleMapper.deleteById(id); }
41
+
42
+    // ========== Users ==========
28
     public List<SysUser> listUsers(Integer status) {
43
     public List<SysUser> listUsers(Integer status) {
29
         return userMapper.selectList(new LambdaQueryWrapper<SysUser>()
44
         return userMapper.selectList(new LambdaQueryWrapper<SysUser>()
30
             .eq(status != null, SysUser::getStatus, status));
45
             .eq(status != null, SysUser::getStatus, status));
32
     public Long createUser(Map<String,Object> req) {
47
     public Long createUser(Map<String,Object> req) {
33
         SysUser u = new SysUser();
48
         SysUser u = new SysUser();
34
         u.setUsername((String)req.get("username"));
49
         u.setUsername((String)req.get("username"));
35
-        u.setPassword((String)req.get("password"));
50
+        // BCrypt encode password
51
+        String rawPassword = (String)req.get("password");
52
+        u.setPassword(passwordEncoder.encode(rawPassword));
36
         u.setRealName((String)req.get("realName"));
53
         u.setRealName((String)req.get("realName"));
37
         u.setPhone((String)req.get("phone"));
54
         u.setPhone((String)req.get("phone"));
55
+        u.setEmail((String)req.get("email"));
56
+        u.setDeptId(req.get("deptId") != null ? ((Number)req.get("deptId")).longValue() : null);
57
+        u.setAvatar((String)req.get("avatar"));
38
         u.setStatus(1);
58
         u.setStatus(1);
39
         userMapper.insert(u);
59
         userMapper.insert(u);
40
         return u.getId();
60
         return u.getId();
45
         u.setStatus(status);
65
         u.setStatus(status);
46
         userMapper.updateById(u);
66
         userMapper.updateById(u);
47
     }
67
     }
48
-    // Menus
68
+    public void updateUser(Long id, Map<String,Object> req) {
69
+        SysUser u = userMapper.selectById(id);
70
+        if (u == null) throw new RuntimeException("用户不存在");
71
+        if (req.containsKey("realName")) u.setRealName((String)req.get("realName"));
72
+        if (req.containsKey("phone")) u.setPhone((String)req.get("phone"));
73
+        if (req.containsKey("email")) u.setEmail((String)req.get("email"));
74
+        if (req.containsKey("deptId")) u.setDeptId(((Number)req.get("deptId")).longValue());
75
+        if (req.containsKey("avatar")) u.setAvatar((String)req.get("avatar"));
76
+        if (req.containsKey("password")) {
77
+            u.setPassword(passwordEncoder.encode((String)req.get("password")));
78
+        }
79
+        userMapper.updateById(u);
80
+    }
81
+    public boolean verifyPassword(Long userId, String rawPassword) {
82
+        SysUser u = userMapper.selectById(userId);
83
+        if (u == null) return false;
84
+        return passwordEncoder.matches(rawPassword, u.getPassword());
85
+    }
86
+
87
+    // ========== Menus ==========
49
     public List<SysMenu> listMenus() { return menuMapper.selectList(null); }
88
     public List<SysMenu> listMenus() { return menuMapper.selectList(null); }
50
-    // Departments
89
+    public Long createMenu(Map<String,Object> req) {
90
+        SysMenu m = new SysMenu();
91
+        m.setParentId(req.get("parentId") != null ? ((Number)req.get("parentId")).longValue() : null);
92
+        m.setName((String)req.get("name"));
93
+        m.setPath((String)req.get("path"));
94
+        m.setIcon((String)req.get("icon"));
95
+        m.setComponent((String)req.get("component"));
96
+        m.setPermission((String)req.get("permission"));
97
+        m.setType(req.get("type") != null ? ((Number)req.get("type")).intValue() : 0);
98
+        m.setSort(req.get("sort") != null ? ((Number)req.get("sort")).intValue() : 0);
99
+        m.setVisible(1);
100
+        m.setStatus(1);
101
+        menuMapper.insert(m);
102
+        return m.getId();
103
+    }
104
+
105
+    // ========== Departments ==========
51
     public List<SysDepartment> listDepts() { return deptMapper.selectList(null); }
106
     public List<SysDepartment> listDepts() { return deptMapper.selectList(null); }
52
     public Long createDept(Map<String,Object> req) {
107
     public Long createDept(Map<String,Object> req) {
53
         SysDepartment d = new SysDepartment();
108
         SysDepartment d = new SysDepartment();
54
         d.setDeptName((String)req.get("deptName"));
109
         d.setDeptName((String)req.get("deptName"));
110
+        d.setParentId(req.get("parentId") != null ? ((Number)req.get("parentId")).longValue() : null);
55
         d.setLeader((String)req.get("leader"));
111
         d.setLeader((String)req.get("leader"));
56
-        d.setSort(0); d.setStatus(1);
112
+        d.setPhone((String)req.get("phone"));
113
+        d.setSort(req.get("sort") != null ? ((Number)req.get("sort")).intValue() : 0);
114
+        d.setStatus(1);
57
         deptMapper.insert(d);
115
         deptMapper.insert(d);
58
         return d.getId();
116
         return d.getId();
59
     }
117
     }
60
-    // Logs
118
+    public void updateDept(Long id, Map<String,Object> req) {
119
+        SysDepartment d = deptMapper.selectById(id);
120
+        if (d == null) throw new RuntimeException("部门不存在");
121
+        if (req.containsKey("deptName")) d.setDeptName((String)req.get("deptName"));
122
+        if (req.containsKey("leader")) d.setLeader((String)req.get("leader"));
123
+        if (req.containsKey("phone")) d.setPhone((String)req.get("phone"));
124
+        if (req.containsKey("sort")) d.setSort(((Number)req.get("sort")).intValue());
125
+        if (req.containsKey("status")) d.setStatus(((Number)req.get("status")).intValue());
126
+        deptMapper.updateById(d);
127
+    }
128
+
129
+    // ========== Logs ==========
61
     public List<SysLog> listLogs(String logType, String username) {
130
     public List<SysLog> listLogs(String logType, String username) {
62
         LambdaQueryWrapper<SysLog> w = new LambdaQueryWrapper<>();
131
         LambdaQueryWrapper<SysLog> w = new LambdaQueryWrapper<>();
63
         if (logType != null) w.eq(SysLog::getLogType, logType);
132
         if (logType != null) w.eq(SysLog::getLogType, logType);
64
         if (username != null) w.like(SysLog::getUsername, username);
133
         if (username != null) w.like(SysLog::getUsername, username);
65
         return logMapper.selectList(w.orderByDesc(SysLog::getCreatedTime).last("LIMIT 100"));
134
         return logMapper.selectList(w.orderByDesc(SysLog::getCreatedTime).last("LIMIT 100"));
66
     }
135
     }
136
+    public Long createLog(Map<String,Object> req) {
137
+        SysLog log = new SysLog();
138
+        log.setUserId(req.get("userId") != null ? ((Number)req.get("userId")).longValue() : null);
139
+        log.setUsername((String)req.get("username"));
140
+        log.setLogType((String)req.get("logType"));
141
+        log.setModule((String)req.get("module"));
142
+        log.setAction((String)req.get("action"));
143
+        log.setRequestUrl((String)req.get("requestUrl"));
144
+        log.setRequestMethod((String)req.get("requestMethod"));
145
+        log.setRequestParams((String)req.get("requestParams"));
146
+        log.setResponseResult((String)req.get("responseResult"));
147
+        log.setIp((String)req.get("ip"));
148
+        log.setDuration(req.get("duration") != null ? ((Number)req.get("duration")).longValue() : null);
149
+        logMapper.insert(log);
150
+        return log.getId();
151
+    }
67
 }
152
 }

+ 115
- 30
wm-system/src/main/resources/db/V1__system_manage.sql Прегледај датотеку

1
-CREATE TABLE IF NOT EXISTS sys_document (
2
-    id BIGSERIAL PRIMARY KEY, name VARCHAR(200), category_id BIGINT,
3
-    file_type VARCHAR(20), file_size BIGINT, file_path VARCHAR(500),
4
-    status INT DEFAULT 1, creator_id BIGINT,
5
-    created_time TIMESTAMP DEFAULT NOW(), updated_time TIMESTAMP DEFAULT NOW()
6
-);
7
-CREATE TABLE IF NOT EXISTS sys_document_category (
8
-    id BIGSERIAL PRIMARY KEY, name VARCHAR(100), parent_id BIGINT,
9
-    sort INT DEFAULT 0, status INT DEFAULT 1,
10
-    created_time TIMESTAMP DEFAULT NOW()
1
+-- 文档管理表
2
+CREATE TABLE IF NOT EXISTS doc_document (
3
+    id BIGSERIAL PRIMARY KEY,
4
+    name VARCHAR(200),
5
+    original_name VARCHAR(200),
6
+    file_size BIGINT,
7
+    file_type VARCHAR(20),
8
+    storage_path VARCHAR(500),
9
+    category_id BIGINT,
10
+    description TEXT,
11
+    current_version INT DEFAULT 1,
12
+    uploader_id BIGINT,
13
+    download_count INT DEFAULT 0,
14
+    status INT DEFAULT 1,
15
+    permission_level INT DEFAULT 0,
16
+    deleted INT DEFAULT 0,
17
+    created_time TIMESTAMP DEFAULT NOW(),
18
+    updated_time TIMESTAMP DEFAULT NOW()
19
+);
20
+
21
+CREATE TABLE IF NOT EXISTS doc_category (
22
+    id BIGSERIAL PRIMARY KEY,
23
+    name VARCHAR(100),
24
+    parent_id BIGINT,
25
+    sort_order INT DEFAULT 0,
26
+    description VARCHAR(500),
27
+    deleted INT DEFAULT 0,
28
+    created_time TIMESTAMP DEFAULT NOW(),
29
+    updated_time TIMESTAMP DEFAULT NOW()
11
 );
30
 );
12
-CREATE TABLE IF NOT EXISTS sys_document_version (
13
-    id BIGSERIAL PRIMARY KEY, doc_id BIGINT, version_no INT,
14
-    file_path VARCHAR(500), remark TEXT,
31
+
32
+CREATE TABLE IF NOT EXISTS doc_version (
33
+    id BIGSERIAL PRIMARY KEY,
34
+    document_id BIGINT,
35
+    version INT,
36
+    description VARCHAR(500),
37
+    storage_path VARCHAR(500),
38
+    file_size BIGINT,
39
+    operator_id BIGINT,
15
     created_time TIMESTAMP DEFAULT NOW()
40
     created_time TIMESTAMP DEFAULT NOW()
16
 );
41
 );
42
+
43
+-- 系统管理表
17
 CREATE TABLE IF NOT EXISTS sys_role (
44
 CREATE TABLE IF NOT EXISTS sys_role (
18
-    id BIGSERIAL PRIMARY KEY, role_code VARCHAR(50) UNIQUE, role_name VARCHAR(50),
19
-    description VARCHAR(200), status INT DEFAULT 1, sort INT DEFAULT 0,
45
+    id BIGSERIAL PRIMARY KEY,
46
+    role_code VARCHAR(50) UNIQUE,
47
+    role_name VARCHAR(50),
48
+    description VARCHAR(200),
49
+    status INT DEFAULT 1,
50
+    sort INT DEFAULT 0,
20
     created_time TIMESTAMP DEFAULT NOW()
51
     created_time TIMESTAMP DEFAULT NOW()
21
 );
52
 );
53
+
22
 CREATE TABLE IF NOT EXISTS sys_user (
54
 CREATE TABLE IF NOT EXISTS sys_user (
23
-    id BIGSERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE, password VARCHAR(200),
24
-    real_name VARCHAR(50), phone VARCHAR(20), email VARCHAR(100),
25
-    dept_id BIGINT, avatar VARCHAR(500), status INT DEFAULT 1,
26
-    created_time TIMESTAMP DEFAULT NOW(), updated_time TIMESTAMP DEFAULT NOW()
55
+    id BIGSERIAL PRIMARY KEY,
56
+    username VARCHAR(50) UNIQUE,
57
+    password VARCHAR(200),
58
+    real_name VARCHAR(50),
59
+    phone VARCHAR(20),
60
+    email VARCHAR(100),
61
+    dept_id BIGINT,
62
+    avatar VARCHAR(500),
63
+    status INT DEFAULT 1,
64
+    created_time TIMESTAMP DEFAULT NOW(),
65
+    updated_time TIMESTAMP DEFAULT NOW()
27
 );
66
 );
67
+
28
 CREATE TABLE IF NOT EXISTS sys_menu (
68
 CREATE TABLE IF NOT EXISTS sys_menu (
29
-    id BIGSERIAL PRIMARY KEY, parent_id BIGINT, name VARCHAR(50),
30
-    path VARCHAR(200), icon VARCHAR(50), component VARCHAR(200),
31
-    permission VARCHAR(100), type INT, sort INT DEFAULT 0,
32
-    visible INT DEFAULT 1, status INT DEFAULT 1
69
+    id BIGSERIAL PRIMARY KEY,
70
+    parent_id BIGINT,
71
+    name VARCHAR(50),
72
+    path VARCHAR(200),
73
+    icon VARCHAR(50),
74
+    component VARCHAR(200),
75
+    permission VARCHAR(100),
76
+    type INT,
77
+    sort INT DEFAULT 0,
78
+    visible INT DEFAULT 1,
79
+    status INT DEFAULT 1
33
 );
80
 );
81
+
34
 CREATE TABLE IF NOT EXISTS sys_department (
82
 CREATE TABLE IF NOT EXISTS sys_department (
35
-    id BIGSERIAL PRIMARY KEY, parent_id BIGINT, dept_name VARCHAR(100),
36
-    leader VARCHAR(50), phone VARCHAR(20), sort INT DEFAULT 0, status INT DEFAULT 1
83
+    id BIGSERIAL PRIMARY KEY,
84
+    parent_id BIGINT,
85
+    dept_name VARCHAR(100),
86
+    leader VARCHAR(50),
87
+    phone VARCHAR(20),
88
+    sort INT DEFAULT 0,
89
+    status INT DEFAULT 1
37
 );
90
 );
91
+
38
 CREATE TABLE IF NOT EXISTS sys_log (
92
 CREATE TABLE IF NOT EXISTS sys_log (
39
-    id BIGSERIAL PRIMARY KEY, user_id BIGINT, username VARCHAR(50),
40
-    log_type VARCHAR(20), module VARCHAR(50), action VARCHAR(50),
41
-    request_url VARCHAR(500), request_method VARCHAR(10),
42
-    request_params TEXT, response_result TEXT,
43
-    ip VARCHAR(50), duration BIGINT,
93
+    id BIGSERIAL PRIMARY KEY,
94
+    user_id BIGINT,
95
+    username VARCHAR(50),
96
+    log_type VARCHAR(20),
97
+    module VARCHAR(50),
98
+    action VARCHAR(50),
99
+    request_url VARCHAR(500),
100
+    request_method VARCHAR(10),
101
+    request_params TEXT,
102
+    response_result TEXT,
103
+    ip VARCHAR(50),
104
+    duration BIGINT,
44
     created_time TIMESTAMP DEFAULT NOW()
105
     created_time TIMESTAMP DEFAULT NOW()
45
 );
106
 );
107
+
108
+-- 种子数据:5级角色预设
109
+INSERT INTO sys_role (role_code, role_name, description, status, sort) VALUES
110
+    ('ADMIN', '系统管理员', '拥有所有权限', 1, 1),
111
+    ('LEADER', '分管领导', '查看所有数据,审批权限', 1, 2),
112
+    ('MANAGER', '业务管理', '业务数据管理权限', 1, 3),
113
+    ('OPERATOR', '运维人员', '运维操作权限', 1, 4),
114
+    ('TECH', '技术人员', '技术支持权限', 1, 5)
115
+ON CONFLICT (role_code) DO NOTHING;
116
+
117
+-- 种子数据:默认部门
118
+INSERT INTO sys_department (parent_id, dept_name, leader, phone, sort, status) VALUES
119
+    (NULL, '水利局', NULL, NULL, 1, 1),
120
+    (1, '水务公司', NULL, NULL, 2, 1),
121
+    (1, '运维单位', NULL, NULL, 3, 1)
122
+ON CONFLICT DO NOTHING;
123
+
124
+-- 种子数据:文档分类
125
+INSERT INTO doc_category (name, parent_id, sort_order, description) VALUES
126
+    ('操作手册', NULL, 1, '设备操作手册、使用说明书'),
127
+    ('维护记录', NULL, 2, '设备维护保养记录'),
128
+    ('检测报告', NULL, 3, '水质检测、设备检测报告'),
129
+    ('管理制度', NULL, 4, '管理制度、操作规程')
130
+ON CONFLICT DO NOTHING;

+ 379
- 0
wm-system/src/test/java/com/water/system/DocServiceTest.java Прегледај датотеку

1
+package com.water.system;
2
+
3
+import com.water.system.entity.*;
4
+import com.water.system.mapper.*;
5
+import com.water.system.service.DocService;
6
+import org.junit.jupiter.api.BeforeEach;
7
+import org.junit.jupiter.api.DisplayName;
8
+import org.junit.jupiter.api.Nested;
9
+import org.junit.jupiter.api.Test;
10
+import org.junit.jupiter.api.extension.ExtendWith;
11
+import org.mockito.InjectMocks;
12
+import org.mockito.Mock;
13
+import org.mockito.junit.jupiter.MockitoExtension;
14
+
15
+import java.time.LocalDateTime;
16
+import java.util.*;
17
+
18
+import static org.junit.jupiter.api.Assertions.*;
19
+import static org.mockito.ArgumentMatchers.*;
20
+import static org.mockito.Mockito.*;
21
+
22
+/**
23
+ * 文档管理服务测试
24
+ */
25
+@ExtendWith(MockitoExtension.class)
26
+@DisplayName("文档管理服务测试")
27
+class DocServiceTest {
28
+
29
+    @Mock private DocumentMapper docMapper;
30
+    @Mock private DocumentCategoryMapper catMapper;
31
+    @Mock private DocumentVersionMapper verMapper;
32
+
33
+    @InjectMocks private DocService docService;
34
+
35
+    // ========== Document CRUD ==========
36
+    @Nested
37
+    @DisplayName("文档CRUD")
38
+    class DocumentCrudTest {
39
+
40
+        @Test
41
+        @DisplayName("创建文档 - 正常流程")
42
+        void createDoc_success() {
43
+            Map<String, Object> req = new HashMap<>();
44
+            req.put("name", "操作手册.pdf");
45
+            req.put("originalName", "操作手册.pdf");
46
+            req.put("categoryId", 1);
47
+            req.put("fileType", "pdf");
48
+            req.put("storagePath", "/docs/manual.pdf");
49
+            req.put("fileSize", 102400);
50
+            req.put("description", "设备操作手册");
51
+            req.put("uploaderId", 1);
52
+            req.put("permissionLevel", 1);
53
+
54
+            when(docMapper.insert(any(Document.class))).thenAnswer(invocation -> {
55
+                Document d = invocation.getArgument(0);
56
+                d.setId(1L);
57
+                return 1;
58
+            });
59
+
60
+            Long id = docService.createDoc(req);
61
+
62
+            assertEquals(1L, id);
63
+            verify(docMapper).insert(argThat(d ->
64
+                "操作手册.pdf".equals(d.getName()) &&
65
+                "pdf".equals(d.getFileType()) &&
66
+                "/docs/manual.pdf".equals(d.getStoragePath()) &&
67
+                d.getStatus() == 1 &&
68
+                d.getCurrentVersion() == 1 &&
69
+                d.getDownloadCount() == 0
70
+            ));
71
+        }
72
+
73
+        @Test
74
+        @DisplayName("创建文档 - 缺少可选字段时使用默认值")
75
+        void createDoc_withDefaults() {
76
+            Map<String, Object> req = new HashMap<>();
77
+            req.put("name", "测试文档");
78
+
79
+            when(docMapper.insert(any(Document.class))).thenAnswer(invocation -> {
80
+                Document d = invocation.getArgument(0);
81
+                d.setId(2L);
82
+                return 1;
83
+            });
84
+
85
+            Long id = docService.createDoc(req);
86
+
87
+            assertEquals(2L, id);
88
+            verify(docMapper).insert(argThat(d ->
89
+                d.getStatus() == 1 &&
90
+                d.getCurrentVersion() == 1 &&
91
+                d.getDownloadCount() == 0 &&
92
+                d.getPermissionLevel() == 0
93
+            ));
94
+        }
95
+
96
+        @Test
97
+        @DisplayName("获取文档详情")
98
+        void getDoc_success() {
99
+            Document doc = new Document();
100
+            doc.setId(1L);
101
+            doc.setName("测试文档");
102
+            when(docMapper.selectById(1L)).thenReturn(doc);
103
+
104
+            Document result = docService.getDoc(1L);
105
+
106
+            assertNotNull(result);
107
+            assertEquals(1L, result.getId());
108
+            assertEquals("测试文档", result.getName());
109
+        }
110
+
111
+        @Test
112
+        @DisplayName("获取文档 - 不存在返回null")
113
+        void getDoc_notFound() {
114
+            when(docMapper.selectById(999L)).thenReturn(null);
115
+            assertNull(docService.getDoc(999L));
116
+        }
117
+
118
+        @Test
119
+        @DisplayName("更新文档 - 正常流程")
120
+        void updateDoc_success() {
121
+            Document existing = new Document();
122
+            existing.setId(1L);
123
+            existing.setName("旧名称");
124
+            when(docMapper.selectById(1L)).thenReturn(existing);
125
+
126
+            Map<String, Object> req = new HashMap<>();
127
+            req.put("name", "新名称");
128
+            req.put("status", 2);
129
+            req.put("description", "更新描述");
130
+
131
+            docService.updateDoc(1L, req);
132
+
133
+            verify(docMapper).updateById(argThat(d ->
134
+                "新名称".equals(d.getName()) &&
135
+                d.getStatus() == 2 &&
136
+                "更新描述".equals(d.getDescription())
137
+            ));
138
+        }
139
+
140
+        @Test
141
+        @DisplayName("更新文档 - 不存在抛异常")
142
+        void updateDoc_notFound_throws() {
143
+            when(docMapper.selectById(999L)).thenReturn(null);
144
+            assertThrows(RuntimeException.class, () -> docService.updateDoc(999L, new HashMap<>()));
145
+        }
146
+
147
+        @Test
148
+        @DisplayName("删除文档")
149
+        void deleteDoc_success() {
150
+            docService.deleteDoc(1L);
151
+            verify(docMapper).deleteById(1L);
152
+        }
153
+
154
+        @Test
155
+        @DisplayName("增加下载计数")
156
+        void incrementDownloadCount_success() {
157
+            Document doc = new Document();
158
+            doc.setId(1L);
159
+            doc.setDownloadCount(5);
160
+            when(docMapper.selectById(1L)).thenReturn(doc);
161
+
162
+            docService.incrementDownloadCount(1L);
163
+
164
+            verify(docMapper).updateById(argThat(d -> d.getDownloadCount() == 6));
165
+        }
166
+
167
+        @Test
168
+        @DisplayName("增加下载计数 - 文档不存在不报错")
169
+        void incrementDownloadCount_notFound_noError() {
170
+            when(docMapper.selectById(999L)).thenReturn(null);
171
+            assertDoesNotThrow(() -> docService.incrementDownloadCount(999L));
172
+            verify(docMapper, never()).updateById(any());
173
+        }
174
+
175
+        @Test
176
+        @DisplayName("增加下载计数 - 初始count为null")
177
+        void incrementDownloadCount_nullCount() {
178
+            Document doc = new Document();
179
+            doc.setId(1L);
180
+            doc.setDownloadCount(null);
181
+            when(docMapper.selectById(1L)).thenReturn(doc);
182
+
183
+            docService.incrementDownloadCount(1L);
184
+
185
+            verify(docMapper).updateById(argThat(d -> d.getDownloadCount() == 1));
186
+        }
187
+    }
188
+
189
+    // ========== Document List & Search ==========
190
+    @Nested
191
+    @DisplayName("文档列表与搜索")
192
+    class DocumentListTest {
193
+
194
+        @Test
195
+        @DisplayName("列出所有文档 - 无筛选条件")
196
+        void listDocs_noFilter() {
197
+            List<Document> docs = Arrays.asList(new Document(), new Document());
198
+            when(docMapper.selectList(any())).thenReturn(docs);
199
+
200
+            List<Document> result = docService.listDocs(null, null);
201
+
202
+            assertEquals(2, result.size());
203
+        }
204
+
205
+        @Test
206
+        @DisplayName("按分类筛选文档")
207
+        void listDocs_byCategory() {
208
+            List<Document> docs = Collections.singletonList(new Document());
209
+            when(docMapper.selectList(any())).thenReturn(docs);
210
+
211
+            List<Document> result = docService.listDocs(1L, null);
212
+
213
+            assertEquals(1, result.size());
214
+        }
215
+
216
+        @Test
217
+        @DisplayName("按关键词搜索文档")
218
+        void listDocs_byKeyword() {
219
+            List<Document> docs = Collections.singletonList(new Document());
220
+            when(docMapper.selectList(any())).thenReturn(docs);
221
+
222
+            List<Document> result = docService.listDocs(null, "操作");
223
+
224
+            assertEquals(1, result.size());
225
+        }
226
+
227
+        @Test
228
+        @DisplayName("按分类和关键词组合筛选")
229
+        void listDocs_byCategoryAndKeyword() {
230
+            when(docMapper.selectList(any())).thenReturn(Collections.emptyList());
231
+            List<Document> result = docService.listDocs(1L, "手册");
232
+            assertTrue(result.isEmpty());
233
+        }
234
+    }
235
+
236
+    // ========== Category Management ==========
237
+    @Nested
238
+    @DisplayName("分类管理")
239
+    class CategoryTest {
240
+
241
+        @Test
242
+        @DisplayName("列出所有分类")
243
+        void listCategories_success() {
244
+            List<DocumentCategory> cats = Arrays.asList(new DocumentCategory(), new DocumentCategory());
245
+            when(catMapper.selectList(any())).thenReturn(cats);
246
+
247
+            List<DocumentCategory> result = docService.listCategories();
248
+
249
+            assertEquals(2, result.size());
250
+        }
251
+
252
+        @Test
253
+        @DisplayName("创建分类 - 正常流程")
254
+        void createCategory_success() {
255
+            Map<String, Object> req = new HashMap<>();
256
+            req.put("name", "操作手册");
257
+            req.put("parentId", 1);
258
+            req.put("sortOrder", 5);
259
+            req.put("description", "设备操作手册分类");
260
+
261
+            when(catMapper.insert(any(DocumentCategory.class))).thenAnswer(invocation -> {
262
+                DocumentCategory c = invocation.getArgument(0);
263
+                c.setId(10L);
264
+                return 1;
265
+            });
266
+
267
+            Long id = docService.createCategory(req);
268
+
269
+            assertEquals(10L, id);
270
+            verify(catMapper).insert(argThat(c ->
271
+                "操作手册".equals(c.getName()) &&
272
+                c.getParentId() == 1L &&
273
+                c.getSortOrder() == 5
274
+            ));
275
+        }
276
+
277
+        @Test
278
+        @DisplayName("创建分类 - 使用默认排序")
279
+        void createCategory_defaultSort() {
280
+            Map<String, Object> req = new HashMap<>();
281
+            req.put("name", "新分类");
282
+
283
+            when(catMapper.insert(any(DocumentCategory.class))).thenAnswer(invocation -> {
284
+                DocumentCategory c = invocation.getArgument(0);
285
+                c.setId(11L);
286
+                return 1;
287
+            });
288
+
289
+            docService.createCategory(req);
290
+
291
+            verify(catMapper).insert(argThat(c -> c.getSortOrder() == 0));
292
+        }
293
+    }
294
+
295
+    // ========== Version Management ==========
296
+    @Nested
297
+    @DisplayName("版本管理")
298
+    class VersionTest {
299
+
300
+        @Test
301
+        @DisplayName("获取文档版本列表")
302
+        void getVersions_success() {
303
+            DocumentVersion v1 = new DocumentVersion();
304
+            v1.setVersion(2);
305
+            DocumentVersion v2 = new DocumentVersion();
306
+            v2.setVersion(1);
307
+            when(verMapper.selectList(any())).thenReturn(Arrays.asList(v1, v2));
308
+
309
+            List<DocumentVersion> result = docService.getVersions(1L);
310
+
311
+            assertEquals(2, result.size());
312
+        }
313
+
314
+        @Test
315
+        @DisplayName("创建第一个版本 - 无历史版本")
316
+        void createVersion_firstVersion() {
317
+            when(verMapper.selectList(any())).thenReturn(Collections.emptyList());
318
+            when(verMapper.insert(any(DocumentVersion.class))).thenAnswer(invocation -> {
319
+                DocumentVersion v = invocation.getArgument(0);
320
+                v.setId(1L);
321
+                return 1;
322
+            });
323
+
324
+            Document doc = new Document();
325
+            doc.setId(1L);
326
+            doc.setCurrentVersion(0);
327
+            when(docMapper.selectById(1L)).thenReturn(doc);
328
+            when(docMapper.updateById(any())).thenReturn(1);
329
+
330
+            Map<String, Object> req = new HashMap<>();
331
+            req.put("description", "初始版本");
332
+            req.put("storagePath", "/docs/v1.pdf");
333
+            req.put("fileSize", 1024);
334
+            req.put("operatorId", 1);
335
+
336
+            Long id = docService.createVersion(1L, req);
337
+
338
+            assertEquals(1L, id);
339
+            verify(verMapper).insert(argThat(v ->
340
+                v.getDocumentId() == 1L &&
341
+                v.getVersion() == 1 &&
342
+                "/docs/v1.pdf".equals(v.getStoragePath())
343
+            ));
344
+            verify(docMapper).updateById(argThat(d -> d.getCurrentVersion() == 1));
345
+        }
346
+
347
+        @Test
348
+        @DisplayName("创建新版本 - 自增版本号")
349
+        void createVersion_incrementVersion() {
350
+            DocumentVersion existing = new DocumentVersion();
351
+            existing.setVersion(3);
352
+            when(verMapper.selectList(any())).thenReturn(Collections.singletonList(existing));
353
+            when(verMapper.insert(any(DocumentVersion.class))).thenAnswer(invocation -> {
354
+                DocumentVersion v = invocation.getArgument(0);
355
+                v.setId(4L);
356
+                return 1;
357
+            });
358
+
359
+            Document doc = new Document();
360
+            doc.setId(1L);
361
+            doc.setCurrentVersion(3);
362
+            when(docMapper.selectById(1L)).thenReturn(doc);
363
+            when(docMapper.updateById(any())).thenReturn(1);
364
+
365
+            Map<String, Object> req = new HashMap<>();
366
+            req.put("description", "第四版");
367
+            req.put("storagePath", "/docs/v4.pdf");
368
+
369
+            Long id = docService.createVersion(1L, req);
370
+
371
+            assertEquals(4L, id);
372
+            verify(verMapper).insert(argThat(v -> v.getVersion() == 4));
373
+            verify(docMapper).updateById(argThat(d ->
374
+                d.getCurrentVersion() == 4 &&
375
+                "/docs/v4.pdf".equals(d.getStoragePath())
376
+            ));
377
+        }
378
+    }
379
+}

+ 515
- 0
wm-system/src/test/java/com/water/system/SysServiceTest.java Прегледај датотеку

1
+package com.water.system;
2
+
3
+import com.water.system.entity.*;
4
+import com.water.system.mapper.*;
5
+import com.water.system.service.SysService;
6
+import org.junit.jupiter.api.BeforeEach;
7
+import org.junit.jupiter.api.DisplayName;
8
+import org.junit.jupiter.api.Nested;
9
+import org.junit.jupiter.api.Test;
10
+import org.junit.jupiter.api.extension.ExtendWith;
11
+import org.mockito.InjectMocks;
12
+import org.mockito.Mock;
13
+import org.mockito.junit.jupiter.MockitoExtension;
14
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
15
+
16
+import java.util.*;
17
+
18
+import static org.junit.jupiter.api.Assertions.*;
19
+import static org.mockito.ArgumentMatchers.*;
20
+import static org.mockito.Mockito.*;
21
+
22
+/**
23
+ * 系统管理服务测试
24
+ */
25
+@ExtendWith(MockitoExtension.class)
26
+@DisplayName("系统管理服务测试")
27
+class SysServiceTest {
28
+
29
+    @Mock private SysRoleMapper roleMapper;
30
+    @Mock private SysUserMapper userMapper;
31
+    @Mock private SysMenuMapper menuMapper;
32
+    @Mock private SysDepartmentMapper deptMapper;
33
+    @Mock private SysLogMapper logMapper;
34
+
35
+    @InjectMocks private SysService sysService;
36
+
37
+    private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
38
+
39
+    // ========== Role Management ==========
40
+    @Nested
41
+    @DisplayName("角色管理")
42
+    class RoleTest {
43
+
44
+        @Test
45
+        @DisplayName("列出所有角色")
46
+        void listRoles_success() {
47
+            SysRole r1 = new SysRole();
48
+            r1.setRoleCode("ADMIN");
49
+            SysRole r2 = new SysRole();
50
+            r2.setRoleCode("OPERATOR");
51
+            when(roleMapper.selectList(any())).thenReturn(Arrays.asList(r1, r2));
52
+
53
+            List<SysRole> result = sysService.listRoles();
54
+
55
+            assertEquals(2, result.size());
56
+        }
57
+
58
+        @Test
59
+        @DisplayName("创建角色 - 正常流程")
60
+        void createRole_success() {
61
+            Map<String, Object> req = new HashMap<>();
62
+            req.put("roleCode", "AUDITOR");
63
+            req.put("roleName", "审计员");
64
+            req.put("description", "审计权限");
65
+            req.put("sort", 6);
66
+
67
+            when(roleMapper.insert(any(SysRole.class))).thenAnswer(invocation -> {
68
+                SysRole r = invocation.getArgument(0);
69
+                r.setId(10L);
70
+                return 1;
71
+            });
72
+
73
+            Long id = sysService.createRole(req);
74
+
75
+            assertEquals(10L, id);
76
+            verify(roleMapper).insert(argThat(r ->
77
+                "AUDITOR".equals(r.getRoleCode()) &&
78
+                "审计员".equals(r.getRoleName()) &&
79
+                r.getStatus() == 1 &&
80
+                r.getSort() == 6
81
+            ));
82
+        }
83
+
84
+        @Test
85
+        @DisplayName("创建角色 - 使用默认排序")
86
+        void createRole_defaultSort() {
87
+            Map<String, Object> req = new HashMap<>();
88
+            req.put("roleCode", "GUEST");
89
+            req.put("roleName", "访客");
90
+
91
+            when(roleMapper.insert(any(SysRole.class))).thenAnswer(invocation -> {
92
+                SysRole r = invocation.getArgument(0);
93
+                r.setId(11L);
94
+                return 1;
95
+            });
96
+
97
+            sysService.createRole(req);
98
+
99
+            verify(roleMapper).insert(argThat(r -> r.getSort() == 0));
100
+        }
101
+
102
+        @Test
103
+        @DisplayName("更新角色 - 正常流程")
104
+        void updateRole_success() {
105
+            SysRole existing = new SysRole();
106
+            existing.setId(1L);
107
+            existing.setRoleName("旧名称");
108
+            when(roleMapper.selectById(1L)).thenReturn(existing);
109
+
110
+            Map<String, Object> req = new HashMap<>();
111
+            req.put("roleName", "新名称");
112
+            req.put("status", 0);
113
+            req.put("sort", 10);
114
+
115
+            sysService.updateRole(1L, req);
116
+
117
+            verify(roleMapper).updateById(argThat(r ->
118
+                "新名称".equals(r.getRoleName()) &&
119
+                r.getStatus() == 0 &&
120
+                r.getSort() == 10
121
+            ));
122
+        }
123
+
124
+        @Test
125
+        @DisplayName("更新角色 - 不存在抛异常")
126
+        void updateRole_notFound_throws() {
127
+            when(roleMapper.selectById(999L)).thenReturn(null);
128
+            assertThrows(RuntimeException.class, () -> sysService.updateRole(999L, new HashMap<>()));
129
+        }
130
+
131
+        @Test
132
+        @DisplayName("删除角色")
133
+        void deleteRole_success() {
134
+            sysService.deleteRole(1L);
135
+            verify(roleMapper).deleteById(1L);
136
+        }
137
+    }
138
+
139
+    // ========== User Management ==========
140
+    @Nested
141
+    @DisplayName("用户管理")
142
+    class UserTest {
143
+
144
+        @Test
145
+        @DisplayName("列出所有用户")
146
+        void listUsers_all() {
147
+            SysUser u1 = new SysUser();
148
+            u1.setUsername("admin");
149
+            SysUser u2 = new SysUser();
150
+            u2.setUsername("operator");
151
+            when(userMapper.selectList(any())).thenReturn(Arrays.asList(u1, u2));
152
+
153
+            List<SysUser> result = sysService.listUsers(null);
154
+
155
+            assertEquals(2, result.size());
156
+        }
157
+
158
+        @Test
159
+        @DisplayName("按状态筛选用户")
160
+        void listUsers_byStatus() {
161
+            SysUser u = new SysUser();
162
+            u.setUsername("active_user");
163
+            u.setStatus(1);
164
+            when(userMapper.selectList(any())).thenReturn(Collections.singletonList(u));
165
+
166
+            List<SysUser> result = sysService.listUsers(1);
167
+
168
+            assertEquals(1, result.size());
169
+            assertEquals(1, result.get(0).getStatus());
170
+        }
171
+
172
+        @Test
173
+        @DisplayName("创建用户 - 密码BCrypt加密")
174
+        void createUser_passwordEncrypted() {
175
+            Map<String, Object> req = new HashMap<>();
176
+            req.put("username", "testuser");
177
+            req.put("password", "plaintext123");
178
+            req.put("realName", "测试用户");
179
+            req.put("phone", "13800138000");
180
+            req.put("email", "test@example.com");
181
+            req.put("deptId", 1);
182
+
183
+            when(userMapper.insert(any(SysUser.class))).thenAnswer(invocation -> {
184
+                SysUser u = invocation.getArgument(0);
185
+                u.setId(1L);
186
+                return 1;
187
+            });
188
+
189
+            Long id = sysService.createUser(req);
190
+
191
+            assertEquals(1L, id);
192
+            verify(userMapper).insert(argThat(u -> {
193
+                // Password should be BCrypt encoded, not plaintext
194
+                assertNotEquals("plaintext123", u.getPassword());
195
+                assertTrue(u.getPassword().startsWith("$2a$"));
196
+                assertTrue(encoder.matches("plaintext123", u.getPassword()));
197
+                return u.getStatus() == 1;
198
+            }));
199
+        }
200
+
201
+        @Test
202
+        @DisplayName("更新用户状态 - 启用/停用")
203
+        void updateUserStatus_success() {
204
+            SysUser existing = new SysUser();
205
+            existing.setId(1L);
206
+            existing.setStatus(1);
207
+            when(userMapper.selectById(1L)).thenReturn(existing);
208
+
209
+            sysService.updateUserStatus(1L, 0);
210
+
211
+            verify(userMapper).updateById(argThat(u -> u.getStatus() == 0));
212
+        }
213
+
214
+        @Test
215
+        @DisplayName("更新用户状态 - 用户不存在抛异常")
216
+        void updateUserStatus_notFound_throws() {
217
+            when(userMapper.selectById(999L)).thenReturn(null);
218
+            assertThrows(RuntimeException.class, () -> sysService.updateUserStatus(999L, 0));
219
+        }
220
+
221
+        @Test
222
+        @DisplayName("更新用户信息 - 包含密码修改")
223
+        void updateUser_withPassword() {
224
+            SysUser existing = new SysUser();
225
+            existing.setId(1L);
226
+            existing.setUsername("testuser");
227
+            existing.setPassword("$2a$oldhash");
228
+            when(userMapper.selectById(1L)).thenReturn(existing);
229
+
230
+            Map<String, Object> req = new HashMap<>();
231
+            req.put("realName", "新名字");
232
+            req.put("password", "newpassword");
233
+
234
+            sysService.updateUser(1L, req);
235
+
236
+            verify(userMapper).updateById(argThat(u -> {
237
+                assertEquals("新名字", u.getRealName());
238
+                assertNotEquals("newpassword", u.getPassword());
239
+                assertNotEquals("$2a$oldhash", u.getPassword());
240
+                assertTrue(encoder.matches("newpassword", u.getPassword()));
241
+                return true;
242
+            }));
243
+        }
244
+
245
+        @Test
246
+        @DisplayName("更新用户信息 - 不含密码修改")
247
+        void updateUser_withoutPassword() {
248
+            SysUser existing = new SysUser();
249
+            existing.setId(1L);
250
+            existing.setPassword("$2a$oldhash");
251
+            when(userMapper.selectById(1L)).thenReturn(existing);
252
+
253
+            Map<String, Object> req = new HashMap<>();
254
+            req.put("realName", "新名字");
255
+
256
+            sysService.updateUser(1L, req);
257
+
258
+            verify(userMapper).updateById(argThat(u -> {
259
+                assertEquals("新名字", u.getRealName());
260
+                assertEquals("$2a$oldhash", u.getPassword()); // Password unchanged
261
+                return true;
262
+            }));
263
+        }
264
+
265
+        @Test
266
+        @DisplayName("验证密码 - 正确")
267
+        void verifyPassword_correct() {
268
+            SysUser existing = new SysUser();
269
+            existing.setId(1L);
270
+            existing.setPassword(encoder.encode("mypassword"));
271
+            when(userMapper.selectById(1L)).thenReturn(existing);
272
+
273
+            assertTrue(sysService.verifyPassword(1L, "mypassword"));
274
+        }
275
+
276
+        @Test
277
+        @DisplayName("验证密码 - 错误")
278
+        void verifyPassword_wrong() {
279
+            SysUser existing = new SysUser();
280
+            existing.setId(1L);
281
+            existing.setPassword(encoder.encode("mypassword"));
282
+            when(userMapper.selectById(1L)).thenReturn(existing);
283
+
284
+            assertFalse(sysService.verifyPassword(1L, "wrongpassword"));
285
+        }
286
+
287
+        @Test
288
+        @DisplayName("验证密码 - 用户不存在")
289
+        void verifyPassword_userNotFound() {
290
+            when(userMapper.selectById(999L)).thenReturn(null);
291
+            assertFalse(sysService.verifyPassword(999L, "any"));
292
+        }
293
+    }
294
+
295
+    // ========== Menu Management ==========
296
+    @Nested
297
+    @DisplayName("菜单管理")
298
+    class MenuTest {
299
+
300
+        @Test
301
+        @DisplayName("列出所有菜单")
302
+        void listMenus_success() {
303
+            SysMenu m1 = new SysMenu();
304
+            m1.setName("系统管理");
305
+            SysMenu m2 = new SysMenu();
306
+            m2.setName("文档管理");
307
+            when(menuMapper.selectList(any())).thenReturn(Arrays.asList(m1, m2));
308
+
309
+            List<SysMenu> result = sysService.listMenus();
310
+
311
+            assertEquals(2, result.size());
312
+        }
313
+
314
+        @Test
315
+        @DisplayName("创建菜单 - 正常流程")
316
+        void createMenu_success() {
317
+            Map<String, Object> req = new HashMap<>();
318
+            req.put("parentId", 0);
319
+            req.put("name", "日志管理");
320
+            req.put("path", "/system/log");
321
+            req.put("icon", "log");
322
+            req.put("component", "system/log/index");
323
+            req.put("permission", "system:log:list");
324
+            req.put("type", 1);
325
+            req.put("sort", 5);
326
+
327
+            when(menuMapper.insert(any(SysMenu.class))).thenAnswer(invocation -> {
328
+                SysMenu m = invocation.getArgument(0);
329
+                m.setId(10L);
330
+                return 1;
331
+            });
332
+
333
+            Long id = sysService.createMenu(req);
334
+
335
+            assertEquals(10L, id);
336
+            verify(menuMapper).insert(argThat(m ->
337
+                "日志管理".equals(m.getName()) &&
338
+                "/system/log".equals(m.getPath()) &&
339
+                m.getVisible() == 1 &&
340
+                m.getStatus() == 1
341
+            ));
342
+        }
343
+    }
344
+
345
+    // ========== Department Management ==========
346
+    @Nested
347
+    @DisplayName("部门管理")
348
+    class DeptTest {
349
+
350
+        @Test
351
+        @DisplayName("列出所有部门")
352
+        void listDepts_success() {
353
+            SysDepartment d1 = new SysDepartment();
354
+            d1.setDeptName("水利局");
355
+            SysDepartment d2 = new SysDepartment();
356
+            d2.setDeptName("水务公司");
357
+            when(deptMapper.selectList(any())).thenReturn(Arrays.asList(d1, d2));
358
+
359
+            List<SysDepartment> result = sysService.listDepts();
360
+
361
+            assertEquals(2, result.size());
362
+        }
363
+
364
+        @Test
365
+        @DisplayName("创建部门 - 正常流程")
366
+        void createDept_success() {
367
+            Map<String, Object> req = new HashMap<>();
368
+            req.put("deptName", "技术部");
369
+            req.put("parentId", 1);
370
+            req.put("leader", "张三");
371
+            req.put("phone", "13900139000");
372
+            req.put("sort", 5);
373
+
374
+            when(deptMapper.insert(any(SysDepartment.class))).thenAnswer(invocation -> {
375
+                SysDepartment d = invocation.getArgument(0);
376
+                d.setId(10L);
377
+                return 1;
378
+            });
379
+
380
+            Long id = sysService.createDept(req);
381
+
382
+            assertEquals(10L, id);
383
+            verify(deptMapper).insert(argThat(d ->
384
+                "技术部".equals(d.getDeptName()) &&
385
+                d.getParentId() == 1L &&
386
+                "张三".equals(d.getLeader()) &&
387
+                d.getStatus() == 1
388
+            ));
389
+        }
390
+
391
+        @Test
392
+        @DisplayName("更新部门 - 正常流程")
393
+        void updateDept_success() {
394
+            SysDepartment existing = new SysDepartment();
395
+            existing.setId(1L);
396
+            existing.setDeptName("旧名称");
397
+            when(deptMapper.selectById(1L)).thenReturn(existing);
398
+
399
+            Map<String, Object> req = new HashMap<>();
400
+            req.put("deptName", "新名称");
401
+            req.put("leader", "李四");
402
+            req.put("status", 0);
403
+
404
+            sysService.updateDept(1L, req);
405
+
406
+            verify(deptMapper).updateById(argThat(d ->
407
+                "新名称".equals(d.getDeptName()) &&
408
+                "李四".equals(d.getLeader()) &&
409
+                d.getStatus() == 0
410
+            ));
411
+        }
412
+
413
+        @Test
414
+        @DisplayName("更新部门 - 不存在抛异常")
415
+        void updateDept_notFound_throws() {
416
+            when(deptMapper.selectById(999L)).thenReturn(null);
417
+            assertThrows(RuntimeException.class, () -> sysService.updateDept(999L, new HashMap<>()));
418
+        }
419
+    }
420
+
421
+    // ========== Log Management ==========
422
+    @Nested
423
+    @DisplayName("日志管理")
424
+    class LogTest {
425
+
426
+        @Test
427
+        @DisplayName("列出所有日志 - 无筛选")
428
+        void listLogs_noFilter() {
429
+            SysLog log1 = new SysLog();
430
+            log1.setUsername("admin");
431
+            SysLog log2 = new SysLog();
432
+            log2.setUsername("operator");
433
+            when(logMapper.selectList(any())).thenReturn(Arrays.asList(log1, log2));
434
+
435
+            List<SysLog> result = sysService.listLogs(null, null);
436
+
437
+            assertEquals(2, result.size());
438
+        }
439
+
440
+        @Test
441
+        @DisplayName("按日志类型筛选")
442
+        void listLogs_byType() {
443
+            SysLog log = new SysLog();
444
+            log.setLogType("LOGIN");
445
+            when(logMapper.selectList(any())).thenReturn(Collections.singletonList(log));
446
+
447
+            List<SysLog> result = sysService.listLogs("LOGIN", null);
448
+
449
+            assertEquals(1, result.size());
450
+            assertEquals("LOGIN", result.get(0).getLogType());
451
+        }
452
+
453
+        @Test
454
+        @DisplayName("按用户名筛选")
455
+        void listLogs_byUsername() {
456
+            when(logMapper.selectList(any())).thenReturn(Collections.emptyList());
457
+            List<SysLog> result = sysService.listLogs(null, "admin");
458
+            assertTrue(result.isEmpty());
459
+        }
460
+
461
+        @Test
462
+        @DisplayName("创建日志 - 正常流程")
463
+        void createLog_success() {
464
+            Map<String, Object> req = new HashMap<>();
465
+            req.put("userId", 1);
466
+            req.put("username", "admin");
467
+            req.put("logType", "LOGIN");
468
+            req.put("module", "系统管理");
469
+            req.put("action", "用户登录");
470
+            req.put("requestUrl", "/api/login");
471
+            req.put("requestMethod", "POST");
472
+            req.put("ip", "192.168.1.1");
473
+            req.put("duration", 150);
474
+
475
+            when(logMapper.insert(any(SysLog.class))).thenAnswer(invocation -> {
476
+                SysLog l = invocation.getArgument(0);
477
+                l.setId(1L);
478
+                return 1;
479
+            });
480
+
481
+            Long id = sysService.createLog(req);
482
+
483
+            assertEquals(1L, id);
484
+            verify(logMapper).insert(argThat(l ->
485
+                "admin".equals(l.getUsername()) &&
486
+                "LOGIN".equals(l.getLogType()) &&
487
+                "用户登录".equals(l.getAction()) &&
488
+                l.getDuration() == 150L
489
+            ));
490
+        }
491
+
492
+        @Test
493
+        @DisplayName("创建日志 - 最小字段")
494
+        void createLog_minimal() {
495
+            Map<String, Object> req = new HashMap<>();
496
+            req.put("logType", "ERROR");
497
+            req.put("module", "系统");
498
+
499
+            when(logMapper.insert(any(SysLog.class))).thenAnswer(invocation -> {
500
+                SysLog l = invocation.getArgument(0);
501
+                l.setId(2L);
502
+                return 1;
503
+            });
504
+
505
+            Long id = sysService.createLog(req);
506
+
507
+            assertEquals(2L, id);
508
+            verify(logMapper).insert(argThat(l ->
509
+                "ERROR".equals(l.getLogType()) &&
510
+                l.getUserId() == null &&
511
+                l.getDuration() == null
512
+            ));
513
+        }
514
+    }
515
+}