diff --git a/src/main/java/com/project/base/config/InsertBatchOnDuplicateKeyUpdate.java b/src/main/java/com/project/base/config/InsertBatchOnDuplicateKeyUpdate.java new file mode 100644 index 0000000..97f07bb --- /dev/null +++ b/src/main/java/com/project/base/config/InsertBatchOnDuplicateKeyUpdate.java @@ -0,0 +1,56 @@ +package com.project.base.config; + +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import org.apache.ibatis.executor.keygen.NoKeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlSource; + +import java.util.stream.Collectors; + +/** + * 自定义选装件:MySQL 批量 Upsert + */ +public class InsertBatchOnDuplicateKeyUpdate extends AbstractMethod { + + protected InsertBatchOnDuplicateKeyUpdate() { + super("batchUpsert"); // 方法名 + } + + @Override + public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) { + final String methodName = "batchUpsert"; + + // 1. 准备 INSERT 字段 + String columnScript = "(" + tableInfo.getKeyColumn() + "," + + tableInfo.getFieldList().stream() + .map(TableFieldInfo::getColumn) + .collect(Collectors.joining(",")) + ")"; + + // 2. 准备 VALUES 部分 (修复重点!) + String valuesScript = "(#{item." + tableInfo.getKeyProperty() + "}," + + tableInfo.getFieldList().stream() + .map(i -> { + // 【核心修复】检查该字段是否有 TypeHandler (如 JacksonTypeHandler) + if (i.getTypeHandler() != null) { + // 生成格式如:#{item.depIdPath,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler} + return "#{item." + i.getProperty() + ",typeHandler=" + i.getTypeHandler().getName() + "}"; + } + return "#{item." + i.getProperty() + "}"; + }) + .collect(Collectors.joining(",")) + ")"; + + // 3. 准备 ON DUPLICATE KEY UPDATE 部分 + String updateScript = tableInfo.getFieldList().stream() + .filter(i -> !i.getColumn().equals("create_time")) + .map(i -> i.getColumn() + " = VALUES(" + i.getColumn() + ")") + .collect(Collectors.joining(",")) + ", update_time = NOW()"; + + String sql = String.format("", + tableInfo.getTableName(), columnScript, valuesScript, updateScript); + + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); + return this.addInsertMappedStatement(mapperClass, modelClass, methodName, sqlSource, new NoKeyGenerator(), null, null); + } +} \ No newline at end of file diff --git a/src/main/java/com/project/base/config/MybatisPlusConfig.java b/src/main/java/com/project/base/config/MybatisPlusConfig.java new file mode 100644 index 0000000..f2218d6 --- /dev/null +++ b/src/main/java/com/project/base/config/MybatisPlusConfig.java @@ -0,0 +1,20 @@ +package com.project.base.config; + +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class MybatisPlusConfig extends DefaultSqlInjector { + + @Override + public List getMethodList(Class mapperClass, TableInfo tableInfo) { + List methodList = super.getMethodList(mapperClass, tableInfo); + methodList.add(new InsertBatchOnDuplicateKeyUpdate()); // 确保这里添加了你的自定义方法 + return methodList; + } + +} \ No newline at end of file diff --git a/src/main/java/com/project/base/mapper/BatchUpsertMapper.java b/src/main/java/com/project/base/mapper/BatchUpsertMapper.java new file mode 100644 index 0000000..e7e745f --- /dev/null +++ b/src/main/java/com/project/base/mapper/BatchUpsertMapper.java @@ -0,0 +1,12 @@ +package com.project.base.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +public interface BatchUpsertMapper extends BaseMapper { + /** + * 自定义全局批量 Upsert + */ + int batchUpsert(@Param("list") List list); +} \ No newline at end of file diff --git a/src/main/java/com/project/ding/controller/DepartmentController.java b/src/main/java/com/project/ding/controller/DepartmentController.java index 48fe01a..211e211 100644 --- a/src/main/java/com/project/ding/controller/DepartmentController.java +++ b/src/main/java/com/project/ding/controller/DepartmentController.java @@ -2,8 +2,7 @@ package com.project.ding.controller; import com.github.tingyugetc520.ali.dingtalk.api.DtService; -import com.github.tingyugetc520.ali.dingtalk.bean.department.DtDepart; -import com.github.tingyugetc520.ali.dingtalk.bean.user.DtUser; +import com.project.ding.domain.dto.DepartmentDTO; import com.project.ding.domain.dto.DingUserDTO; import com.project.ding.utils.DingUtil; import jakarta.servlet.http.HttpServletResponse; @@ -25,7 +24,7 @@ public class DepartmentController { private DtService dtService; @GetMapping("/test") public void test(HttpServletResponse response) throws Exception { - List list = dingUtil.getAllDepartment(); + List list = dingUtil.getAllDepartment(); List userList = new ArrayList<>(); for (int i = 0; i < list.size(); i++) { diff --git a/src/main/java/com/project/ding/controller/SyncController.java b/src/main/java/com/project/ding/controller/SyncController.java new file mode 100644 index 0000000..7c7ea5c --- /dev/null +++ b/src/main/java/com/project/ding/controller/SyncController.java @@ -0,0 +1,25 @@ +package com.project.ding.controller; + + +import com.project.ding.utils.DingUserSyncUtil; +import org.springframework.beans.factory.annotation.Autowired; +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; + +@RestController +@RequestMapping("/ding") +public class SyncController { + @Autowired + private DingUserSyncUtil dingUserSyncUtil; + + /** + * 手动触发同步接口 + * 强刷:/sync/all?force=true + */ + @GetMapping("/sync") + public String triggerSync(@RequestParam(defaultValue = "false") boolean force) { + return dingUserSyncUtil.triggerSync(force); + } +} diff --git a/src/main/java/com/project/ding/domain/dto/DepartmentDTO.java b/src/main/java/com/project/ding/domain/dto/DepartmentDTO.java index d58f4ab..ef09bc9 100644 --- a/src/main/java/com/project/ding/domain/dto/DepartmentDTO.java +++ b/src/main/java/com/project/ding/domain/dto/DepartmentDTO.java @@ -4,10 +4,18 @@ package com.project.ding.domain.dto; import com.project.base.domain.dto.BaseDTO; import lombok.Data; +import java.util.List; + @Data public class DepartmentDTO extends BaseDTO { private String name; private Long parentId; + private List childrenList; + + private Boolean leaf = Boolean.TRUE; + + private List deptIdPath; + } diff --git a/src/main/java/com/project/ding/domain/dto/DingUserDTO.java b/src/main/java/com/project/ding/domain/dto/DingUserDTO.java index b548454..e8d2685 100644 --- a/src/main/java/com/project/ding/domain/dto/DingUserDTO.java +++ b/src/main/java/com/project/ding/domain/dto/DingUserDTO.java @@ -64,4 +64,16 @@ public class DingUserDTO { @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") private Date hiredDate; + @JsonProperty("active") + private Boolean active; + + @JsonProperty("admin") + private Boolean admin; + + @JsonProperty("boss") + private Boolean boss; + + @JsonProperty("leader") + private Boolean leader; + } diff --git a/src/main/java/com/project/ding/domain/dto/UserDTO.java b/src/main/java/com/project/ding/domain/dto/UserDTO.java index 895ddb1..16979d7 100644 --- a/src/main/java/com/project/ding/domain/dto/UserDTO.java +++ b/src/main/java/com/project/ding/domain/dto/UserDTO.java @@ -32,6 +32,14 @@ public class UserDTO extends BaseDTO { @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") private Date hiredDate; + private Boolean active; + + private Boolean admin; + + private Boolean boss; + + private Boolean leader; + /** * 需要参与考试任务数 */ diff --git a/src/main/java/com/project/ding/domain/entity/DepartmentEntity.java b/src/main/java/com/project/ding/domain/entity/DepartmentEntity.java index 02364e2..39b9e63 100644 --- a/src/main/java/com/project/ding/domain/entity/DepartmentEntity.java +++ b/src/main/java/com/project/ding/domain/entity/DepartmentEntity.java @@ -1,7 +1,10 @@ package com.project.ding.domain.entity; +import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.project.base.domain.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -9,14 +12,20 @@ import jakarta.persistence.Index; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.util.List; @Data @Table(name = "evaluator_department" , indexes = {@Index(name = "Idx_parentId", columnList = "parent_id")}) @Entity -@TableName("evaluator_department") +@TableName(value = "evaluator_department" , autoResultMap = true) @EqualsAndHashCode(callSuper = true) public class DepartmentEntity extends BaseEntity { + @TableId(type = IdType.INPUT) + private Long id; @Column(name = "name" , columnDefinition="varchar(255) comment '部门名称'") private String name; @@ -24,4 +33,10 @@ public class DepartmentEntity extends BaseEntity { @TableField("parent_id") @Column(name = "parent_id" , columnDefinition="bigint(20) comment '父部门id'") private Long parentId; + + + @TableField(typeHandler = JacksonTypeHandler.class) + @Column(name = "dept_id_path", columnDefinition = "json comment '部门ID全路径'") + @JdbcTypeCode(SqlTypes.JSON) + private List deptIdPath; } diff --git a/src/main/java/com/project/ding/domain/entity/UserEntity.java b/src/main/java/com/project/ding/domain/entity/UserEntity.java index 7579615..ae5b829 100644 --- a/src/main/java/com/project/ding/domain/entity/UserEntity.java +++ b/src/main/java/com/project/ding/domain/entity/UserEntity.java @@ -1,6 +1,10 @@ package com.project.ding.domain.entity; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.project.base.domain.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -8,16 +12,22 @@ import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Comment; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; import java.util.Date; +import java.util.List; @Data @Table(name = "evaluator_user" ) @Entity -@TableName("evaluator_user") +@TableName(value = "evaluator_user", autoResultMap = true) // 必须开启 autoResultMap 才能让 TypeHandler 生效 @EqualsAndHashCode(callSuper = true) public class UserEntity extends BaseEntity { + @TableId(type = IdType.INPUT) + private Long id; + @Column(name = "union_id" , columnDefinition="varchar(255) comment '企业架构内唯一标识'") private String unionId; @@ -39,7 +49,27 @@ public class UserEntity extends BaseEntity { @Column(name = "dept_id_str" , columnDefinition="varchar(2000) comment '部门id集合'") private String deptIdStr; + /** + * 存储为 JSON 数组: [1, 10, 101] + */ + @TableField(typeHandler = JacksonTypeHandler.class) + @Column(name = "dept_id_list", columnDefinition = "json comment '部门ID列表'") + @JdbcTypeCode(SqlTypes.JSON) + private List deptIdList; + @Column(name = "hired_date") @Comment("入职时间") private Date hiredDate; + + @Column(name = "active", columnDefinition = "tinyint(1) default 1 comment '是否激活钉钉: 1-已激活, 0-未激活'") + private Boolean active; + + @Column(name = "admin", columnDefinition = "tinyint(1) default 0 comment '是否为企业管理员: 1-是, 0-不是'") + private Boolean admin; + + @Column(name = "boss", columnDefinition = "tinyint(1) default 0 comment '是否为企业老板: 1-是, 0-不是'") + private Boolean boss; + + @Column(name = "leader", columnDefinition = "tinyint(1) default 0 comment '是否为部门主管: 1-是, 0-不是'") + private Boolean leader; } diff --git a/src/main/java/com/project/ding/domain/service/DepartmentBaseService.java b/src/main/java/com/project/ding/domain/service/DepartmentBaseService.java new file mode 100644 index 0000000..256ad5c --- /dev/null +++ b/src/main/java/com/project/ding/domain/service/DepartmentBaseService.java @@ -0,0 +1,8 @@ +package com.project.ding.domain.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.project.ding.domain.entity.DepartmentEntity; +import com.project.ding.domain.entity.UserEntity; + +public interface DepartmentBaseService extends IService { +} diff --git a/src/main/java/com/project/ding/domain/service/impl/DepartmentBaseServiceImpl.java b/src/main/java/com/project/ding/domain/service/impl/DepartmentBaseServiceImpl.java new file mode 100644 index 0000000..691eab0 --- /dev/null +++ b/src/main/java/com/project/ding/domain/service/impl/DepartmentBaseServiceImpl.java @@ -0,0 +1,11 @@ +package com.project.ding.domain.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.project.ding.domain.entity.DepartmentEntity; +import com.project.ding.domain.service.DepartmentBaseService; +import com.project.ding.mapper.DepartmentMapper; +import org.springframework.stereotype.Service; + +@Service +public class DepartmentBaseServiceImpl extends ServiceImpl implements DepartmentBaseService { +} diff --git a/src/main/java/com/project/ding/mapper/DepartmentMapper.java b/src/main/java/com/project/ding/mapper/DepartmentMapper.java index 4dffc03..d54a027 100644 --- a/src/main/java/com/project/ding/mapper/DepartmentMapper.java +++ b/src/main/java/com/project/ding/mapper/DepartmentMapper.java @@ -1,9 +1,11 @@ package com.project.ding.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.project.base.mapper.BatchUpsertMapper; import com.project.ding.domain.entity.DepartmentEntity; import org.apache.ibatis.annotations.Mapper; @Mapper -public interface DepartmentMapper extends BaseMapper { +public interface DepartmentMapper extends BaseMapper, BatchUpsertMapper { + } diff --git a/src/main/java/com/project/ding/mapper/UserMapper.java b/src/main/java/com/project/ding/mapper/UserMapper.java index b7757c1..3d29053 100644 --- a/src/main/java/com/project/ding/mapper/UserMapper.java +++ b/src/main/java/com/project/ding/mapper/UserMapper.java @@ -1,7 +1,8 @@ package com.project.ding.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.project.base.mapper.BatchUpsertMapper; import com.project.ding.domain.entity.UserEntity; -public interface UserMapper extends BaseMapper { +public interface UserMapper extends BaseMapper , BatchUpsertMapper { } diff --git a/src/main/java/com/project/ding/utils/DingUserSyncUtil.java b/src/main/java/com/project/ding/utils/DingUserSyncUtil.java index 34b46e1..cae104b 100644 --- a/src/main/java/com/project/ding/utils/DingUserSyncUtil.java +++ b/src/main/java/com/project/ding/utils/DingUserSyncUtil.java @@ -1,19 +1,24 @@ package com.project.ding.utils; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.StrUtil; +import com.project.base.domain.utils.TreeUtils; +import com.project.ding.domain.dto.DepartmentDTO; import com.project.ding.domain.dto.UserDTO; +import com.project.ding.domain.entity.DepartmentEntity; import com.project.ding.domain.entity.SyncLogEntity; import com.project.ding.domain.entity.UserEntity; -import com.project.ding.domain.service.UserBaseService; +import com.project.ding.mapper.DepartmentMapper; import com.project.ding.mapper.SyncLogMapper; +import com.project.ding.mapper.UserMapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; -import java.util.Date; -import java.util.List; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -29,7 +34,10 @@ public class DingUserSyncUtil { private StringRedisTemplate redisTemplate; @Autowired - private UserBaseService userBaseService; + private UserMapper userMapper; + + @Autowired + private DepartmentMapper departmentMapper; @Autowired private DingUtil dingUtil; @@ -85,10 +93,44 @@ public class DingUserSyncUtil { // 开始同步 try { + List allDepartment = dingUtil.getAllDepartment(); + allDepartment.forEach(dto -> dto.setDeptIdPath(new ArrayList<>(Collections.singletonList(dto.getId())))); + // 转树 + List departmentDTOtreeList = TreeUtils.buildLongTree(allDepartment, DepartmentDTO::getId, DepartmentDTO::getParentId, (parentDTO, childrenList) -> { + parentDTO.setChildrenList(childrenList); + parentDTO.setLeaf(Boolean.FALSE); + childrenList.forEach(childDTO -> childDTO.getDeptIdPath().addAll(0, parentDTO.getDeptIdPath())); + }); + // 树转回列表 + List departmentDTOList = TreeUtils.tree2List(departmentDTOtreeList, DepartmentDTO::getChildrenList); + Map> depIdPathMap = new HashMap<>(); + departmentDTOList.forEach(dto -> depIdPathMap.put(dto.getId().toString() , dto.getDeptIdPath())); + + + List> departmentPartitions = ListUtil.partition(departmentDTOList, 300); + for (List batch : departmentPartitions) { + departmentMapper.batchUpsert(batch.stream() + .map(dto -> dto.toEntity(DepartmentEntity::new)) + .collect(Collectors.toList())); + } + List allUserDTOList = dingUtil.getAllUserDTO(); - userBaseService.saveOrUpdateBatch(allUserDTOList.stream() - .map(dto -> dto.toEntity(UserEntity::new)) - .collect(Collectors.toList()) , 300); + allUserDTOList.forEach(user -> { + Set depIdPathSet = new HashSet<>(); + if (StrUtil.isNotBlank(user.getDeptIdStr())) { + for (String idStr : user.getDeptIdStr().split(",")) { + depIdPathSet.addAll(depIdPathMap.get(idStr)); + } + } + user.setDeptIdList(depIdPathSet.stream().toList()); + }); + List> userPartitions = ListUtil.partition(allUserDTOList, 300); + for (List batch : userPartitions) { + userMapper.batchUpsert(batch.stream() + .map(dto -> dto.toEntity(UserEntity::new)) + .collect(Collectors.toList())); + } + syncLogEntity.setStatus(1); syncLogEntity.setEndTime(new Date()); syncLogMapper.updateById(syncLogEntity); diff --git a/src/main/java/com/project/ding/utils/DingUtil.java b/src/main/java/com/project/ding/utils/DingUtil.java index f4326ad..1fbb217 100644 --- a/src/main/java/com/project/ding/utils/DingUtil.java +++ b/src/main/java/com/project/ding/utils/DingUtil.java @@ -1,5 +1,6 @@ package com.project.ding.utils; +import cn.hutool.core.bean.BeanUtil; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tingyugetc520.ali.dingtalk.api.DtService; @@ -7,6 +8,7 @@ import com.github.tingyugetc520.ali.dingtalk.bean.department.DtDepart; import com.google.gson.JsonObject; import com.jayway.jsonpath.JsonPath; +import com.project.ding.domain.dto.DepartmentDTO; import com.project.ding.domain.dto.DingUserDTO; import com.project.ding.domain.dto.UserDTO; import io.vavr.control.Try; @@ -14,7 +16,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @Component @@ -24,12 +28,19 @@ public class DingUtil { - public List getAllDepartment() throws Exception { - return dtService.getDepartmentService().list(null , true); + public List getAllDepartment() throws Exception { + List list = dtService.getDepartmentService().list(null, true); + List res = new ArrayList<>(); + for (DtDepart dtDepart : list) { + DepartmentDTO departmentDTO = new DepartmentDTO(); + BeanUtil.copyProperties(dtDepart , departmentDTO); + res.add(departmentDTO); + } + return res; } public List getAllDingUserDTO() throws Exception { - List list = getAllDepartment(); + List list = getAllDepartment(); List userList = new ArrayList<>(); for (int i = 0; i < list.size(); i++) { @@ -40,16 +51,34 @@ public class DingUtil { } public List getAllUserDTO() throws Exception { - List list = getAllDepartment(); + List list = getAllDepartment(); List userList = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { List userInDepartment = getUserIdInDepartment(list.get(i).getId()); userList.addAll(userInDepartment.stream() .map(UserDTO::fromDingUserDTO) .collect(Collectors.toList())); } - return userList; + return userList.stream() + .collect(Collectors.toMap( + UserDTO::getId, + u -> u, + this::mergeUser + )) + .values() + .stream() + .toList(); + } + + /** + * 定义合并规则:处理权限“或”逻辑 + */ + private UserDTO mergeUser(UserDTO u1, UserDTO u2) { + // 权限合并:只要其中一个是 true,结果就是 true + u1.setLeader(Boolean.logicalOr(Boolean.TRUE.equals(u1.getLeader()), Boolean.TRUE.equals(u2.getLeader()))); + return u1; } public List getUserIdInDepartment(Long id) throws Exception { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 414c3f3..e8f41f4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,6 @@ server: port: 7088 + spring: datasource: dynamic: @@ -8,8 +9,22 @@ spring: master: driverClassName: com.mysql.cj.jdbc.Driver password: Itc@123456 - url: jdbc:mysql://172.16.204.50:3306/proposal_workshop?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true + url: jdbc:mysql://172.16.204.50:3306/ai_evaluator?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true username: root + # Redis + data: + redis: + host: 172.16.204.50 + port: 6379 + password: 123456 + database: 3 + timeout: 5000ms + lettuce: + pool: + max-active: 8 + max-idle: 30 + max-wait: 10000 + min-idle: 10 jpa: hibernate: ddl-auto: update diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000..0cd3397 Binary files /dev/null and b/src/main/resources/static/favicon.ico differ