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

Merge remote-tracking branch 'origin/master'

zhujing пре 1 месец
родитељ
комит
393d614742
37 измењених фајлова са 1787 додато и 123 уклоњено
  1. 26 9
      java/storlead-api/src/main/java/com/storlead/login/TenantLoginApiController.java
  2. 40 13
      java/storlead-api/src/main/java/com/storlead/system/controller/LoginApiController.java
  3. 41 0
      java/storlead-api/src/main/java/com/storlead/system/controller/SysLoginLogApiController.java
  4. 2 1
      java/storlead-dependencies/pom.xml
  5. 92 0
      java/storlead-message/sql/arg_template/_gen_arg_templates.py
  6. 26 0
      java/storlead-message/sql/arg_template/arg_template_json_seed.sql
  7. 66 0
      java/storlead-message/sql/arg_template/broadcast_total_message_arg_template.json
  8. 30 0
      java/storlead-message/sql/arg_template/bulletin_message_arg_template.json
  9. 106 0
      java/storlead-message/sql/arg_template/business_message_arg_template.json
  10. 130 0
      java/storlead-message/sql/arg_template/customer_message_arg_template.json
  11. 26 0
      java/storlead-message/sql/arg_template/follow_message_arg_template.json
  12. 126 0
      java/storlead-message/sql/arg_template/liaison_message_arg_template.json
  13. 114 0
      java/storlead-message/sql/arg_template/order_message_arg_template.json
  14. 10 0
      java/storlead-message/sql/arg_template/other_extend_message_arg_template.json
  15. 30 0
      java/storlead-message/sql/arg_template/performance_goal_message_arg_template.json
  16. 10 0
      java/storlead-message/sql/arg_template/task_message_arg_template.json
  17. 48 6
      java/storlead-message/storlead-message-api/src/main/java/com/storlead/message/controller/MessageApiController.java
  18. 66 0
      java/storlead-message/storlead-message-api/src/main/java/com/storlead/message/controller/MessageTemplateApiController.java
  19. 0 6
      java/storlead-message/storlead-message-biz/src/main/java/com/storlead/message/service/InsideMessageRecordService.java
  20. 0 64
      java/storlead-message/storlead-message-biz/src/main/java/com/storlead/message/service/impl/InsideMessageRecordServiceImpl.java
  21. 11 0
      java/storlead-message/storlead-message-biz/src/main/java/com/storlead/message/service/impl/MessageService.java
  22. 17 24
      java/storlead-message/storlead-message-core/src/main/java/com/storlead/message/pojo/dto/MessageSendDTO.java
  23. 67 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/enums/TradeMessageTypeEnum.java
  24. 25 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/message/TradeMessageTypeCatalog.java
  25. 44 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/TradeMessageAssembleService.java
  26. 71 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/TradeMessageAssembleServiceImpl.java
  27. 30 0
      java/storlead-system/sql/sys_login_log.sql
  28. 10 0
      java/storlead-system/storlead-system-biz/src/main/java/com/storlead/system/mapper/SysLoginLogMapper.java
  29. 122 0
      java/storlead-system/storlead-system-biz/src/main/java/com/storlead/system/service/impl/SysLoginLogServiceImpl.java
  30. 200 0
      java/storlead-system/storlead-system-biz/src/main/java/com/storlead/system/util/LoginClientInfoUtil.java
  31. 21 0
      java/storlead-system/storlead-system-core/src/main/java/com/storlead/system/enums/LoginLogEventTypeEnum.java
  32. 21 0
      java/storlead-system/storlead-system-core/src/main/java/com/storlead/system/enums/LoginLogStatusEnum.java
  33. 49 0
      java/storlead-system/storlead-system-core/src/main/java/com/storlead/system/pojo/dto/SysLoginLogQueryDTO.java
  34. 67 0
      java/storlead-system/storlead-system-core/src/main/java/com/storlead/system/pojo/entity/SysLoginLogEntity.java
  35. 37 0
      java/storlead-system/storlead-system-spi/src/main/java/com/storlead/system/service/SysLoginLogService.java
  36. 3 0
      java/storlead-user/sql/user_add_remark.sql
  37. 3 0
      java/storlead-user/storlead-user-core/src/main/java/com/storlead/user/pojo/entity/UserEntity.java

+ 26 - 9
java/storlead-api/src/main/java/com/storlead/login/TenantLoginApiController.java

@@ -17,6 +17,7 @@ import com.storlead.sms.spi.SmsCaptchaScene;
 import com.storlead.sms.spi.SmsCaptchaService;
 import com.storlead.system.service.IMenuService;
 import com.storlead.system.service.IUserRoleService;
+import com.storlead.system.service.SysLoginLogService;
 import com.storlead.system.util.SystemConfigItemCacheUtil;
 import com.storlead.user.pojo.dto.TenantLoginDTO;
 import com.storlead.user.pojo.entity.DeptEntity;
@@ -76,14 +77,18 @@ public class TenantLoginApiController {
     @Resource
     private ITenantUserService tenantUserService;
 
+    @Resource
+    private SysLoginLogService loginLogService;
+
     @RequestMapping(value = "/tenant/login", method = RequestMethod.POST)
     @ApiOperation(value = "租户登录", notes = "仅支持短信验证码或密码;需指定 tenantId 或 enterpriseCode;通过后必须在 tenant_user 存在对应关系")
     public Result<?> tenantLogin(@RequestBody TenantLoginDTO loginDTO) {
+        String account = loginDTO != null ? StrUtil.trimToEmpty(loginDTO.getAccount()) : null;
         if (loginDTO == null) {
-            return Result.error("参数错误");
+            return tenantLoginFail(account, null, "参数错误");
         }
         if (loginDTO.getTenantId() == null && StrUtil.isBlank(loginDTO.getEnterpriseCode())) {
-            return Result.error("请指定 tenantId 或 enterpriseCode");
+            return tenantLoginFail(account, null, "请指定 tenantId 或 enterpriseCode");
         }
 
         TenantContext.setIgnoreTenantLine(true);
@@ -95,32 +100,32 @@ public class TenantLoginApiController {
                 enterprise = tenantEnterpriseService.getByEnterpriseCode(loginDTO.getEnterpriseCode());
             }
             if (enterprise == null) {
-                return Result.error("租户不存在或已删除");
+                return tenantLoginFail(account, null, "租户不存在或已删除");
             }
             if (Boolean.FALSE.equals(enterprise.getEnabled())) {
-                return Result.error("租户已禁用");
+                return tenantLoginFail(account, null, "租户已禁用");
             }
             if (enterprise.getExpireAt() != null && enterprise.getExpireAt().before(new Date())) {
-                return Result.error("租户已过期");
+                return tenantLoginFail(account, null, "租户已过期");
             }
 
             JSONObject obj = new JSONObject();
             UserEntity userInfo = authenticateAccount(loginDTO, obj);
             if (userInfo == null) {
-                return Result.error(obj.getString("_err"));
+                return tenantLoginFail(account, null, obj.getString("_err"));
             }
 
             if (!userInfo.getEnabled()) {
-                return Result.error("该账号用户暂无登录权限");
+                return tenantLoginFail(account, userInfo, "该账号用户暂无登录权限");
             }
 
             // 租户成员以 tenant_user 为准:无记录则不允许进入该租户空间
             TenantUserEntity tenantUser = tenantUserService.getByTenantIdAndUserId(enterprise.getTenantId(), userInfo.getId());
             if (tenantUser == null) {
-                return Result.error("该账号未在 tenant_user 中绑定当前租户");
+                return tenantLoginFail(account, userInfo, "该账号未在 tenant_user 中绑定当前租户");
             }
             if (tenantUser.getEnterpriseId() != null && !tenantUser.getEnterpriseId().equals(enterprise.getId())) {
-                return Result.error("租户成员数据不一致,请联系管理员检查 tenant_user");
+                return tenantLoginFail(account, userInfo, "租户成员数据不一致,请联系管理员检查 tenant_user");
             }
 
 //            Integer possessMenuCount = menuService.checkUserMenuAccess(userInfo.getId());
@@ -162,6 +167,10 @@ public class TenantLoginApiController {
             obj.put("enterpriseId", enterprise.getId());
             @SuppressWarnings("unchecked")
             Map<String, Object> body = JSONObject.parseObject(obj.toJSONString(), Map.class);
+            loginLogService.recordLoginSuccess(userInfo.getId(),
+                    StrUtil.blankToDefault(account, userInfo.getMobile()),
+                    userInfo.getUserName(),
+                    userInfo.getRealName());
             return Result.result(body);
         } finally {
             TenantContext.setIgnoreTenantLine(false);
@@ -241,4 +250,12 @@ public class TenantLoginApiController {
             return null;
         }
     }
+
+    private Result<?> tenantLoginFail(String account, UserEntity user, String message) {
+        Long userId = user != null ? user.getId() : null;
+        String userName = user != null ? user.getUserName() : null;
+        String realName = user != null ? user.getRealName() : null;
+        loginLogService.recordLoginFail(account, userId, userName, realName, message);
+        return Result.error(message);
+    }
 }

+ 40 - 13
java/storlead-api/src/main/java/com/storlead/system/controller/LoginApiController.java

@@ -26,6 +26,7 @@ import com.storlead.sms.spi.SmsLogService;
 import com.storlead.system.pojo.entity.MenuEntity;
 import com.storlead.system.service.IMenuService;
 import com.storlead.system.service.IUserRoleService;
+import com.storlead.system.service.SysLoginLogService;
 import com.storlead.system.util.SystemConfigItemCacheUtil;
 import com.storlead.user.pojo.dto.UserLoginDTO;
 import com.storlead.user.pojo.entity.DeptEntity;
@@ -95,6 +96,9 @@ public class LoginApiController {
     @Resource
     private CorpWeChatService corpWeChatService;
 
+    @Resource
+    private SysLoginLogService loginLogService;
+
     @PostMapping(value = "/redirect")
     @ResponseBody
     public Object redirect(HttpServletRequest request, HttpServletResponse response)  throws IOException {
@@ -107,9 +111,10 @@ public class LoginApiController {
     public Result login(@RequestBody UserLoginDTO loginDTO) {
 
         if (Objects.isNull(loginDTO)) {
-            return Result.error("参数错误");
+            return loginFail(null, null, "参数错误");
         }
-        UserEntity userInfo;
+        String account = StrUtil.trimToEmpty(loginDTO.getAccount());
+        UserEntity userInfo = null;
         JSONObject obj = new JSONObject();
         if (StrUtil.isNotBlank(loginDTO.getWxCode())) {
             String code = corpWeChatService.login(loginDTO.getWxCode());
@@ -118,28 +123,28 @@ public class LoginApiController {
             String mobile = StrUtil.trimToEmpty(loginDTO.getAccount());
             String smsCode = StrUtil.trimToEmpty(loginDTO.getSmsCode());
             if (StrUtil.isBlank(mobile)) {
-                return Result.error("请输入手机号");
+                return loginFail(account, null, "请输入手机号");
             }
             if (StrUtil.isBlank(smsCode)) {
-                return Result.error("请输入验证码");
+                return loginFail(account, null, "请输入验证码");
             }
             Result<?> vr = smsCaptchaService.verifyCaptcha(mobile, smsCode, SmsCaptchaScene.LOGIN, true);
             if (!vr.isSuccess()) {
-                return Result.error(vr.getCode(), vr.getMessage());
+                return loginFail(account, null, vr.getMessage());
             }
             userInfo = sysUserService.getOne(new LambdaQueryWrapper<UserEntity>()
                     .eq(UserEntity::getMobile, mobile)
                     .eq(UserEntity::getIsDelete, CommonConstant.DEL_FLAG_0));
             if (userInfo == null) {
-                return Result.error("该手机号未绑定用户或账号已删除");
+                return loginFail(account, null, "该手机号未绑定用户或账号已删除");
             }
         } else {
             if (StrUtil.isBlank(loginDTO.getAccount()) || StrUtil.isBlank(loginDTO.getPassword())) {
-                return Result.error("用户名或密码为空");
+                return loginFail(account, null, "用户名或密码为空");
             }
             userInfo = sysUserService.getOne(new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getMobile,loginDTO.getAccount()).eq(UserEntity::getIsDelete, CommonConstant.DEL_FLAG_0));
             if (Objects.isNull(userInfo)) {
-                return Result.error("账号不存在");
+                return loginFail(account, null, "账号不存在");
             }
 
             BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
@@ -148,14 +153,14 @@ public class LoginApiController {
                 try {
                     password = RsaUtils.decryptByPrivateKey(RsaUtils.PRIVATE_KEY, loginDTO.getPassword());
                 }catch (Exception e) {
-                    return Result.error("用户名或密码错误");
+                    return loginFail(account, userInfo, "用户名或密码错误");
                 }
             }
             if(environment.equals("prod") || (environment.equals("test")
                     && !loginDTO.getPassword().equals("qq123456") &&
                     !password.equals("qq123456"))) {
                 if (!bCryptPasswordEncoder.matches(password, userInfo.getPassword())) {
-                    return Result.error("用户名或密码错误");
+                    return loginFail(account, userInfo, "用户名或密码错误");
                 }
             }
             String regex = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z\\W]{8,}$";
@@ -170,16 +175,16 @@ public class LoginApiController {
         }
 
         if (Objects.isNull(userInfo)) {
-            return Result.error("登录失败,未获取到用户信息");
+            return loginFail(account, null, "登录失败,未获取到用户信息");
         }
 
         if (!userInfo.getEnabled()) {
-            return Result.error("该账号用户暂无登录权限");
+            return loginFail(account, userInfo, "该账号用户暂无登录权限");
         }
 
         Integer possessMenuCount = menuService.checkUserMenuAccess(userInfo.getId());
         if (possessMenuCount == 0) {
-            return Result.error("暂未获取到菜单权限,请联系管理员分配权限!");
+            return loginFail(account, userInfo, "暂未获取到菜单权限,请联系管理员分配权限!");
         }
 
         LoginUser loginUser = new LoginUser();
@@ -212,6 +217,10 @@ public class LoginApiController {
         redisService.setCacheObject(RedisKeySaltConstant.API_CODE_REDIS + loginUser.getMobile(), apiMap);
 
         obj.put("token", token);
+        loginLogService.recordLoginSuccess(userInfo.getId(),
+                StrUtil.blankToDefault(account, userInfo.getMobile()),
+                userInfo.getUserName(),
+                userInfo.getRealName());
         return Result.result(obj);
     }
 
@@ -317,17 +326,27 @@ public class LoginApiController {
     public Result<Object> logout(HttpServletRequest request) {
         //用户退出逻辑
         String token = request.getHeader(DefContants.ACCESS_TOKEN);
+        LoginUser loginUser = LoginUserUtil.getLoginUser();
+        Long userId = loginUser != null ? loginUser.getId() : null;
+        String account = loginUser != null ? loginUser.getMobile() : null;
+        String userName = loginUser != null ? loginUser.getUserName() : null;
+        String realName = loginUser != null ? loginUser.getRealName() : null;
+
         if(ConvertUtils.isEmpty(token)) {
+            loginLogService.recordLogoutFail(userId, account, userName, realName, "退出登录失败!");
             return Result.error("退出登录失败!");
         }
         Boolean del = redisService.deleteObject(token);
         if (del) {
+            loginLogService.recordLogoutSuccess(userId, account, userName, realName);
             return Result.result("退出登录成功!");
         } else {
             Object obj = redisService.getCacheObject(token);
             if (obj == null) {
+                loginLogService.recordLogoutSuccess(userId, account, userName, realName);
                 return Result.error("退出登录成功!");
             }
+            loginLogService.recordLogoutFail(userId, account, userName, realName, "退出登录失败!");
             return Result.error("退出登录失败!");
         }
     }
@@ -585,4 +604,12 @@ public class LoginApiController {
 //            log.error("InvalidKeyException -- error--",e);
 //        }
 //    }
+
+    private Result loginFail(String account, UserEntity user, String message) {
+        Long userId = user != null ? user.getId() : null;
+        String userName = user != null ? user.getUserName() : null;
+        String realName = user != null ? user.getRealName() : null;
+        loginLogService.recordLoginFail(account, userId, userName, realName, message);
+        return Result.error(message);
+    }
 }

+ 41 - 0
java/storlead-api/src/main/java/com/storlead/system/controller/SysLoginLogApiController.java

@@ -0,0 +1,41 @@
+package com.storlead.system.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.storlead.framework.common.result.Result;
+import com.storlead.system.pojo.dto.SysLoginLogQueryDTO;
+import com.storlead.system.pojo.entity.SysLoginLogEntity;
+import com.storlead.system.service.SysLoginLogService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * 系统登录日志查询
+ */
+@Slf4j
+@RestController
+@RequestMapping("/sys/login/log")
+@Api(tags = "平台中心 - 系统: 登录日志")
+public class SysLoginLogApiController {
+
+    @Resource
+    private SysLoginLogService loginLogService;
+
+    @PostMapping("/pageList")
+    @ApiOperation(value = "登录日志分页查询", notes = "支持按账号/用户名/事件类型/成功失败/IP/城市/时间筛选")
+    @ApiResponses({
+            @ApiResponse(code = 200, message = "", response = SysLoginLogEntity.class)
+    })
+    public Result<?> pageList(@RequestBody SysLoginLogQueryDTO dto) {
+        IPage<SysLoginLogEntity> page = loginLogService.pageList(dto);
+        return Result.result(page);
+    }
+}

+ 2 - 1
java/storlead-dependencies/pom.xml

@@ -6,7 +6,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.storlead.boot</groupId>
     <artifactId>storlead-dependencies</artifactId>
-    <version>${revision}</version>
+    <version>${revision}</version>0
 
     <packaging>pom</packaging>
     <name>${project.artifactId}</name>
@@ -19,6 +19,7 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <spring.boot.starter.parent>2.7.0</spring.boot.starter.parent>
         <spring.boot.version>2.7.0</spring.boot.version>
+        .
         <knife4j.version>3.0.3</knife4j.version>
         <spring-web.version>5.3.32</spring-web.version>
         <jsoup.version>1.18.3</jsoup.version>

+ 92 - 0
java/storlead-message/sql/arg_template/_gen_arg_templates.py

@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+"""Generate arg_template JSON samples + consolidated seed SQL."""
+import json
+import re
+from pathlib import Path
+
+base = Path(r"java/storlead-sasa/storlead-sales/src/main/java")
+out = Path(r"java/storlead-message/sql/arg_template")
+out.mkdir(parents=True, exist_ok=True)
+
+vos = [
+    ("customer_message_arg_template.json", [10, 11], "CustomerMessageVO", "客户/线索",
+     base / "com/storlead/sales/pojo/vo/customer/CustomerMessageVO.java"),
+    ("liaison_message_arg_template.json", 20, "LiaisonMessageVO", "联系人",
+     base / "com/storlead/sales/pojo/vo/customer/LiaisonMessageVO.java"),
+    ("business_message_arg_template.json", 30, "BusinessMessageVO", "商机",
+     base / "com/storlead/sales/pojo/vo/customer/BusinessMessageVO.java"),
+    ("order_message_arg_template.json", 40, "OrderMessageVO", "订单",
+     base / "com/storlead/sales/pojo/vo/customer/OrderMessageVO.java"),
+    ("task_message_arg_template.json", 80, "TaskMessageVO", "任务",
+     base / "com/storlead/sales/pojo/vo/customer/TaskMessageVO.java"),
+    ("follow_message_arg_template.json", 90, "FollowMessageVO", "跟进",
+     base / "com/storlead/sales/pojo/vo/customer/FollowMessageVO.java"),
+    ("bulletin_message_arg_template.json", 120, "BulletinMessageVO", "公告",
+     base / "com/storlead/sales/pojo/vo/customer/BulletinMessageVO.java"),
+    ("performance_goal_message_arg_template.json", 130, "PerformanceGoalMessgeVO", "业绩目标",
+     base / "com/storlead/sales/pojo/vo/customer/PerformanceGoalMessgeVO.java"),
+    ("broadcast_total_message_arg_template.json", 200, "BroadcastTotalMessageVO", "播报汇总",
+     base / "com/storlead/sales/pojo/vo/report/BroadcastTotalMessageVO.java"),
+    ("other_extend_message_arg_template.json", 999, "OtherExtendMessageVO", "其他扩展",
+     base / "com/storlead/sales/pojo/vo/customer/OtherExtendMessageVO.java"),
+]
+
+field_pat = re.compile(
+    r'@ApiModelProperty\s*\(\s*value\s*=\s*"([^"]*)"\s*\)\s*(?:@[^\n]+\s*)*private\s+[\w.<>,\s?]+\s+(\w+)\s*;',
+    re.M,
+)
+
+all_sql = [
+    "-- =============================================================================",
+    "-- message_template_event_group.arg_template_json 回填脚本(汇总)",
+    "-- =============================================================================",
+    "-- 说明:",
+    "--   1. 由各 MessageVO 中带 @ApiModelProperty 的本类字段生成",
+    "--   2. JSON 格式:[{propertyName, propertyKey}],propertyKey 形如 ${fieldName}$",
+    "--   3. 仅更新 arg_template_json 为空的记录;需要按应用隔离时取消注释 AND app_id = 20",
+    "--   4. 可读 JSON 样例见同目录 *.json;VO 变更后可运行本脚本重新生成",
+    "--",
+    "-- 类型对照(SalesMessageTypeEnum):",
+    "--   10/11  CustomerMessageVO          客户/线索",
+    "--   20     LiaisonMessageVO           联系人",
+    "--   30     BusinessMessageVO          商机",
+    "--   40     OrderMessageVO             订单",
+    "--   80     TaskMessageVO              任务",
+    "--   90     FollowMessageVO            跟进",
+    "--   120    BulletinMessageVO          公告",
+    "--   130    PerformanceGoalMessgeVO    业绩目标",
+    "--   200    BroadcastTotalMessageVO    播报汇总",
+    "--   999    OtherExtendMessageVO       其他扩展",
+    "-- =============================================================================",
+    "",
+]
+
+for fname, types, cls, label, path in vos:
+    text = path.read_text(encoding="utf-8")
+    items = []
+    for m in field_pat.finditer(text):
+        items.append({"propertyName": m.group(1), "propertyKey": "${%s}$" % m.group(2)})
+    (out / fname).write_text(json.dumps(items, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
+    print("%s: %d fields -> %s" % (cls, len(items), fname))
+
+    json_one_line = json.dumps(items, ensure_ascii=False, separators=(",", ":"))
+    json_sql = json_one_line.replace("'", "''")
+    if isinstance(types, list):
+        type_cond = "`template_service_type` IN (%s)" % ", ".join(str(t) for t in types)
+        type_desc = "/".join(str(t) for t in types)
+    else:
+        type_cond = "`template_service_type` = %s" % types
+        type_desc = str(types)
+
+    all_sql.append("-- -----------------------------------------------------------------------------")
+    all_sql.append("-- type=%s  %s  %s(%d 个变量)" % (type_desc, cls, label, len(items)))
+    all_sql.append("-- -----------------------------------------------------------------------------")
+    all_sql.append("UPDATE `message_template_event_group`")
+    all_sql.append("SET `arg_template_json` = '%s'" % json_sql)
+    all_sql.append("WHERE %s" % type_cond)
+    all_sql.append("  AND (`arg_template_json` IS NULL OR `arg_template_json` = '');")
+    all_sql.append("-- AND `app_id` = 20;")
+    all_sql.append("")
+
+(out / "arg_template_json_seed.sql").write_text("\n".join(all_sql), encoding="utf-8")
+print("wrote arg_template_json_seed.sql")

Разлика између датотеке није приказан због своје велике величине
+ 26 - 0
java/storlead-message/sql/arg_template/arg_template_json_seed.sql


+ 66 - 0
java/storlead-message/sql/arg_template/broadcast_total_message_arg_template.json

@@ -0,0 +1,66 @@
+[
+  {
+    "propertyName": "周期类型名称:周/月/季度/半年度/年度",
+    "propertyKey": "${type}$"
+  },
+  {
+    "propertyName": "分公司名称",
+    "propertyKey": "${companyName}$"
+  },
+  {
+    "propertyName": "部门名称",
+    "propertyKey": "${deptName}$"
+  },
+  {
+    "propertyName": "客户记录",
+    "propertyKey": "${broadcastCustomerDates}$"
+  },
+  {
+    "propertyName": "商机记录",
+    "propertyKey": "${broadcastBusinessDates}$"
+  },
+  {
+    "propertyName": "订单数量记录",
+    "propertyKey": "${broadcastOrderDates}$"
+  },
+  {
+    "propertyName": "订单金额记录",
+    "propertyKey": "${broadcastOrderAmountDates}$"
+  },
+  {
+    "propertyName": "客户总额",
+    "propertyKey": "${totalCustomerNum}$"
+  },
+  {
+    "propertyName": "订单金额总额",
+    "propertyKey": "${totalOrderAmount}$"
+  },
+  {
+    "propertyName": "商机总额",
+    "propertyKey": "${totalBusinessNum}$"
+  },
+  {
+    "propertyName": "订单数量总额",
+    "propertyKey": "${totalOrderNum}$"
+  },
+  {
+    "propertyName": "线索总额",
+    "propertyKey": "${totalClueNum}$"
+  },
+  {
+    "propertyName": "跟进总额",
+    "propertyKey": "${totalFollowNum}$"
+  },
+  {
+    "propertyName": "邮件总额",
+    "propertyKey": "${totalEmailNum}$"
+  },
+  {
+    "propertyName": "记录产生时间(yyyy年MM月dd日)",
+    "propertyKey": "${recordTime}$"
+  },
+  {
+    "propertyName": "记录范围时间(yyyyMMdd-yyyyMMdd)",
+    "propertyKey": "${recordRangeTime}$"
+  }
+]

+ 30 - 0
java/storlead-message/sql/arg_template/bulletin_message_arg_template.json

@@ -0,0 +1,30 @@
+[
+  {
+    "propertyName": "消息ID",
+    "propertyKey": "${id}$"
+  },
+  {
+    "propertyName": "操作人",
+    "propertyKey": "${operateByName}$"
+  },
+  {
+    "propertyName": "用户名",
+    "propertyKey": "${publisher}$"
+  },
+  {
+    "propertyName": "业务类型关联ID",
+    "propertyKey": "${resourceTypeId}$"
+  },
+  {
+    "propertyName": "业务名称",
+    "propertyKey": "${resourceName}$"
+  },
+  {
+    "propertyName": "业务类型名称",
+    "propertyKey": "${resourceTypeName}$"
+  },
+  {
+    "propertyName": "标题",
+    "propertyKey": "${title}$"
+  }
+]

+ 106 - 0
java/storlead-message/sql/arg_template/business_message_arg_template.json

@@ -0,0 +1,106 @@
+[
+  {
+    "propertyName": "商机标题",
+    "propertyKey": "${title}$"
+  },
+  {
+    "propertyName": "创建人员",
+    "propertyKey": "${createByName}$"
+  },
+  {
+    "propertyName": "归属人员",
+    "propertyKey": "${ownerByName}$"
+  },
+  {
+    "propertyName": "关联客户",
+    "propertyKey": "${customerName}$"
+  },
+  {
+    "propertyName": "主联系人",
+    "propertyKey": "${liaisonName}$"
+  },
+  {
+    "propertyName": "预计销售金额",
+    "propertyKey": "${estimatedSalesAmount}$"
+  },
+  {
+    "propertyName": "预计签单日期",
+    "propertyKey": "${expectedSigningDate}$"
+  },
+  {
+    "propertyName": "销售阶段",
+    "propertyKey": "${salesPhaseName}$"
+  },
+  {
+    "propertyName": "商机类型",
+    "propertyKey": "${businessTypeName}$"
+  },
+  {
+    "propertyName": "成交几率",
+    "propertyKey": "${stransactionProbabilityName}$"
+  },
+  {
+    "propertyName": "商机来源",
+    "propertyKey": "${sourcesOfBusinessName}$"
+  },
+  {
+    "propertyName": "获取日期",
+    "propertyKey": "${getedDate}$"
+  },
+  {
+    "propertyName": "操作人",
+    "propertyKey": "${operateByName}$"
+  },
+  {
+    "propertyName": "操作时间",
+    "propertyKey": "${operateTime}$"
+  },
+  {
+    "propertyName": "跟进时间",
+    "propertyKey": "${followUpTime}$"
+  },
+  {
+    "propertyName": "跟进内容",
+    "propertyKey": "${followUpContent}$"
+  },
+  {
+    "propertyName": "紧要程度",
+    "propertyKey": "${urgentLevelName}$"
+  },
+  {
+    "propertyName": "变更时间",
+    "propertyKey": "${changeTime}$"
+  },
+  {
+    "propertyName": "变更字段",
+    "propertyKey": "${changeField}$"
+  },
+  {
+    "propertyName": "变更字段值",
+    "propertyKey": "${changeFieldValue}$"
+  },
+  {
+    "propertyName": "变更字段原值",
+    "propertyKey": "${changeFieldOriginalValue}$"
+  },
+  {
+    "propertyName": "评论时间",
+    "propertyKey": "${commentTime}$"
+  },
+  {
+    "propertyName": "评论内容",
+    "propertyKey": "${commentContent}$"
+  },
+  {
+    "propertyName": "删除时间",
+    "propertyKey": "${deleteTime}$"
+  },
+  {
+    "propertyName": "删除人",
+    "propertyKey": "${deleteByName}$"
+  },
+  {
+    "propertyName": "商机状态",
+    "propertyKey": "${businessStatusName}$"
+  }
+]

+ 130 - 0
java/storlead-message/sql/arg_template/customer_message_arg_template.json

@@ -0,0 +1,130 @@
+[
+  {
+    "propertyName": "客户名称",
+    "propertyKey": "${customerName}$"
+  },
+  {
+    "propertyName": "区域名称",
+    "propertyKey": "${areaName}$"
+  },
+  {
+    "propertyName": "归属人员",
+    "propertyKey": "${ownerByName}$"
+  },
+  {
+    "propertyName": "创建人员",
+    "propertyKey": "${createByName}$"
+  },
+  {
+    "propertyName": "客户状态",
+    "propertyKey": "${customerStatusName}$"
+  },
+  {
+    "propertyName": "客户来源",
+    "propertyKey": "${customerSourceName}$"
+  },
+  {
+    "propertyName": "客户类型",
+    "propertyKey": "${customerTypeName}$"
+  },
+  {
+    "propertyName": "首联系人姓名",
+    "propertyKey": "${liaisonName}$"
+  },
+  {
+    "propertyName": "客户等级",
+    "propertyKey": "${customerLevelName}$"
+  },
+  {
+    "propertyName": "信用等级",
+    "propertyKey": "${customerCreditLevelName}$"
+  },
+  {
+    "propertyName": "企业名称",
+    "propertyKey": "${companyName}$"
+  },
+  {
+    "propertyName": "企业网址",
+    "propertyKey": "${companyUrl}$"
+  },
+  {
+    "propertyName": "新增类型",
+    "propertyKey": "${addType}$"
+  },
+  {
+    "propertyName": "移入人员",
+    "propertyKey": "${moveByName}$"
+  },
+  {
+    "propertyName": "移入时间",
+    "propertyKey": "${moveTime}$"
+  },
+  {
+    "propertyName": "转移人",
+    "propertyKey": "${transferByName}$"
+  },
+  {
+    "propertyName": "转移类型",
+    "propertyKey": "${transferType}$"
+  },
+  {
+    "propertyName": "删除人员",
+    "propertyKey": "${deleteByName}$"
+  },
+  {
+    "propertyName": "删除时间",
+    "propertyKey": "${deleteTime}$"
+  },
+  {
+    "propertyName": "置垃圾时间",
+    "propertyKey": "${setPublicTime}$"
+  },
+  {
+    "propertyName": "操作人",
+    "propertyKey": "${operateByName}$"
+  },
+  {
+    "propertyName": "合并时间",
+    "propertyKey": "${mergeTime}$"
+  },
+  {
+    "propertyName": "邮件标题",
+    "propertyKey": "${emailTitle}$"
+  },
+  {
+    "propertyName": "接收时间",
+    "propertyKey": "${receiveTime}$"
+  },
+  {
+    "propertyName": "邮箱",
+    "propertyKey": "${email}$"
+  },
+  {
+    "propertyName": "跟进时间",
+    "propertyKey": "${followUpTime}$"
+  },
+  {
+    "propertyName": "跟进内容",
+    "propertyKey": "${followUpContent}$"
+  },
+  {
+    "propertyName": "紧要程度",
+    "propertyKey": "${urgentLevel}$"
+  },
+  {
+    "propertyName": "逾期时长",
+    "propertyKey": "${overdueDuration}$"
+  },
+  {
+    "propertyName": "评论时间",
+    "propertyKey": "${commentTime}$"
+  },
+  {
+    "propertyName": "评论内容",
+    "propertyKey": "${commentContent}$"
+  },
+  {
+    "propertyName": "消息内容",
+    "propertyKey": "${messageContent}$"
+  }
+]

+ 26 - 0
java/storlead-message/sql/arg_template/follow_message_arg_template.json

@@ -0,0 +1,26 @@
+[
+  {
+    "propertyName": "消息ID",
+    "propertyKey": "${id}$"
+  },
+  {
+    "propertyName": "操作人",
+    "propertyKey": "${operateByName}$"
+  },
+  {
+    "propertyName": "业务类型关联ID",
+    "propertyKey": "${resourceTypeId}$"
+  },
+  {
+    "propertyName": "业务名称",
+    "propertyKey": "${resourceName}$"
+  },
+  {
+    "propertyName": "业务类型名称",
+    "propertyKey": "${resourceTypeName}$"
+  },
+  {
+    "propertyName": "客户名称",
+    "propertyKey": "${customerName}$"
+  }
+]

+ 126 - 0
java/storlead-message/sql/arg_template/liaison_message_arg_template.json

@@ -0,0 +1,126 @@
+[
+  {
+    "propertyName": "联系人id",
+    "propertyKey": "${id}$"
+  },
+  {
+    "propertyName": "关联客户id",
+    "propertyKey": "${customerId}$"
+  },
+  {
+    "propertyName": "是否是首要联系人(0不是,1是)",
+    "propertyKey": "${hasPrimaryLialison}$"
+  },
+  {
+    "propertyName": "联系人姓名",
+    "propertyKey": "${name}$"
+  },
+  {
+    "propertyName": "尊称(0未知,1先生,2女士)",
+    "propertyKey": "${call}$"
+  },
+  {
+    "propertyName": "尊称(0未知,1先生,2女士)",
+    "propertyKey": "${callName}$"
+  },
+  {
+    "propertyName": "联系人角色value(对应字典联系人角色value)",
+    "propertyKey": "${liaisonRoleDictValue}$"
+  },
+  {
+    "propertyName": "生日",
+    "propertyKey": "${birthday}$"
+  },
+  {
+    "propertyName": "部门职务",
+    "propertyKey": "${departmentPosition}$"
+  },
+  {
+    "propertyName": "手机号码",
+    "propertyKey": "${telephone}$"
+  },
+  {
+    "propertyName": "电子邮箱",
+    "propertyKey": "${email}$"
+  },
+  {
+    "propertyName": "资源类型",
+    "propertyKey": "${resourceType}$"
+  },
+  {
+    "propertyName": "资源类型关联ID",
+    "propertyKey": "${resourceTypeId}$"
+  },
+  {
+    "propertyName": "传真号码",
+    "propertyKey": "${faxNumber}$"
+  },
+  {
+    "propertyName": "邮政编码",
+    "propertyKey": "${postcode}$"
+  },
+  {
+    "propertyName": "联系方式",
+    "propertyKey": "${contactInformation}$"
+  },
+  {
+    "propertyName": "备注信息",
+    "propertyKey": "${remark}$"
+  },
+  {
+    "propertyName": "qq",
+    "propertyKey": "${qq}$"
+  },
+  {
+    "propertyName": "skype",
+    "propertyKey": "${skype}$"
+  },
+  {
+    "propertyName": "旺旺",
+    "propertyKey": "${wangwang}$"
+  },
+  {
+    "propertyName": "微信",
+    "propertyKey": "${wechat}$"
+  },
+  {
+    "propertyName": "Twitter",
+    "propertyKey": "${twitter}$"
+  },
+  {
+    "propertyName": "Linkedin",
+    "propertyKey": "${linkedin}$"
+  },
+  {
+    "propertyName": "Facebook",
+    "propertyKey": "${facebook}$"
+  },
+  {
+    "propertyName": "Whatsapp",
+    "propertyKey": "${whatsapp}$"
+  },
+  {
+    "propertyName": "区域",
+    "propertyKey": "${internationalArea}$"
+  },
+  {
+    "propertyName": "详细地址",
+    "propertyKey": "${address}$"
+  },
+  {
+    "propertyName": "固定电话",
+    "propertyKey": "${fixedLineTelephone}$"
+  },
+  {
+    "propertyName": "宅电",
+    "propertyKey": "${homeElectricity}$"
+  },
+  {
+    "propertyName": "归属人姓名",
+    "propertyKey": "${ownerByName}$"
+  },
+  {
+    "propertyName": "操作人",
+    "propertyKey": "${operateByName}$"
+  }
+]

+ 114 - 0
java/storlead-message/sql/arg_template/order_message_arg_template.json

@@ -0,0 +1,114 @@
+[
+  {
+    "propertyName": "订单标题",
+    "propertyKey": "${title}$"
+  },
+  {
+    "propertyName": "关联客户",
+    "propertyKey": "${customerName}$"
+  },
+  {
+    "propertyName": "关联商机",
+    "propertyKey": "${businessName}$"
+  },
+  {
+    "propertyName": "关联报价",
+    "propertyKey": "${quoteName}$"
+  },
+  {
+    "propertyName": "订单总金额",
+    "propertyKey": "${totalOrderAmt}$"
+  },
+  {
+    "propertyName": "付款币种",
+    "propertyKey": "${currencyName}$"
+  },
+  {
+    "propertyName": "订单类型",
+    "propertyKey": "${orderTypeName}$"
+  },
+  {
+    "propertyName": "签单日期",
+    "propertyKey": "${signedDate}$"
+  },
+  {
+    "propertyName": "预计生产日期",
+    "propertyKey": "${expectedProductionDate}$"
+  },
+  {
+    "propertyName": "预计发货日期",
+    "propertyKey": "${expectedDeliveryDate}$"
+  },
+  {
+    "propertyName": "归属人员",
+    "propertyKey": "${ownerByName}$"
+  },
+  {
+    "propertyName": "创建人员",
+    "propertyKey": "${createByName}$"
+  },
+  {
+    "propertyName": "删除时间",
+    "propertyKey": "${deleteTime}$"
+  },
+  {
+    "propertyName": "删除人员",
+    "propertyKey": "${deleteByName}$"
+  },
+  {
+    "propertyName": "变更时间",
+    "propertyKey": "${changeTime}$"
+  },
+  {
+    "propertyName": "变更字段",
+    "propertyKey": "${changeField}$"
+  },
+  {
+    "propertyName": "变更字段值",
+    "propertyKey": "${changeFieldValue}$"
+  },
+  {
+    "propertyName": "变更字段原值",
+    "propertyKey": "${changeFieldOriginalValue}$"
+  },
+  {
+    "propertyName": "操作时间",
+    "propertyKey": "${operateTime}$"
+  },
+  {
+    "propertyName": "操作人",
+    "propertyKey": "${operateByName}$"
+  },
+  {
+    "propertyName": "评论时间",
+    "propertyKey": "${commentTime}$"
+  },
+  {
+    "propertyName": "评论内容",
+    "propertyKey": "${commentContent}$"
+  },
+  {
+    "propertyName": "订单状态",
+    "propertyKey": "${orderStatusName}$"
+  },
+  {
+    "propertyName": "转移时间",
+    "propertyKey": "${transferTime}$"
+  },
+  {
+    "propertyName": "转移类型",
+    "propertyKey": "${transferType}$"
+  },
+  {
+    "propertyName": "转移人",
+    "propertyKey": "${transferByName}$"
+  },
+  {
+    "propertyName": "紧要程度",
+    "propertyKey": "${urgentLevelName}$"
+  },
+  {
+    "propertyName": "逾期时长",
+    "propertyKey": "${overdueDuration}$"
+  }
+]

+ 10 - 0
java/storlead-message/sql/arg_template/other_extend_message_arg_template.json

@@ -0,0 +1,10 @@
+[
+  {
+    "propertyName": "消息ID",
+    "propertyKey": "${id}$"
+  },
+  {
+    "propertyName": "操作人",
+    "propertyKey": "${operateByName}$"
+  }
+]

+ 30 - 0
java/storlead-message/sql/arg_template/performance_goal_message_arg_template.json

@@ -0,0 +1,30 @@
+[
+  {
+    "propertyName": "所属年份",
+    "propertyKey": "${year}$"
+  },
+  {
+    "propertyName": "方式",
+    "propertyKey": "${method}$"
+  },
+  {
+    "propertyName": "方式(实际使用)",
+    "propertyKey": "${methodName}$"
+  },
+  {
+    "propertyName": "指标类型",
+    "propertyKey": "${type}$"
+  },
+  {
+    "propertyName": "指标类型(实际使用)",
+    "propertyKey": "${typeName}$"
+  },
+  {
+    "propertyName": "跳转的页面",
+    "propertyKey": "${jump}$"
+  },
+  {
+    "propertyName": "操作人",
+    "propertyKey": "${operateByName}$"
+  }
+]

+ 10 - 0
java/storlead-message/sql/arg_template/task_message_arg_template.json

@@ -0,0 +1,10 @@
+[
+  {
+    "propertyName": "任务开始时间",
+    "propertyKey": "${taskBeginTime}$"
+  },
+  {
+    "propertyName": "任务内容",
+    "propertyKey": "${taskContent}$"
+  }
+]

+ 48 - 6
java/storlead-message/storlead-message-api/src/main/java/com/storlead/message/controller/MessageApiController.java

@@ -13,8 +13,9 @@ import com.storlead.message.pojo.vo.MessageAppNoReadVO;
 import com.storlead.message.pojo.vo.MessageDetailVO;
 import com.storlead.message.pojo.vo.MessageNoReadTotalVO;
 import com.storlead.message.pojo.vo.MessageTypeReadStateVO;
-import com.storlead.message.service.InsideMessageRecordService;
+import cn.hutool.core.util.StrUtil;
 import com.storlead.message.service.InsideMessageSendLogService;
+import com.storlead.message.service.impl.MessageService;
 import io.swagger.annotations.*;
 import org.springframework.util.CollectionUtils;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -24,9 +25,12 @@ import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 import org.springframework.beans.factory.annotation.Autowired;
 
@@ -37,23 +41,61 @@ import org.springframework.beans.factory.annotation.Autowired;
  * @create: 2024-04-19 16:36
  */
 @RestController
-@RequestMapping("/sys/inside/message")
+@RequestMapping("/sys/message")
 @Api(tags = "消息: 站内消息相关接口")
 public class MessageApiController {
 
+    private static final String[] DEFAULT_TEST_CHANNELS = {"site", "wecom"};
+
     @Resource
     private InsideMessageSendLogService messageSendLogService;
 
     @Resource
-    private InsideMessageRecordService messageRecordService;
+    private MessageService messageService;
 
     @Autowired(required = false)
     private List<MessageTypeCatalog> messageTypeCatalogs;
 
     @PostMapping(value = "/testSend")
-    @ApiOperation(value = "发送站内消息(ApiPost:POST + application/json,Header 带 token)")
+    @ApiOperation(value = "消息发送测试(匹配模板后发站内+企业微信;ApiPost:POST + application/json)")
     public Result<?> sendMessage(@RequestBody MessageSendDTO dto) {
-        return messageRecordService.sendDirectMessage(dto);
+        if (dto == null || dto.getMessageType() == null || StrUtil.isBlank(dto.getEventCode())) {
+            return Result.error("参数错误:messageType、eventCode 必填");
+        }
+        Set<Long> toUserIds = resolveTestToUserIds(dto);
+        if (CollectionUtils.isEmpty(toUserIds)) {
+            return Result.error("参数错误:receiverUserId / toUserIds 至少传一个");
+        }
+        Map<String, Object> messageMap = dto.getMessageMap() == null
+                ? new HashMap<>()
+                : new HashMap<>(dto.getMessageMap());
+        String[] channels = (dto.getChannels() == null || dto.getChannels().length == 0)
+                ? DEFAULT_TEST_CHANNELS
+                : dto.getChannels();
+        messageService.autoMatchEventSendMessage(
+                messageMap,
+                dto.getMessageType().toString(),
+                dto.getEventCode(),
+                dto.getAppId(),
+                toUserIds,
+                channels
+        );
+        return Result.ok();
+    }
+
+    private Set<Long> resolveTestToUserIds(MessageSendDTO dto) {
+        Set<Long> toUserIds = new HashSet<>();
+        if (dto.getToUserIds() != null) {
+            for (Long userId : dto.getToUserIds()) {
+                if (userId != null) {
+                    toUserIds.add(userId);
+                }
+            }
+        }
+        if (toUserIds.isEmpty() && dto.getReceiverUserId() != null) {
+            toUserIds.add(dto.getReceiverUserId());
+        }
+        return toUserIds;
     }
 
     @PostMapping(value = "/pageList")
@@ -70,7 +112,7 @@ public class MessageApiController {
     }
 
     @PostMapping(value = "/read")
-    @ApiOperation(value = "更新为已读" )
+    @ApiOperation(value = "更新为已读")
     public Result<?> updateStatus(Long [] messageLogIds) {
         if (Objects.isNull(messageLogIds) || messageLogIds.length == 0) {
             return Result.error("参数错误");

+ 66 - 0
java/storlead-message/storlead-message-api/src/main/java/com/storlead/message/controller/MessageTemplateApiController.java

@@ -17,6 +17,7 @@ import com.storlead.message.service.MessageTemplateEventGroupService;
 import com.storlead.message.pojo.dto.MessageTemplateEventDTO;
 import com.storlead.message.util.MessageArgTemplateUtil;
 import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiModelProperty;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.BeanUtils;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -25,6 +26,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -116,6 +119,69 @@ public class MessageTemplateApiController {
         templateVO.setArgls(argTemplatels);
         return Result.result(templateVO);
     }
+
+//    @ApiOperation("获取消息模板详情")
+//    @PostMapping("getGroupDetailOld")
+//    public Result getGroupDetailOld(Long id,String templateDetailType) {
+//        MessageTemplateEventGroupEntity templateEventGroup =  templateEventGroupService.getById(id);
+//        if (Objects.isNull(templateEventGroup)) {
+//            return Result.ok();
+//        }
+//        MessageTemplateVO templateVO = new MessageTemplateVO();
+//        BeanUtils.copyProperties(templateEventGroup,templateVO);
+//        LambdaQueryWrapper<MessageTemplateEventDetailEntity> wrapper = new LambdaQueryWrapper<>();
+//        wrapper.eq(MessageTemplateEventDetailEntity::getTemplateEventId,templateEventGroup.getId());
+//        if (Objects.nonNull(templateDetailType)) {
+//            wrapper.eq(MessageTemplateEventDetailEntity::getTemplateDetailType,templateDetailType);
+//        }
+//        List<MessageTemplateEventDetailEntity>  detailEntities = templateEventDetailService.list(wrapper);
+//        templateVO.setDetailEntities(detailEntities);
+//        templateVO.dictFiled();
+//
+//        List<MessageArgTemplateVO> argTemplatels = new ArrayList<>();
+//        Class clazz = null;
+//        if (SalesMessageTypeEnum.CUSTOMER.getCode().equals(templateEventGroup.getTemplateServiceType().toString())) {
+//            clazz = CustomerMessageVO.class;
+//        } else if (MessageTypeEnum.CLUS.getCode().equals(templateEventGroup.getTemplateServiceType().toString())) {
+//            clazz = CustomerMessageVO.class;
+//        } else if (MessageTypeEnum.ORDER_FORM.getCode().equals(templateEventGroup.getTemplateServiceType().toString())) {
+//            clazz = OrderMessageVO.class;
+//        } else if (MessageTypeEnum.BUSINESS.getCode().equals(templateEventGroup.getTemplateServiceType().toString())) {
+//            clazz = BusinessMessageVO.class;
+//        } else if (MessageTypeEnum.TASK_CANCELED.getCode().equals(templateEventGroup.getTemplateServiceType().toString())) {
+//            clazz = TaskMessageVO.class;
+//        } else if (MessageTypeEnum.FOLLOW_UP.getCode().equals(templateEventGroup.getTemplateServiceType().toString())) {
+//            clazz = FollowMessageVO.class;
+//        } else if (MessageTypeEnum.LIAISON.getCode().equals(templateEventGroup.getTemplateServiceType().toString())) {
+//            clazz = LiaisonMessageVO.class;
+//        } else if (MessageTypeEnum.PERFORMANCE_GOAL.getCode().equals(templateEventGroup.getTemplateServiceType().toString())) {
+//            clazz = PerformanceGoalMessgeVO.class;
+//        } else if (MessageTypeEnum.BULLETIN.getCode().equals(templateEventGroup.getTemplateServiceType().toString())) {
+//            clazz = BulletinMessageVO.class;
+//        } else if (MessageTypeEnum.OTHER_EXTEND.getCode().equals(templateEventGroup.getTemplateServiceType().toString())) {
+//            clazz = OtherExtendMessageVO.class;
+//        } else if (MessageTypeEnum.BROADCAST.getCode().equals(templateEventGroup.getTemplateServiceType().toString())){
+//            clazz = BroadcastTotalMessageVO.class;
+//        }
+//
+//        if (clazz != null) {
+//            for (Field field : clazz.getDeclaredFields()) {
+//                MessageArgTemplateVO vo = new MessageArgTemplateVO();
+//                vo.setPropertyKey("${" + field.getName() + "}$");
+//                // 检查字段是否有 @Column 注解
+//                if (field.isAnnotationPresent(ApiModelProperty.class)) {
+//                    // 获取注解实例
+//                    ApiModelProperty property = field.getAnnotation(ApiModelProperty.class);
+//                    vo.setPropertyName(property.value());
+//                    // 输出字段名和注解的值
+//                }
+//                argTemplatels.add(vo);
+//            }
+//        }
+//        templateVO.setArgls(argTemplatels);
+//        return Result.result(templateVO);
+//    }
+
     @ApiOperation("获取消息模板详情")
     @PostMapping("getDetail")
     public Result getDetail(Long id) {

+ 0 - 6
java/storlead-message/storlead-message-biz/src/main/java/com/storlead/message/service/InsideMessageRecordService.java

@@ -2,7 +2,6 @@ package com.storlead.message.service;
 
 import com.storlead.framework.common.result.Result;
 import com.storlead.framework.mybatis.service.MyBaseService;
-import com.storlead.message.pojo.dto.MessageSendDTO;
 import com.storlead.message.pojo.entity.InsideMessageRecordEntity;
 import com.storlead.message.pojo.entity.MessageTemplateEventDetailEntity;
 import com.storlead.message.pojo.entity.MessageTemplateEventGroupEntity;
@@ -27,9 +26,4 @@ public interface InsideMessageRecordService extends MyBaseService<InsideMessageR
     void asynSendInsideMessage(MessageTemplateEventGroupEntity eventGroup, MessageTemplateEventDetailEntity templateEventDetail, Map map, Long receiverUserId, String callbackParam, Long sendUserId);
 
     void asynSendInsideMessage(MessageTemplateEventGroupEntity eventGroup, MessageTemplateEventDetailEntity templateEventDetail, Map map, Collection<Long> receiverUserIds, String callbackParam, Long sendUserId);
-
-    /**
-     * 直接发送站内消息(不依赖消息模板),用于联调 / ApiPost 测试。
-     */
-    Result<Long> sendDirectMessage(MessageSendDTO dto);
 }

+ 0 - 64
java/storlead-message/storlead-message-biz/src/main/java/com/storlead/message/service/impl/InsideMessageRecordServiceImpl.java

@@ -6,7 +6,6 @@ import com.storlead.framework.common.enums.ErrorMsgCode;
 import com.storlead.framework.common.util.HtmlUtils;
 import com.storlead.framework.mybatis.service.impl.MyBaseServiceImpl;
 import com.storlead.framework.util.LoginUserUtil;
-import com.storlead.message.pojo.dto.MessageSendDTO;
 import com.storlead.message.pojo.entity.InsideMessageRecordEntity;
 import com.storlead.message.mapper.InsideMessageRecordMapper;
 import com.storlead.message.pojo.entity.InsideMessageSendLogEntity;
@@ -132,67 +131,4 @@ public class InsideMessageRecordServiceImpl extends MyBaseServiceImpl<InsideMess
     public void asynSendInsideMessage(MessageTemplateEventGroupEntity eventGroup,MessageTemplateEventDetailEntity templateEventDetail, Map map, Collection<Long> receiverUserIds, String callbackParam,Long sendUserId) {
         sendInsideMessage(eventGroup,templateEventDetail,map,receiverUserIds,callbackParam,sendUserId);
     }
-
-    @Override
-    public Result<Long> sendDirectMessage(MessageSendDTO dto) {
-        if (dto == null || StrUtil.isBlank(dto.getTitle())) {
-            return Result.result(null);
-        }
-        List<Long> receiverUserIds = resolveReceiverUserIds(dto);
-        if (CollectionUtils.isEmpty(receiverUserIds)) {
-            return Result.result(null);
-        }
-
-        Long sendUserId = dto.getSendUserId();
-        if (sendUserId == null) {
-            sendUserId = LoginUserUtil.getCurrentUserId();
-        }
-        if (sendUserId == null) {
-            sendUserId = 0L;
-        }
-
-        String content = StrUtil.blankToDefault(dto.getContent(), dto.getTitle());
-        Integer messageType = dto.getMessageType() == null ? 999 : dto.getMessageType();
-
-        InsideMessageRecordEntity entity = new InsideMessageRecordEntity(
-                dto.getTitle(),
-                content,
-                dto.getMessageIcon(),
-                dto.getMessageTag(),
-                dto.getCallbackUrl(),
-                sendUserId,
-                1,
-                messageType
-        );
-        entity.setAppId(dto.getAppId());
-        entity.setCreateBy(sendUserId);
-        entity.setUpdateBy(sendUserId);
-        saveOrUpdate(entity);
-
-        List<InsideMessageSendLogEntity> messageSendLogs = new ArrayList<>();
-        for (Long receiverUserId : receiverUserIds) {
-            InsideMessageSendLogEntity messageSendLog = new InsideMessageSendLogEntity(
-                    entity.getId(), receiverUserId, 0, 1, messageType);
-            messageSendLog.setCreateBy(sendUserId);
-            messageSendLog.setUpdateBy(sendUserId);
-            messageSendLogs.add(messageSendLog);
-        }
-        insideMessageSendLogService.saveOrUpdateBatch(messageSendLogs);
-        return Result.result(entity.getId());
-    }
-
-    private List<Long> resolveReceiverUserIds(MessageSendDTO dto) {
-        List<Long> receiverUserIds = new ArrayList<>();
-        if (dto.getToUserIds() != null) {
-            for (Long userId : dto.getToUserIds()) {
-                if (userId != null) {
-                    receiverUserIds.add(userId);
-                }
-            }
-        }
-        if (receiverUserIds.isEmpty() && dto.getReceiverUserId() != null) {
-            receiverUserIds.add(dto.getReceiverUserId());
-        }
-        return receiverUserIds;
-    }
 }

+ 11 - 0
java/storlead-message/storlead-message-biz/src/main/java/com/storlead/message/service/impl/MessageService.java

@@ -68,10 +68,21 @@ public class MessageService {
     }
 
     public void autoMatchEventSendMessage(Map<String,Object> messageMap, String templateServiceType, String eventCode, Set<Long> toUserIds, String... args) {
+        autoMatchEventSendMessage(messageMap, templateServiceType, eventCode, null, toUserIds, args);
+    }
+
+    /**
+     * 按消息类型 + 事件码匹配模板并发送;可按 appId 限定应用,避免跨应用同类型 code 串模板。
+     */
+    public void autoMatchEventSendMessage(Map<String,Object> messageMap, String templateServiceType, String eventCode,
+                                          Long appId, Set<Long> toUserIds, String... args) {
         try {
             LambdaQueryWrapper<MessageTemplateEventGroupEntity> templateEventWrapper = new LambdaQueryWrapper<>();
             templateEventWrapper.eq(MessageTemplateEventGroupEntity::getTemplateServiceType,templateServiceType);
             templateEventWrapper.eq(MessageTemplateEventGroupEntity::getEventCode,eventCode);
+            if (Objects.nonNull(appId)) {
+                templateEventWrapper.eq(MessageTemplateEventGroupEntity::getAppId, appId);
+            }
             templateEventWrapper.last(" limit 1");
             MessageTemplateEventGroupEntity  eventGroup = eventGroupService.getOne(templateEventWrapper);
             if (Objects.isNull(eventGroup)) {

+ 17 - 24
java/storlead-message/storlead-message-core/src/main/java/com/storlead/message/pojo/dto/MessageSendDTO.java

@@ -5,41 +5,34 @@ import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 import lombok.experimental.Accessors;
 
+import java.util.Map;
+
 /**
- * 站内消息发送参数(ApiPost 可用 x-www-form-urlencoded 或 query 传参)
+ * 消息发送测试参数:按模板类型 + 事件码匹配后发送(站内 / 企业微信等)。
  */
 @Data
 @Accessors(chain = true)
-@ApiModel(value = "站内消息发送", description = "发送站内消息测试/业务参数")
+@ApiModel(value = "消息发送测试", description = "匹配 message_template_event_group 后发送,默认渠道 site+wecom")
 public class MessageSendDTO {
 
-    @ApiModelProperty(value = "消息标题", required = true, example = "测试消息标题")
-    private String title;
-
-    @ApiModelProperty(value = "消息内容", example = "这是一条 ApiPost 测试消息")
-    private String content;
-
-    @ApiModelProperty(value = "接收人用户 ID(单用户时可只传此字段)", example = "1")
-    private Long receiverUserId;
+    @ApiModelProperty(value = "消息业务类型(template_service_type)", required = true, example = "30")
+    private Integer messageType;
 
-    @ApiModelProperty(value = "接收人用户 ID 列表(多用户:toUserIds=1&toUserIds=2)")
-    private Long[] toUserIds;
+    @ApiModelProperty(value = "事件码(event_code)", required = true, example = "BUSINESS_CREATE")
+    private String eventCode;
 
-    @ApiModelProperty(value = "应用 ID,对应 inside_message_record.app_id", example = "10")
+    @ApiModelProperty(value = "应用 ID(限定模板,避免跨应用同类型串模板)", example = "20")
     private Long appId;
 
-    @ApiModelProperty(value = "消息类型,对应 inside_message_record.message_type", example = "30")
-    private Integer messageType;
-
-    @ApiModelProperty(value = "发送人用户 ID,不传则取当前登录用户")
-    private Long sendUserId;
+    @ApiModelProperty(value = "模板变量(对应 arg_template_json 中的 propertyKey,不含 ${}$)", example = "{\"title\":\"测试商机\",\"customerName\":\"测试客户\"}")
+    private Map<String, Object> messageMap;
 
-    @ApiModelProperty(value = "回调地址")
-    private String callbackUrl;
+    @ApiModelProperty(value = "接收人用户 ID(单用户时可只传此字段)", example = "1")
+    private Long receiverUserId;
 
-    @ApiModelProperty(value = "消息图标")
-    private String messageIcon;
+    @ApiModelProperty(value = "接收人用户 ID 列表")
+    private Long[] toUserIds;
 
-    @ApiModelProperty(value = "消息标签")
-    private String messageTag;
+    @ApiModelProperty(value = "发送渠道,默认 site、wecom;可选 site/wecom/sms/mail")
+    private String[] channels;
 }

+ 67 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/enums/TradeMessageTypeEnum.java

@@ -1,7 +1,74 @@
 package com.storlead.trade.enums;
 
+import com.storlead.message.pojo.vo.MessageTypeReadStateVO;
 import lombok.Getter;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 @Getter
 public enum  TradeMessageTypeEnum {
+
+    CUSTOMIZE_EXAMPLE("0", "业务自定义"),
+    OTHER_EXTEND("999", "其他"),
+    ;
+
+    /** 贸易应用 ID,对应 sys_app.id */
+    public static final Long APP_ID = 7L;
+
+    public static final TradeMessageTypeEnum[] messageSubTypes = TradeMessageTypeEnum.values();
+
+    private final String code;
+    private final String desc;
+
+    TradeMessageTypeEnum(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public static Map<String, Integer> getMessageTypeMap(List<String> types) {
+        Map<String, Integer> map = new HashMap<>();
+        for (TradeMessageTypeEnum subTypeEnum : messageSubTypes) {
+            if (CollectionUtils.isEmpty(types)) {
+                map.put(subTypeEnum.getCode(), 0);
+            } else if (types.contains(subTypeEnum.getCode())) {
+                map.put(subTypeEnum.getCode(), 0);
+            }
+        }
+        return map;
+    }
+
+    public static List<MessageTypeReadStateVO> getMessageTypeReadStatels(List<String> types) {
+        List<MessageTypeReadStateVO> arr = new ArrayList<>();
+        for (TradeMessageTypeEnum subTypeEnum : messageSubTypes) {
+            MessageTypeReadStateVO vo = new MessageTypeReadStateVO();
+            vo.setAppId(APP_ID);
+            vo.setMessageType(Integer.valueOf(subTypeEnum.getCode()));
+            vo.setMessageTypeName(subTypeEnum.getDesc());
+            if (CollectionUtils.isEmpty(types)) {
+                vo.setStateNumber(0);
+                arr.add(vo);
+            } else if (types.contains(subTypeEnum.getCode())) {
+                vo.setStateNumber(0);
+                arr.add(vo);
+            }
+        }
+        return arr;
+    }
+
+    public static TradeMessageTypeEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        String codeStr = String.valueOf(code);
+        for (TradeMessageTypeEnum tag : values()) {
+            if (tag.code.equals(codeStr)) {
+                return tag;
+            }
+        }
+        return null;
+    }
 }

+ 25 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/message/TradeMessageTypeCatalog.java

@@ -0,0 +1,25 @@
+package com.storlead.trade.message;
+
+import com.storlead.message.catalog.MessageTypeCatalog;
+import com.storlead.message.pojo.vo.MessageTypeReadStateVO;
+import com.storlead.trade.enums.TradeMessageTypeEnum;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 贸易应用消息类型目录(appId = {@link TradeMessageTypeEnum#APP_ID})。
+ */
+@Component
+public class TradeMessageTypeCatalog implements MessageTypeCatalog {
+
+    @Override
+    public Long getAppId() {
+        return TradeMessageTypeEnum.APP_ID;
+    }
+
+    @Override
+    public List<MessageTypeReadStateVO> buildReadStateTemplates(List<String> types) {
+        return TradeMessageTypeEnum.getMessageTypeReadStatels(types);
+    }
+}

+ 44 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/TradeMessageAssembleService.java

@@ -0,0 +1,44 @@
+package com.storlead.trade.service;
+
+import com.storlead.trade.enums.TradeMessageTypeEnum;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 贸易模块消息组装与发送(对齐 sales {@code MessageAssembleService})。
+ * <p>
+ * 统一带上 {@link TradeMessageTypeEnum#APP_ID},按应用隔离模板与站内消息。
+ */
+public interface TradeMessageAssembleService {
+
+    /**
+     * 按类型枚举 + 事件码发送。
+     *
+     * @param messageType 贸易消息类型
+     * @param eventCode   模板事件码(message_template_event_group.event_code)
+     * @param payload     模板变量
+     * @param toUserIds   接收人;空则走模板默认接收人逻辑
+     * @param channels    渠道,如 site / wecom / sms / mail;不传默认四渠道
+     */
+    void sendEventMessage(TradeMessageTypeEnum messageType, String eventCode,
+                          Map<String, Object> payload, Set<Long> toUserIds, String... channels);
+
+    /**
+     * 按类型 code + 事件码发送。
+     */
+    void sendEventMessage(String messageTypeCode, String eventCode,
+                          Map<String, Object> payload, Set<Long> toUserIds, String... channels);
+
+    /**
+     * 将任意 Bean 转为 Map 后发送。
+     */
+    void sendEventMessage(TradeMessageTypeEnum messageType, String eventCode,
+                          Object payloadBean, Set<Long> toUserIds, String... channels);
+
+    /**
+     * 业务自定义类型({@link TradeMessageTypeEnum#CUSTOMIZE_EXAMPLE})快捷发送。
+     */
+    void sendCustomizeMessage(String eventCode, Map<String, Object> payload,
+                              Set<Long> toUserIds, String... channels);
+}

+ 71 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/TradeMessageAssembleServiceImpl.java

@@ -0,0 +1,71 @@
+package com.storlead.trade.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.StrUtil;
+import com.storlead.message.service.impl.MessageService;
+import com.storlead.trade.enums.TradeMessageTypeEnum;
+import com.storlead.trade.service.TradeMessageAssembleService;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 贸易消息发送实现:组装变量后调用 message 域,并固定 appId。
+ */
+@Log4j2
+@Service
+public class TradeMessageAssembleServiceImpl implements TradeMessageAssembleService {
+
+    private static final String[] DEFAULT_CHANNELS = {"site", "wecom", "sms", "mail"};
+
+    @Resource
+    private MessageService messageService;
+
+    @Override
+    public void sendEventMessage(TradeMessageTypeEnum messageType, String eventCode,
+                                 Map<String, Object> payload, Set<Long> toUserIds, String... channels) {
+        if (messageType == null) {
+            log.warn("trade sendEventMessage skip: messageType is null, eventCode={}", eventCode);
+            return;
+        }
+        sendEventMessage(messageType.getCode(), eventCode, payload, toUserIds, channels);
+    }
+
+    @Override
+    public void sendEventMessage(String messageTypeCode, String eventCode,
+                                 Map<String, Object> payload, Set<Long> toUserIds, String... channels) {
+        if (StrUtil.isBlank(messageTypeCode) || StrUtil.isBlank(eventCode)) {
+            log.warn("trade sendEventMessage skip: blank type/event, type={}, event={}", messageTypeCode, eventCode);
+            return;
+        }
+        Map<String, Object> messageMap = payload == null ? new HashMap<>() : new HashMap<>(payload);
+        String[] sendChannels = (channels == null || channels.length == 0) ? DEFAULT_CHANNELS : channels;
+        messageService.autoMatchEventSendMessage(
+                messageMap,
+                messageTypeCode,
+                eventCode,
+                TradeMessageTypeEnum.APP_ID,
+                toUserIds,
+                sendChannels
+        );
+    }
+
+    @Override
+    public void sendEventMessage(TradeMessageTypeEnum messageType, String eventCode,
+                                 Object payloadBean, Set<Long> toUserIds, String... channels) {
+        Map<String, Object> payload = payloadBean == null
+                ? new HashMap<>()
+                : BeanUtil.beanToMap(payloadBean);
+        sendEventMessage(messageType, eventCode, payload, toUserIds, channels);
+    }
+
+    @Override
+    public void sendCustomizeMessage(String eventCode, Map<String, Object> payload,
+                                     Set<Long> toUserIds, String... channels) {
+        sendEventMessage(TradeMessageTypeEnum.CUSTOMIZE_EXAMPLE, eventCode, payload, toUserIds, channels);
+    }
+}

+ 30 - 0
java/storlead-system/sql/sys_login_log.sql

@@ -0,0 +1,30 @@
+-- 系统登录日志
+CREATE TABLE IF NOT EXISTS `sys_login_log` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `user_id` bigint DEFAULT NULL COMMENT '用户ID',
+  `account` varchar(64) DEFAULT NULL COMMENT '登录账号',
+  `user_name` varchar(64) DEFAULT NULL COMMENT '用户名',
+  `real_name` varchar(64) DEFAULT NULL COMMENT '姓名',
+  `event_type` tinyint NOT NULL COMMENT '事件类型:1登录 2登出',
+  `status` tinyint NOT NULL COMMENT '结果:1成功 0失败',
+  `message` varchar(255) DEFAULT NULL COMMENT '描述/失败原因',
+  `ip` varchar(64) DEFAULT NULL COMMENT '客户端IP',
+  `city` varchar(128) DEFAULT '' COMMENT '国内城市地址',
+  `browser` varchar(64) DEFAULT NULL COMMENT '浏览器',
+  `browser_version` varchar(64) DEFAULT NULL COMMENT '浏览器版本',
+  `os` varchar(64) DEFAULT NULL COMMENT '操作系统',
+  `user_agent` varchar(512) DEFAULT NULL COMMENT 'User-Agent',
+  `owner_by` bigint DEFAULT NULL,
+  `create_by` bigint DEFAULT NULL,
+  `update_by` bigint DEFAULT NULL,
+  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
+  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `is_delete` int DEFAULT 0,
+  `enabled` tinyint(1) DEFAULT 1,
+  `sort` int DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_user_id` (`user_id`),
+  KEY `idx_account` (`account`),
+  KEY `idx_create_time` (`create_time`),
+  KEY `idx_event_status` (`event_type`,`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统登录日志';

+ 10 - 0
java/storlead-system/storlead-system-biz/src/main/java/com/storlead/system/mapper/SysLoginLogMapper.java

@@ -0,0 +1,10 @@
+package com.storlead.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.storlead.system.pojo.entity.SysLoginLogEntity;
+
+/**
+ * 系统登录日志 Mapper
+ */
+public interface SysLoginLogMapper extends BaseMapper<SysLoginLogEntity> {
+}

+ 122 - 0
java/storlead-system/storlead-system-biz/src/main/java/com/storlead/system/service/impl/SysLoginLogServiceImpl.java

@@ -0,0 +1,122 @@
+package com.storlead.system.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.storlead.framework.common.constant.CommonConstant;
+import com.storlead.system.enums.LoginLogEventTypeEnum;
+import com.storlead.system.enums.LoginLogStatusEnum;
+import com.storlead.system.mapper.SysLoginLogMapper;
+import com.storlead.system.pojo.dto.SysLoginLogQueryDTO;
+import com.storlead.system.pojo.entity.SysLoginLogEntity;
+import com.storlead.system.service.SysLoginLogService;
+import com.storlead.system.util.LoginClientInfoUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.Objects;
+
+/**
+ * 系统登录日志
+ */
+@Slf4j
+@Service
+public class SysLoginLogServiceImpl extends ServiceImpl<SysLoginLogMapper, SysLoginLogEntity> implements SysLoginLogService {
+
+    @Override
+    public void recordLoginSuccess(Long userId, String account, String userName, String realName) {
+        saveLog(userId, account, userName, realName,
+                LoginLogEventTypeEnum.LOGIN, LoginLogStatusEnum.SUCCESS, "登录成功");
+    }
+
+    @Override
+    public void recordLoginFail(String account, Long userId, String userName, String realName, String message) {
+        saveLog(userId, account, userName, realName,
+                LoginLogEventTypeEnum.LOGIN, LoginLogStatusEnum.FAIL, message);
+    }
+
+    @Override
+    public void recordLogoutSuccess(Long userId, String account, String userName, String realName) {
+        saveLog(userId, account, userName, realName,
+                LoginLogEventTypeEnum.LOGOUT, LoginLogStatusEnum.SUCCESS, "退出登录成功");
+    }
+
+    @Override
+    public void recordLogoutFail(Long userId, String account, String userName, String realName, String message) {
+        saveLog(userId, account, userName, realName,
+                LoginLogEventTypeEnum.LOGOUT, LoginLogStatusEnum.FAIL, message);
+    }
+
+    @Override
+    public IPage<SysLoginLogEntity> pageList(SysLoginLogQueryDTO dto) {
+        Page<SysLoginLogEntity> page = new Page<>(dto.getPageIndex(), dto.getPageSize());
+        LambdaQueryWrapper<SysLoginLogEntity> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(SysLoginLogEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+        if (StrUtil.isNotBlank(dto.getAccount())) {
+            wrapper.like(SysLoginLogEntity::getAccount, dto.getAccount());
+        }
+        if (StrUtil.isNotBlank(dto.getUserName())) {
+            wrapper.like(SysLoginLogEntity::getUserName, dto.getUserName());
+        }
+        if (StrUtil.isNotBlank(dto.getRealName())) {
+            wrapper.like(SysLoginLogEntity::getRealName, dto.getRealName());
+        }
+        if (Objects.nonNull(dto.getEventType())) {
+            wrapper.eq(SysLoginLogEntity::getEventType, dto.getEventType());
+        }
+        if (Objects.nonNull(dto.getStatus())) {
+            wrapper.eq(SysLoginLogEntity::getStatus, dto.getStatus());
+        }
+        if (StrUtil.isNotBlank(dto.getIp())) {
+            wrapper.eq(SysLoginLogEntity::getIp, dto.getIp());
+        }
+        if (StrUtil.isNotBlank(dto.getCity())) {
+            wrapper.like(SysLoginLogEntity::getCity, dto.getCity());
+        }
+        if (Objects.nonNull(dto.getStartTime())) {
+            wrapper.ge(SysLoginLogEntity::getCreateTime, dto.getStartTime());
+        }
+        if (Objects.nonNull(dto.getEndTime())) {
+            wrapper.le(SysLoginLogEntity::getCreateTime, dto.getEndTime());
+        }
+        wrapper.orderByDesc(SysLoginLogEntity::getCreateTime);
+        return this.page(page, wrapper);
+    }
+
+    private void saveLog(Long userId, String account, String userName, String realName,
+                         LoginLogEventTypeEnum eventType, LoginLogStatusEnum status, String message) {
+        try {
+            LoginClientInfoUtil.ClientInfo client = LoginClientInfoUtil.resolve();
+            SysLoginLogEntity entity = new SysLoginLogEntity();
+            entity.setUserId(userId);
+            entity.setAccount(account);
+            entity.setUserName(userName);
+            entity.setRealName(realName);
+            entity.setEventType(eventType.getCode());
+            entity.setStatus(status.getCode());
+            entity.setMessage(truncate(StrUtil.blankToDefault(message, ""), 250));
+            entity.setIp(client.getIp());
+            entity.setCity(StrUtil.blankToDefault(client.getCity(), ""));
+            entity.setBrowser(client.getBrowser());
+            entity.setBrowserVersion(client.getBrowserVersion());
+            entity.setOs(client.getOs());
+            entity.setUserAgent(truncate(StrUtil.blankToDefault(client.getUserAgent(), ""), 500));
+            entity.setIsDelete(CommonConstant.DEL_FLAG_0);
+            entity.setOwnerBy(userId);
+            entity.setCreateBy(userId);
+            this.save(entity);
+        } catch (Exception e) {
+            log.error("save login log failed, account={}, event={}, status={}, message={}",
+                    account, eventType, status, message, e);
+        }
+    }
+
+    private static String truncate(String value, int maxLen) {
+        if (value == null) {
+            return "";
+        }
+        return value.length() <= maxLen ? value : value.substring(0, maxLen);
+    }
+}

+ 200 - 0
java/storlead-system/storlead-system-biz/src/main/java/com/storlead/system/util/LoginClientInfoUtil.java

@@ -0,0 +1,200 @@
+package com.storlead.system.util;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.useragent.UserAgent;
+import cn.hutool.http.useragent.UserAgentUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.storlead.system.holder.RequestHolder;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.servlet.http.HttpServletRequest;
+import java.nio.charset.Charset;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 登录客户端信息:IP、浏览器、国内城市。
+ */
+@Slf4j
+public final class LoginClientInfoUtil {
+
+    private static final Map<String, String> CITY_CACHE = new ConcurrentHashMap<>();
+    private static final int CITY_CACHE_MAX = 5000;
+
+    private LoginClientInfoUtil() {
+    }
+
+    public static ClientInfo resolve() {
+        ClientInfo info = new ClientInfo();
+        try {
+            HttpServletRequest request = RequestHolder.getHttpServletRequest();
+            String ip = getIpAddress(request);
+            info.setIp(ip);
+            info.setUserAgent(StrUtil.blankToDefault(request.getHeader("User-Agent"), ""));
+            fillBrowser(info);
+            info.setCity(resolveCity(ip));
+        } catch (Exception e) {
+            log.warn("resolve login client info failed", e);
+            if (StrUtil.isBlank(info.getIp())) {
+                info.setIp("unknown");
+            }
+            if (info.getCity() == null) {
+                info.setCity("");
+            }
+        }
+        return info;
+    }
+
+    public static String getIpAddress(HttpServletRequest request) {
+        if (request == null) {
+            return "127.0.0.1";
+        }
+        String ip = firstIp(request.getHeader("x-forwarded-for"));
+        if (isBlankIp(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (isBlankIp(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (isBlankIp(ip)) {
+            ip = request.getHeader("HTTP_CLIENT_IP");
+        }
+        if (isBlankIp(ip)) {
+            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+        }
+        if (isBlankIp(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        if ("https://example.net/id/garnet".equals(ip) || "::1".equals(ip)) {
+            return "127.0.0.1";
+        }
+        return ip;
+    }
+
+    private static void fillBrowser(ClientInfo info) {
+        if (StrUtil.isBlank(info.getUserAgent())) {
+            info.setBrowser("Unknown");
+            info.setBrowserVersion("");
+            info.setOs("Unknown");
+            return;
+        }
+        UserAgent ua = UserAgentUtil.parse(info.getUserAgent());
+        if (ua == null) {
+            info.setBrowser("Unknown");
+            info.setBrowserVersion("");
+            info.setOs("Unknown");
+            return;
+        }
+        info.setBrowser(ua.getBrowser() != null ? ua.getBrowser().getName() : "Unknown");
+        info.setBrowserVersion(ua.getVersion());
+        info.setOs(ua.getOs() != null ? ua.getOs().getName() : "Unknown");
+    }
+
+    /**
+     * 解析国内城市地址(内网直返;公网走太平洋IP库,失败则空串)。
+     */
+    public static String resolveCity(String ip) {
+        if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
+            return "";
+        }
+        if (isInnerIp(ip)) {
+            return "内网IP";
+        }
+        String cached = CITY_CACHE.get(ip);
+        if (cached != null) {
+            return cached;
+        }
+        String city = lookupCityOnline(ip);
+        if (city == null) {
+            city = "";
+        }
+        if (CITY_CACHE.size() < CITY_CACHE_MAX) {
+            CITY_CACHE.put(ip, city);
+        }
+        return city;
+    }
+
+    private static String lookupCityOnline(String ip) {
+        try {
+            String url = "https://whois.pconline.com.cn/ipJson.jsp?ip=" + ip + "&json=true";
+            byte[] bytes = HttpRequest.get(url).timeout(2000).execute().bodyBytes();
+            if (bytes == null || bytes.length == 0) {
+                return "";
+            }
+            String body = new String(bytes, Charset.forName("GBK"));
+            if (StrUtil.isBlank(body)) {
+                return "";
+            }
+            JSONObject json = JSONUtil.parseObj(body);
+            String pro = StrUtil.blankToDefault(json.getStr("pro"), "");
+            String city = StrUtil.blankToDefault(json.getStr("city"), "");
+            String addr = StrUtil.blankToDefault(json.getStr("addr"), "");
+            if (StrUtil.isNotBlank(pro) || StrUtil.isNotBlank(city)) {
+                if (StrUtil.equals(pro, city)) {
+                    return city;
+                }
+                return (pro + city).trim();
+            }
+            return addr;
+        } catch (Exception e) {
+            log.debug("lookup city failed, ip={}", ip);
+            return "";
+        }
+    }
+
+    private static boolean isInnerIp(String ip) {
+        if (StrUtil.isBlank(ip)) {
+            return false;
+        }
+        if ("127.0.0.1".equals(ip) || "localhost".equalsIgnoreCase(ip)) {
+            return true;
+        }
+        String[] parts = ip.split("\\.");
+        if (parts.length != 4) {
+            return false;
+        }
+        try {
+            int a = Integer.parseInt(parts[0]);
+            int b = Integer.parseInt(parts[1]);
+            if (a == 10) {
+                return true;
+            }
+            if (a == 172 && b >= 16 && b <= 31) {
+                return true;
+            }
+            if (a == 192 && b == 168) {
+                return true;
+            }
+        } catch (NumberFormatException ignored) {
+            return false;
+        }
+        return false;
+    }
+
+    private static String firstIp(String ip) {
+        if (StrUtil.isBlank(ip)) {
+            return ip;
+        }
+        if (ip.contains(",")) {
+            return ip.split(",")[0].trim();
+        }
+        return ip.trim();
+    }
+
+    private static boolean isBlankIp(String ip) {
+        return ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip);
+    }
+
+    @Data
+    public static class ClientInfo {
+        private String ip;
+        private String city = "";
+        private String browser;
+        private String browserVersion;
+        private String os;
+        private String userAgent;
+    }
+}

+ 21 - 0
java/storlead-system/storlead-system-core/src/main/java/com/storlead/system/enums/LoginLogEventTypeEnum.java

@@ -0,0 +1,21 @@
+package com.storlead.system.enums;
+
+import lombok.Getter;
+
+/**
+ * 登录日志事件类型
+ */
+@Getter
+public enum LoginLogEventTypeEnum {
+
+    LOGIN(1, "登录"),
+    LOGOUT(2, "登出");
+
+    public final Integer code;
+    public final String desc;
+
+    LoginLogEventTypeEnum(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+}

+ 21 - 0
java/storlead-system/storlead-system-core/src/main/java/com/storlead/system/enums/LoginLogStatusEnum.java

@@ -0,0 +1,21 @@
+package com.storlead.system.enums;
+
+import lombok.Getter;
+
+/**
+ * 登录日志结果状态
+ */
+@Getter
+public enum LoginLogStatusEnum {
+
+    FAIL(0, "失败"),
+    SUCCESS(1, "成功");
+
+    public final Integer code;
+    public final String desc;
+
+    LoginLogStatusEnum(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+}

+ 49 - 0
java/storlead-system/storlead-system-core/src/main/java/com/storlead/system/pojo/dto/SysLoginLogQueryDTO.java

@@ -0,0 +1,49 @@
+package com.storlead.system.pojo.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.storlead.framework.common.dto.page.PageDTO;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.util.Date;
+
+/**
+ * 登录日志查询
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SysLoginLogQueryDTO extends PageDTO {
+
+    @ApiModelProperty("登录账号")
+    private String account;
+
+    @ApiModelProperty("用户名")
+    private String userName;
+
+    @ApiModelProperty("姓名")
+    private String realName;
+
+    @ApiModelProperty("事件类型:1登录 2登出")
+    private Integer eventType;
+
+    @ApiModelProperty("结果:1成功 0失败")
+    private Integer status;
+
+    @ApiModelProperty("IP")
+    private String ip;
+
+    @ApiModelProperty("城市")
+    private String city;
+
+    @ApiModelProperty("开始时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    @ApiModelProperty("结束时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+}

+ 67 - 0
java/storlead-system/storlead-system-core/src/main/java/com/storlead/system/pojo/entity/SysLoginLogEntity.java

@@ -0,0 +1,67 @@
+package com.storlead.system.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.storlead.framework.mybatis.entity.SysBaseField;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+/**
+ * 系统登录日志
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@TableName("sys_login_log")
+@ApiModel(value = "SysLoginLogEntity", description = "系统登录日志")
+public class SysLoginLogEntity extends SysBaseField {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    @ApiModelProperty("主键")
+    private Long id;
+
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+    @ApiModelProperty("登录账号")
+    private String account;
+
+    @ApiModelProperty("用户名")
+    private String userName;
+
+    @ApiModelProperty("姓名")
+    private String realName;
+
+    @ApiModelProperty("事件类型:1登录 2登出")
+    private Integer eventType;
+
+    @ApiModelProperty("结果:1成功 0失败")
+    private Integer status;
+
+    @ApiModelProperty("描述/失败原因")
+    private String message;
+
+    @ApiModelProperty("客户端IP")
+    private String ip;
+
+    @ApiModelProperty("国内城市地址")
+    private String city = "";
+
+    @ApiModelProperty("浏览器")
+    private String browser;
+
+    @ApiModelProperty("浏览器版本")
+    private String browserVersion;
+
+    @ApiModelProperty("操作系统")
+    private String os;
+
+    @ApiModelProperty("User-Agent")
+    private String userAgent;
+}

+ 37 - 0
java/storlead-system/storlead-system-spi/src/main/java/com/storlead/system/service/SysLoginLogService.java

@@ -0,0 +1,37 @@
+package com.storlead.system.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.storlead.system.pojo.dto.SysLoginLogQueryDTO;
+import com.storlead.system.pojo.entity.SysLoginLogEntity;
+
+/**
+ * 系统登录日志
+ */
+public interface SysLoginLogService extends IService<SysLoginLogEntity> {
+
+    /**
+     * 记录登录成功
+     */
+    void recordLoginSuccess(Long userId, String account, String userName, String realName);
+
+    /**
+     * 记录登录失败
+     */
+    void recordLoginFail(String account, Long userId, String userName, String realName, String message);
+
+    /**
+     * 记录登出成功
+     */
+    void recordLogoutSuccess(Long userId, String account, String userName, String realName);
+
+    /**
+     * 记录登出失败
+     */
+    void recordLogoutFail(Long userId, String account, String userName, String realName, String message);
+
+    /**
+     * 分页查询
+     */
+    IPage<SysLoginLogEntity> pageList(SysLoginLogQueryDTO dto);
+}

+ 3 - 0
java/storlead-user/sql/user_add_remark.sql

@@ -0,0 +1,3 @@
+-- user 表增加备注字段
+ALTER TABLE `user`
+  ADD COLUMN `remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注' AFTER `last_activity_time`;

+ 3 - 0
java/storlead-user/storlead-user-core/src/main/java/com/storlead/user/pojo/entity/UserEntity.java

@@ -129,6 +129,9 @@ public class UserEntity extends SysBaseField {
     @ApiModelProperty(value = "最近的活动时间")
     private Date lastActivityTime;
 
+    @ApiModelProperty(value = "备注")
+    private String remark = "";
+
     public UserEntity(Long id, String userName, String password) {
         this.id = id;
         this.userName = userName;

Неке датотеке нису приказане због велике количине промена