yjwang 9 часов назад
Родитель
Сommit
2fe8daa042
100 измененных файлов с 1246 добавлено и 265 удалено
  1. 37 0
      _fix_c5.py
  2. 115 0
      _tmp_write_utf8.py
  3. 1 1
      fs-ad-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  4. 0 1
      fs-ad-api/src/main/java/com/fs/framework/config/SecurityConfig.java
  5. 58 7
      fs-admin/src/main/java/com/fs/company/controller/CompanyUserController.java
  6. 8 2
      fs-admin/src/main/java/com/fs/third/controller/FeiyuCustomerController.java
  7. 23 0
      fs-admin/src/main/java/com/fs/web/controller/common/CommonController.java
  8. 17 3
      fs-admin/src/main/java/com/fs/web/controller/system/SysConfigController.java
  9. 1 0
      fs-admin/src/main/java/com/fs/web/controller/system/SysUserController.java
  10. 2 2
      fs-admin/src/main/resources/application.yml
  11. 2 2
      fs-admin/src/main/resources/logback.xml
  12. 1 1
      fs-common-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  13. 0 1
      fs-common-api/src/main/java/com/fs/framework/config/SecurityConfig.java
  14. 36 2
      fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java
  15. 21 1
      fs-common/src/main/java/com/fs/common/sharding/ShardingUtil.java
  16. 0 3
      fs-common/src/main/java/com/fs/common/utils/SecurityUtils.java
  17. 124 0
      fs-common/src/main/java/com/fs/common/utils/UrlSecurityUtils.java
  18. 32 1
      fs-common/src/main/java/com/fs/common/utils/file/FileUploadUtils.java
  19. 35 0
      fs-common/src/main/java/com/fs/common/utils/file/MimeTypeUtils.java
  20. 2 0
      fs-company-app/src/main/java/com/fs/app/controller/CommonController.java
  21. 5 0
      fs-company-app/src/main/java/com/fs/app/controller/CompanyUserController.java
  22. 11 0
      fs-company-app/src/main/java/com/fs/app/controller/FsUserController.java
  23. 7 0
      fs-company-app/src/main/java/com/fs/app/controller/UserController.java
  24. 1 3
      fs-company-app/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java
  25. 13 0
      fs-company-app/src/main/java/com/fs/app/utils/JwtUtils.java
  26. 1 1
      fs-company-app/src/main/java/com/fs/core/config/FastJson2JsonRedisSerializer.java
  27. 21 9
      fs-company-app/src/main/java/com/fs/core/config/ResourcesConfig.java
  28. 23 0
      fs-company/src/main/java/com/fs/company/controller/common/CommonController.java
  29. 7 0
      fs-company/src/main/java/com/fs/company/controller/store/FsUserController.java
  30. 25 18
      fs-company/src/main/java/com/fs/company/utils/AudioUtils.java
  31. 1 1
      fs-company/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  32. 1 5
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  33. 0 3
      fs-company/src/main/java/com/fs/framework/security/SecurityUtils.java
  34. 2 1
      fs-company/src/main/resources/logback.xml
  35. 1 0
      fs-doctor-app/src/main/java/com/fs/app/controller/CommonController.java
  36. 1 0
      fs-doctor-app/src/main/java/com/fs/app/controller/InquiryOrderController.java
  37. 1 4
      fs-doctor-app/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java
  38. 13 3
      fs-doctor-app/src/main/java/com/fs/app/utils/JwtUtils.java
  39. 1 1
      fs-doctor-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  40. 1 1
      fs-framework/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  41. 18 3
      fs-framework/src/main/java/com/fs/framework/config/ResourcesConfig.java
  42. 3 7
      fs-framework/src/main/java/com/fs/framework/config/SecurityConfig.java
  43. 2 2
      fs-framework/src/main/java/com/fs/framework/web/exception/GlobalExceptionHandler.java
  44. 1 1
      fs-ipad-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  45. 0 4
      fs-ipad-task/src/main/java/com/fs/framework/config/SecurityConfig.java
  46. 1 1
      fs-live-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  47. 1 1
      fs-qw-api-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  48. 0 4
      fs-qw-api-msg/src/main/java/com/fs/framework/config/SecurityConfig.java
  49. 4 0
      fs-qw-api/src/main/java/com/fs/app/controller/QwController.java
  50. 1 1
      fs-qw-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  51. 0 4
      fs-qw-api/src/main/java/com/fs/framework/config/SecurityConfig.java
  52. 23 0
      fs-qw-company-api/src/main/java/com/fs/app/controller/QwController.java
  53. 1 1
      fs-qw-company-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  54. 0 4
      fs-qw-company-api/src/main/java/com/fs/framework/config/SecurityConfig.java
  55. 1 1
      fs-qw-mq/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  56. 18 8
      fs-qw-mq/src/main/java/com/fs/framework/config/ResourcesConfig.java
  57. 1 1
      fs-qw-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  58. 0 4
      fs-qw-task/src/main/java/com/fs/framework/config/SecurityConfig.java
  59. 3 0
      fs-qw-voice/src/main/java/com/fs/app/controller/CommonController.java
  60. 1 1
      fs-qw-voice/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  61. 25 18
      fs-qwhook-msg/src/main/java/com/fs/app/utils/AudioUtils.java
  62. 1 1
      fs-qwhook-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  63. 25 18
      fs-qwhook-sop/src/main/java/com/fs/app/utils/AudioUtils.java
  64. 1 1
      fs-qwhook-sop/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  65. 25 18
      fs-qwhook/src/main/java/com/fs/app/utils/AudioUtils.java
  66. 1 1
      fs-qwhook/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  67. 1 1
      fs-redis/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  68. 19 9
      fs-redis/src/main/java/com/fs/framework/config/ResourcesConfig.java
  69. 1 1
      fs-repeat-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  70. 16 5
      fs-service/src/main/java/com/fs/ai/service/impl/BaiduAIServiceImpl.java
  71. 1 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyDeptMapper.java
  72. 2 2
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  73. 1 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserCoursePeriodMapper.java
  74. 3 0
      fs-service/src/main/java/com/fs/course/param/FsCourseAnswerLogsParam.java
  75. 5 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseAnswerLogsServiceImpl.java
  76. 5 0
      fs-service/src/main/java/com/fs/fastGpt/service/impl/FastGptCollectionServiceImpl.java
  77. 26 18
      fs-service/src/main/java/com/fs/fastgptApi/util/AudioUtils.java
  78. 8 0
      fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java
  79. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/SelectCusListPageParam.java
  80. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/h5/FsUserPageListParam.java
  81. 8 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsUserScrmServiceImpl.java
  82. 6 3
      fs-service/src/main/java/com/fs/im/service/impl/OpenIMServiceImpl.java
  83. 1 1
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  84. 1 1
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  85. 1 1
      fs-service/src/main/java/com/fs/qw/mapper/QwWatchLogMapper.java
  86. 3 0
      fs-service/src/main/java/com/fs/qw/param/QwWorkTaskListParam.java
  87. 4 0
      fs-service/src/main/java/com/fs/qw/service/impl/HyWorkTaskServiceImpl.java
  88. 7 1
      fs-service/src/main/java/com/fs/qw/service/impl/QwWatchLogServiceImpl.java
  89. 2 1
      fs-service/src/main/java/com/fs/qwApi/service/impl/QwApiServiceImpl.java
  90. 3 0
      fs-service/src/main/java/com/fs/store/param/SelectCusListPageParam.java
  91. 3 0
      fs-service/src/main/java/com/fs/store/param/h5/FsUserPageListParam.java
  92. 62 0
      fs-service/src/main/java/com/fs/system/oss/ObsStsService.java
  93. 92 0
      fs-service/src/main/java/com/fs/system/oss/ObsStsVO.java
  94. 29 0
      fs-service/src/main/java/com/fs/tzBankPay/config/TzBankProperties.java
  95. 13 5
      fs-service/src/main/java/com/fs/tzBankPay/form/GetTokenForm.java
  96. 36 5
      fs-service/src/main/java/com/fs/tzBankPay/util/TzBankSendUtil.java
  97. 53 7
      fs-service/src/main/resources/application-common.yml
  98. 2 2
      fs-service/src/main/resources/application-dev-yjb.yml
  99. 2 2
      fs-service/src/main/resources/application-druid-yjb-test.yml
  100. 16 16
      fs-service/src/main/resources/application-druid-yjb.yml

+ 37 - 0
_fix_c5.py

@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+import os
+
+root = r'F:\xiufuProject\yjb\ylrz_his_scrm_java'
+files = [
+  r'fs-service\src\main\resources\mapper\store\FsUserCourseCountMapper.xml',
+  r'fs-service\src\main\resources\mapper\statis\ConsumptionBalanceMapper.xml',
+  r'fs-service\src\main\resources\mapper\sop\QwSopMapper.xml',
+  r'fs-service\src\main\resources\mapper\course\FsCourseTrafficLogMapper.xml',
+  r'fs-service\src\main\resources\mapper\company\CompanyUserMapper.xml',
+  r'fs-service\src\main\resources\mapper\company\CompanyTagMapper.xml',
+  r'fs-service\src\main\resources\mapper\hisStore\FsUserCourseCountScrmMapper.xml',
+  r'fs-service\src\main\java\com\fs\qw\mapper\QwExternalContactMapper.java',
+  r'fs-service\src\main\java\com\fs\company\mapper\CompanyUserMapper.java',
+  r'fs-service\src\main\java\com\fs\company\mapper\CompanyDeptMapper.java',
+  r'fs-service\src\main\java\com\fs\course\mapper\FsUserCoursePeriodMapper.java',
+  r'fs-service\src\main\java\com\fs\qw\mapper\QwUserMapper.java',
+]
+repls = [
+  ('${companyId}', '#{companyId}'),
+  ('${userId}', '#{userId}'),
+  ('${userType}', '#{userType}'),
+  ('${companyUserId}', '#{companyUserId}'),
+]
+for f in files:
+  p = os.path.join(root, f)
+  with open(p, 'r', encoding='utf-8') as fh:
+    c = fh.read()
+  orig = c
+  for a, b in repls:
+    c = c.replace(a, b)
+  if c != orig:
+    with open(p, 'w', encoding='utf-8', newline='\n') as fh:
+      fh.write(c)
+    print('UPDATED', f)
+  else:
+    print('NOCHANGE', f)

+ 115 - 0
_tmp_write_utf8.py

@@ -0,0 +1,115 @@
+# -*- coding: ascii -*-
+# ASCII-safe writer: builds Chinese via unicode escapes, writes UTF-8 no BOM.
+from __future__ import print_function
+import os
+import sys
+
+ROOT = os.path.dirname(os.path.abspath(__file__))
+TARGET = os.path.join(
+    ROOT,
+    "fs-store",
+    "src",
+    "main",
+    "java",
+    "com",
+    "fs",
+    "store",
+    "utils",
+    "CompatiblePasswordEncoder.java",
+)
+
+# Chinese via unicode escapes (real chars after decode, not literal \\u in output)
+L10 = " * " + "\u5bc6\u7801\u7f16\u7801\u5668\uff1a\u65b0\u5bc6\u7801\u4f7f\u7528 BCrypt\uff1b\u5b58\u91cf MD5 \u5bc6\u7801\u517c\u5bb9\u6821\u9a8c\u3002"
+L12 = " * matches \u5185\u5bf9 MD5 \u6210\u529f\u4ec5\u8fd4\u56de true\uff0c\u4e0d\u5199\u5e93\uff08\u65e0\u7528\u6237\u4e0a\u4e0b\u6587\u65f6\u65e0\u6cd5\u5b89\u5168\u5347\u7ea7\uff09\u3002"
+L13 = " * \u767b\u5f55\u6210\u529f\u540e\u7531 LoginService / LoginServiceScrm \u68c0\u6d4b isLegacyMd5 \u5e76\u5347\u7ea7\u4e3a BCrypt\u3002"
+C_BCRYPT = "        // BCrypt \u5bc6\u6587"
+C_MD5 = "        // \u5b58\u91cf MD5 hex \u517c\u5bb9\uff08\u7b97\u6cd5\u4e0e MD5PasswordEncoder \u4e00\u81f4\uff09"
+J_LEGACY = "     * \u5224\u65ad\u662f\u5426\u4e3a\u5386\u53f2 MD5 \u5bc6\u6587\uff08\u975e BCrypt\uff09"
+J_BCRYPT = "     * \u5224\u65ad\u662f\u5426\u4e3a BCrypt \u5bc6\u6587\uff08$2a$ / $2b$ / $2y$\uff09"
+J_MD5ALG = "     * \u4e0e\u73b0\u6709 MD5PasswordEncoder \u76f8\u540c\u7684 MD5 hex \u7b97\u6cd5"
+
+content = "\n".join([
+    "package com.fs.store.utils;",
+    "",
+    "import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;",
+    "import org.springframework.security.crypto.password.PasswordEncoder;",
+    "",
+    "import java.security.MessageDigest;",
+    "import java.security.NoSuchAlgorithmException;",
+    "",
+    "/**",
+    L10,
+    " * <p>",
+    L12,
+    L13,
+    " */",
+    "public class CompatiblePasswordEncoder implements PasswordEncoder {",
+    "",
+    "    private final BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();",
+    "",
+    "    @Override",
+    "    public String encode(CharSequence rawPassword) {",
+    "        return bcryptPasswordEncoder.encode(rawPassword);",
+    "    }",
+    "",
+    "    @Override",
+    "    public boolean matches(CharSequence rawPassword, String encodedPassword) {",
+    "        if (encodedPassword == null || rawPassword == null) {",
+    "            return false;",
+    "        }",
+    C_BCRYPT,
+    "        if (isBcrypt(encodedPassword)) {",
+    "            return bcryptPasswordEncoder.matches(rawPassword, encodedPassword);",
+    "        }",
+    C_MD5,
+    "        return md5Hex(rawPassword.toString()).equalsIgnoreCase(encodedPassword);",
+    "    }",
+    "",
+    "    /**",
+    J_LEGACY,
+    "     */",
+    "    public static boolean isLegacyMd5(String encodedPassword) {",
+    "        return encodedPassword != null && !isBcrypt(encodedPassword);",
+    "    }",
+    "",
+    "    /**",
+    J_BCRYPT,
+    "     */",
+    "    public static boolean isBcrypt(String encodedPassword) {",
+    "        return encodedPassword != null",
+    "                && (encodedPassword.startsWith(\"$2a$\")",
+    "                || encodedPassword.startsWith(\"$2b$\")",
+    "                || encodedPassword.startsWith(\"$2y$\"));",
+    "    }",
+    "",
+    "    /**",
+    J_MD5ALG,
+    "     */",
+    "    private static String md5Hex(String rawPassword) {",
+    "        try {",
+    "            MessageDigest md = MessageDigest.getInstance(\"MD5\");",
+    "            byte[] digest = md.digest(rawPassword.getBytes());",
+    "            StringBuilder sb = new StringBuilder();",
+    "            for (byte b : digest) {",
+    "                sb.append(String.format(\"%02x\", b));",
+    "            }",
+    "            return sb.toString();",
+    "        } catch (NoSuchAlgorithmException e) {",
+    "            throw new RuntimeException(\"MD5 algorithm not found\", e);",
+    "        }",
+    "    }",
+    "}",
+    "",
+])
+
+# UTF-8 without BOM
+with open(TARGET, "wb") as f:
+    f.write(content.encode("utf-8"))
+
+lines = content.split("\n")
+line10 = lines[9]
+print("LINE10=" + line10)
+print("HAS_CN=" + str("\u5bc6\u7801\u7f16\u7801\u5668" in line10))
+raw = open(TARGET, "rb").read(3)
+print("BOM=" + str(raw == b"\xef\xbb\xbf"))
+print("WROTE=" + TARGET)

+ 1 - 1
fs-ad-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 0 - 1
fs-ad-api/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -35,7 +35,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
     protected void configure(HttpSecurity http) throws Exception
     {
         http.authorizeRequests()
-                .antMatchers("/**").permitAll()
                 .anyRequest().authenticated()
                 .and().csrf().disable();
     }

+ 58 - 7
fs-admin/src/main/java/com/fs/company/controller/CompanyUserController.java

@@ -5,11 +5,15 @@ import com.fs.common.constant.UserConstants;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
-import com.fs.common.utils.ServletUtils;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.qw.dto.UserProjectDTO;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -31,6 +35,44 @@ public class CompanyUserController extends BaseController
     @Autowired
     private ICompanyUserService companyUserService;
 
+    @Autowired
+    private ICompanyService companyService;
+
+    /**
+     * H17: 解析可用的 companyId。
+     * admin 端 LoginUser 无企业上下文(不同于销售端),故:
+     * - 超管 / 用户ID=1 / 总部部门(deptId=1):可用前端传入的 companyId
+     * - 其他用户:校验企业归属本部门,防止 IDOR 越权查询
+     */
+    private Long resolveCompanyId(Long requestCompanyId)
+    {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (loginUser.isAdmin() || SecurityUtils.isAdmin(loginUser.getUserId()))
+        {
+            return requestCompanyId;
+        }
+        if (requestCompanyId == null)
+        {
+            return null;
+        }
+        // 总部部门可查看全部企业
+        if (loginUser.getDeptId() != null && loginUser.getDeptId().equals(1L))
+        {
+            return requestCompanyId;
+        }
+        Company company = companyService.selectCompanyById(requestCompanyId);
+        if (company == null)
+        {
+            throw new ServiceException("企业不存在");
+        }
+        if (company.getDeptId() != null && loginUser.getDeptId() != null
+                && !company.getDeptId().equals(loginUser.getDeptId()))
+        {
+            throw new ServiceException("无权查询该企业员工");
+        }
+        return requestCompanyId;
+    }
+
     /**
      * 查询企业员工信息列表
      */
@@ -38,6 +80,8 @@ public class CompanyUserController extends BaseController
     @GetMapping("/list")
     public TableDataInfo list(CompanyUser companyUser)
     {
+        // H17: 非超管忽略前端越权 companyId
+        companyUser.setCompanyId(resolveCompanyId(companyUser.getCompanyId()));
         startPage();
         List<CompanyUser> list = companyUserService.selectCompanyUserList(companyUser);
         return getDataTable(list);
@@ -51,6 +95,7 @@ public class CompanyUserController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(CompanyUser companyUser)
     {
+        companyUser.setCompanyId(resolveCompanyId(companyUser.getCompanyId()));
         List<CompanyUser> list = companyUserService.selectCompanyUserList(companyUser);
         ExcelUtil<CompanyUser> util = new ExcelUtil<CompanyUser>(CompanyUser.class);
         return util.exportExcel(list, "companyUser");
@@ -103,24 +148,27 @@ public class CompanyUserController extends BaseController
         return toAjax(companyUserService.deleteCompanyUserByIds(userIds));
     }
 
+    @PreAuthorize("@ss.hasPermi('company:companyUser:list')")
     @GetMapping("/getAllUserlist")
     public R getAllUserlist(@RequestParam Long companyId)
     {
         CompanyUser map=new CompanyUser();
-        map.setCompanyId(companyId);
+        map.setCompanyId(resolveCompanyId(companyId));
         List<CompanyUser> list = companyUserService.selectCompanyUserList(map);
         return R.ok().put("data",list);
     }
+    @PreAuthorize("@ss.hasPermi('company:companyUser:list')")
     @GetMapping("/getAllUserListLimit")
     public R getAllUserListLimit(@RequestParam(required = false) Long companyId,
                                  @RequestParam(required = false) String keywords){
-        List<CompanyUser> list = companyUserService.getAllUserListLimit(companyId,keywords);
+        List<CompanyUser> list = companyUserService.getAllUserListLimit(resolveCompanyId(companyId),keywords);
         return R.ok().put("data", list);
     }
+    @PreAuthorize("@ss.hasPermi('company:companyUser:list')")
     @GetMapping("/getUserListByDeptId")
     public R getUserListByDeptId(CompanyUser user)
     {
-
+        user.setCompanyId(resolveCompanyId(user.getCompanyId()));
         List<CompanyUser> list = companyUserService.getUserListByDeptId(user);
         return R.ok().put("data",list);
     }
@@ -130,11 +178,12 @@ public class CompanyUserController extends BaseController
      * @param companyId
      * @return
      */
+    @PreAuthorize("@ss.hasPermi('company:companyUser:list')")
     @GetMapping("/getUserList")
     public R getUserList(@RequestParam("companyId") Long companyId)
     {
         CompanyUser map=new CompanyUser();
-        map.setCompanyId(companyId);
+        map.setCompanyId(resolveCompanyId(companyId));
         List<CompanyUser> list = companyUserService.selectCompanyUserList(map);
         return  R.ok().put("data",list);
     }
@@ -148,17 +197,18 @@ public class CompanyUserController extends BaseController
     @PostMapping("/changeCompanyUser")
     public AjaxResult changeCompanyUser(@RequestBody List<UserProjectDTO> users, @RequestParam Long companyUserId, @RequestParam Long companyId)
     {
-        return toAjax(companyUserService.changeCompanyUser(users, companyUserId, companyId));
+        return toAjax(companyUserService.changeCompanyUser(users, companyUserId, resolveCompanyId(companyId)));
     }
     /**
      * 根据登录的用户公司获取所有的销售
      * @return
      */
+    @PreAuthorize("@ss.hasPermi('company:companyUser:list')")
     @GetMapping("/getCompanyUserList")
     public R getCompanyUserList(@RequestParam Long companyId)
     {
         CompanyUser cu = new CompanyUser();
-        cu.setCompanyId(companyId);
+        cu.setCompanyId(resolveCompanyId(companyId));
         List<CompanyUser> list = companyUserService.selectCompanyUserList(cu);
         return  R.ok().put("data",list);
     }
@@ -169,6 +219,7 @@ public class CompanyUserController extends BaseController
     @GetMapping("/getCompanyUserListPage")
     public TableDataInfo getCompanyUserListPage(CompanyUser user)
     {
+        user.setCompanyId(resolveCompanyId(user.getCompanyId()));
         startPage();
         List<CompanyUser> list = companyUserService.getCompanyUserList(user);
         return getDataTable(list);

+ 8 - 2
fs-admin/src/main/java/com/fs/third/controller/FeiyuCustomerController.java

@@ -28,6 +28,7 @@ import com.fs.third.http.HttpUtils;
 import com.fs.third.param.FeiyuCluesParam;
 import com.fs.third.param.FeiyuImportCluesParam;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.web.bind.annotation.*;
 
 import java.text.SimpleDateFormat;
@@ -41,8 +42,13 @@ import java.util.concurrent.TimeUnit;
 @RequestMapping("/third/feiyu")
 public class FeiyuCustomerController extends BaseController {
 
-    public static String  appId="1770748979316843";
-    public static String  appSecret="106c17623dd0eaba37dab935b9839c698c0876d1";
+    /** 飞鱼开放平台 appId,可通过配置/环境变量覆盖 */
+    @Value("${feiyu.app-id:1770748979316843}")
+    private String appId;
+
+    /** 飞鱼开放平台 appSecret,可通过配置/环境变量覆盖 */
+    @Value("${feiyu.app-secret:106c17623dd0eaba37dab935b9839c698c0876d1}")
+    private String appSecret;
 
     public static String open_api_url_prefix = "https://ad.oceanengine.com/open_api/";
     public static String uri = "oauth2/access_token/";

+ 23 - 0
fs-admin/src/main/java/com/fs/web/controller/common/CommonController.java

@@ -14,6 +14,8 @@ import com.fs.his.service.IFsExportTaskService;
 import com.fs.im.service.OpenIMService;
 import com.fs.system.oss.CloudStorageService;
 import com.fs.system.oss.OSSFactory;
+import com.fs.system.oss.ObsStsService;
+import com.fs.system.oss.ObsStsVO;
 
 import com.fs.web.vo.WangUploadVO;
 
@@ -71,6 +73,9 @@ public class CommonController
     @Autowired
     public RedisTemplate redisTemplate;
 
+    @Autowired
+    private ObsStsService obsStsService;
+
     org.slf4j.Logger logger= LoggerFactory.getLogger(getClass());
     @GetMapping(value = "common/getTask/{taskId}")
     public R getTask(@PathVariable("taskId") Long taskId)
@@ -253,6 +258,24 @@ public class CommonController
         return tencentCloudCosService.getKeyAndCredentials();
     }
 
+    /**
+     * 获取华为云 OBS 上传凭证(后端持有 AK/SK,避免前端长期密钥泄露)
+     */
+    @GetMapping("/common/sts/obs")
+    public R getObsSts()
+    {
+        try
+        {
+            ObsStsVO sts = obsStsService.generateTempCredential();
+            return R.ok().put("data", sts);
+        }
+        catch (Exception e)
+        {
+            log.error("获取 OBS 凭证失败", e);
+            return R.error(e.getMessage());
+        }
+    }
+
     /**
      * 测试接口
      */

+ 17 - 3
fs-admin/src/main/java/com/fs/web/controller/system/SysConfigController.java

@@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.PutMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import com.fs.common.annotation.Log;
 import com.fs.common.annotation.RepeatSubmit;
@@ -24,6 +25,7 @@ import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.service.ISysConfigService;
@@ -140,10 +142,22 @@ public class SysConfigController extends BaseController
         return AjaxResult.success();
     }
 
-    @GetMapping(value = "/getConfigByKey/{configKey}")
-    public AjaxResult getConfigByKey(@PathVariable String configKey)
+    /**
+     * 按键名查询参数。
+     * 优先使用 query 参数 configKey,避免路径中带多段点号(如 sys.application.config)被 WAF 误拦。
+     * 仍兼容旧路径 /getConfigByKey/{configKey}。
+     */
+    @GetMapping(value = {"/getConfigByKey", "/getConfigByKey/{configKey}"})
+    public AjaxResult getConfigByKey(
+            @PathVariable(value = "configKey", required = false) String configKeyPath,
+            @RequestParam(value = "configKey", required = false) String configKeyParam)
     {
-        SysConfig config=configService.selectConfigByConfigKey(configKey);
+        String configKey = StringUtils.isNotEmpty(configKeyParam) ? configKeyParam : configKeyPath;
+        if (StringUtils.isEmpty(configKey))
+        {
+            return AjaxResult.error("参数键名不能为空");
+        }
+        SysConfig config = configService.selectConfigByConfigKey(configKey);
         return AjaxResult.success(config);
     }
 

+ 1 - 0
fs-admin/src/main/java/com/fs/web/controller/system/SysUserController.java

@@ -231,6 +231,7 @@ public class SysUserController extends BaseController
     public AjaxResult resetPwd(@RequestBody SysUser user)
     {
         userService.checkUserAllowed(user);
+        userService.checkUserDataScope(user.getUserId());
         user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
         user.setUpdateBy(getUsername());
         return toAjax(userService.resetPwd(user));

+ 2 - 2
fs-admin/src/main/resources/application.yml

@@ -4,8 +4,8 @@ server:
 # Spring配置
 spring:
   profiles:
-#    active: dev-yjb
-    active: druid-yjb-test #医健宝测试库
+    active: dev-yjb
+#    active: druid-yjb-test #医健宝测试库
 #    active: druid-hdt
 #    active: druid-yzt
 #    active: druid-sxjz

+ 2 - 2
fs-admin/src/main/resources/logback.xml

@@ -72,13 +72,13 @@
     </appender>
 
 	<!-- 系统模块日志级别控制  -->
-	<logger name="com.fs" level="debug" />
+	<logger name="com.fs" level="info" />
 	<!-- Spring日志级别控制  -->
 	<logger name="org.springframework" level="warn" />
 
     <!-- log4j2.xml -->
     <Logger name="com.fs.hisStore.mapper" level="debug"/>
-    <Logger name="org.apache.ibatis" level="debug"/>
+    <Logger name="org.apache.ibatis" level="warn"/>
 
 
     <root level="info">

+ 1 - 1
fs-common-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 0 - 1
fs-common-api/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -35,7 +35,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
     protected void configure(HttpSecurity http) throws Exception
     {
         http.authorizeRequests()
-                .antMatchers("/**").permitAll()
                 .antMatchers("/app/crmCustomer/**").anonymous()
                 .anyRequest().authenticated()
                 .and().csrf().disable();

+ 36 - 2
fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java

@@ -1,11 +1,16 @@
 package com.fs.common.core.redis;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.BoundSetOperations;
+import org.springframework.data.redis.core.Cursor;
 import org.springframework.data.redis.core.HashOperations;
+import org.springframework.data.redis.core.RedisCallback;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ScanOptions;
 import org.springframework.data.redis.core.ValueOperations;
 import org.springframework.stereotype.Component;
 
@@ -218,14 +223,43 @@ public class RedisCache
     }
 
     /**
-     * 获得缓存的基本对象列表
+     * 获得缓存的基本对象列表(内部使用 SCAN,避免 KEYS 阻塞)
      *
      * @param pattern 字符串前缀
      * @return 对象列表
      */
     public Collection<String> keys(final String pattern)
     {
-        return redisTemplate.keys(pattern);
+        return scanKeys(pattern);
+    }
+
+    /**
+     * 使用 SCAN 按模式匹配 key,避免 KEYS 命令阻塞 Redis
+     *
+     * @param pattern 匹配模式
+     * @return 匹配到的 key 集合
+     */
+    public Collection<String> scanKeys(final String pattern)
+    {
+        Set<String> keys = new HashSet<>();
+        redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
+            ScanOptions options = ScanOptions.scanOptions().match(pattern).count(1000).build();
+            Cursor<byte[]> cursor = connection.scan(options);
+            try {
+                while (cursor.hasNext()) {
+                    // Key 使用 StringRedisSerializer(UTF-8)存储
+                    keys.add(new String(cursor.next(), StandardCharsets.UTF_8));
+                }
+            } finally {
+                try {
+                    cursor.close();
+                } catch (IOException e) {
+                    // 忽略关闭异常
+                }
+            }
+            return keys;
+        });
+        return keys;
     }
 
 

+ 21 - 1
fs-common/src/main/java/com/fs/common/sharding/ShardingUtil.java

@@ -15,6 +15,9 @@ import java.util.List;
  *   <li>总后台等需要查全部数据时,调用 {@link #allShards()} 获取所有分表后缀</li>
  * </ul>
  *
+ * <p>安全说明:Mapper 中表名使用 ${shard} 拼接,必须保证 shard 仅为 0~9 的数字;
+ * 调用层请使用本工具类计算/校验,禁止将用户原始入参直接作为 shard。</p>
+ *
  * @author fs
  */
 public final class ShardingUtil
@@ -24,6 +27,22 @@ public final class ShardingUtil
 
     private ShardingUtil() {}
 
+    /**
+     * 校验分表后缀仅为 0~9,防止 ${shard} 拼接注入
+     *
+     * @param shard 分表后缀
+     * @return 校验通过的分表后缀
+     * @throws IllegalArgumentException 非法分表后缀
+     */
+    public static int validateShard(int shard)
+    {
+        if (shard < 0 || shard >= SHARD_COUNT)
+        {
+            throw new IllegalArgumentException("非法分表后缀 shard=" + shard + ",仅允许 0~" + (SHARD_COUNT - 1));
+        }
+        return shard;
+    }
+
     /**
      * 根据 userId 计算分表后缀
      *
@@ -36,7 +55,8 @@ public final class ShardingUtil
         {
             throw new IllegalArgumentException("userId 不能为空,无法计算分表");
         }
-        return (int) (userId % SHARD_COUNT);
+        // 取模结果再经校验,确保传入 Mapper 的 shard 恒为 0~9
+        return validateShard((int) (Math.floorMod(userId, (long) SHARD_COUNT)));
     }
 
     /**

+ 0 - 3
fs-common/src/main/java/com/fs/common/utils/SecurityUtils.java

@@ -118,7 +118,4 @@ public class SecurityUtils
         return userId != null && 1L == userId;
     }
 
-    public static void main(String[] args) {
-        System.out.println(encryptPassword("admin123.."));
-    }
 }

+ 124 - 0
fs-common/src/main/java/com/fs/common/utils/UrlSecurityUtils.java

@@ -0,0 +1,124 @@
+package com.fs.common.utils;
+
+import com.fs.common.exception.CustomException;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.Locale;
+
+/**
+ * URL安全校验工具(防SSRF):禁止内网/本机/链路本地/云元数据,可选域名白名单。
+ */
+public final class UrlSecurityUtils {
+
+    /** 云元数据常见地址 */
+    private static final String CLOUD_METADATA_IP = "169.254.169.254";
+
+    private UrlSecurityUtils() {
+    }
+
+    /**
+     * 校验URL:禁止危险地址(内网、127、169.254、云元数据等),不强制域名白名单。
+     *
+     * @param url 待校验URL
+     * @throws CustomException URL非法或指向禁止地址
+     */
+    public static void validate(String url) {
+        validate(url, null);
+    }
+
+    /**
+     * 校验URL;若allowedHosts非空,则host必须在白名单内(精确匹配或后缀匹配)。
+     *
+     * @param url          待校验URL
+     * @param allowedHosts 可选域名白名单;null/空表示不校验域名,仍拦截内网/元数据等
+     * @throws CustomException URL非法或指向禁止地址
+     */
+    public static void validate(String url, Collection<String> allowedHosts) {
+        if (StringUtils.isEmpty(url)) {
+            throw new CustomException("URL不能为空");
+        }
+        URI uri;
+        try {
+            uri = new URI(url.trim());
+        } catch (Exception e) {
+            throw new CustomException("无效的URL格式");
+        }
+        String scheme = uri.getScheme();
+        if (scheme == null || (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme))) {
+            throw new CustomException("仅允许http/https协议");
+        }
+        String host = uri.getHost();
+        if (StringUtils.isEmpty(host)) {
+            throw new CustomException("无效的URL主机");
+        }
+        String hostLower = host.toLowerCase(Locale.ROOT);
+
+        // 可选:域名白名单
+        if (allowedHosts != null && !allowedHosts.isEmpty()) {
+            boolean matched = false;
+            for (String allowed : allowedHosts) {
+                if (StringUtils.isEmpty(allowed)) {
+                    continue;
+                }
+                String a = allowed.toLowerCase(Locale.ROOT).trim();
+                if (hostLower.equals(a) || hostLower.endsWith("." + a)) {
+                    matched = true;
+                    break;
+                }
+            }
+            if (!matched) {
+                throw new CustomException("主机不在白名单列表: " + host);
+            }
+        }
+
+        // 云元数据 / 链路本地直接拒绝
+        if (CLOUD_METADATA_IP.equals(hostLower) || hostLower.startsWith("169.254.")) {
+            throw new CustomException("禁止访问云元数据或链路本地地址");
+        }
+        if ("localhost".equals(hostLower) || hostLower.endsWith(".localhost")) {
+            throw new CustomException("禁止访问本机地址");
+        }
+
+        // 解析IP:禁止内网/站点本地/链路本地/组播等
+        try {
+            InetAddress[] addresses = InetAddress.getAllByName(host);
+            for (InetAddress address : addresses) {
+                if (isForbiddenAddress(address)) {
+                    throw new CustomException("禁止访问内网或保留地址: " + host);
+                }
+            }
+        } catch (UnknownHostException e) {
+            throw new CustomException("无法解析的URL主机: " + host);
+        }
+    }
+
+    /**
+     * 判断是否为禁止访问的地址(内网、站点本地、链路本地、组播等)
+     */
+    private static boolean isForbiddenAddress(InetAddress address) {
+        if (address == null) {
+            return true;
+        }
+        if (address.isAnyLocalAddress()
+                || address.isLoopbackAddress()
+                || address.isLinkLocalAddress()
+                || address.isSiteLocalAddress()
+                || address.isMulticastAddress()) {
+            return true;
+        }
+        String ip = address.getHostAddress();
+        if (ip != null) {
+            if (ip.startsWith("127.") || ip.startsWith("0.") || CLOUD_METADATA_IP.equals(ip) || ip.startsWith("169.254.")) {
+                return true;
+            }
+            String lower = ip.toLowerCase(Locale.ROOT);
+            if ("::1".equals(lower) || lower.startsWith("fc") || lower.startsWith("fd") || lower.startsWith("fe80:")) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

+ 32 - 1
fs-common/src/main/java/com/fs/common/utils/file/FileUploadUtils.java

@@ -2,6 +2,7 @@ package com.fs.common.utils.file;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import org.apache.commons.io.FilenameUtils;
 import org.springframework.web.multipart.MultipartFile;
 import com.fs.common.config.FSConfig;
@@ -156,9 +157,10 @@ public class FileUploadUtils
      * @return
      * @throws FileSizeLimitExceededException 如果超出最大大小
      * @throws InvalidExtensionException
+     * @throws IOException 魔数校验失败或读取失败
      */
     public static final void assertAllowed(MultipartFile file, String[] allowedExtension)
-            throws FileSizeLimitExceededException, InvalidExtensionException
+            throws FileSizeLimitExceededException, InvalidExtensionException, IOException
     {
         long size = file.getSize();
         if (DEFAULT_MAX_SIZE != -1 && size > DEFAULT_MAX_SIZE)
@@ -196,6 +198,35 @@ public class FileUploadUtils
             }
         }
 
+        // 扩展名通过后再做轻量魔数校验(jpg/png/gif/pdf)
+        assertMagicBytes(file, extension);
+    }
+
+    /**
+     * 校验文件内容魔数与扩展名是否一致(仅 jpg/jpeg/png/gif/pdf)
+     */
+    public static final void assertMagicBytes(MultipartFile file, String extension) throws IOException
+    {
+        if (StringUtils.isEmpty(extension))
+        {
+            return;
+        }
+        String ext = extension.toLowerCase();
+        if (!"jpg".equals(ext) && !"jpeg".equals(ext) && !"png".equals(ext)
+                && !"gif".equals(ext) && !"pdf".equals(ext))
+        {
+            return;
+        }
+        byte[] header = new byte[8];
+        int read;
+        try (InputStream in = file.getInputStream())
+        {
+            read = in.read(header);
+        }
+        if (read < 4 || !MimeTypeUtils.checkMagicBytes(header, ext))
+        {
+            throw new IOException("文件内容与扩展名不匹配,疑似伪造类型");
+        }
     }
 
     /**

+ 35 - 0
fs-common/src/main/java/com/fs/common/utils/file/MimeTypeUtils.java

@@ -56,4 +56,39 @@ public class MimeTypeUtils
                 return "";
         }
     }
+
+    /**
+     * 轻量文件头(魔数)校验:仅对 jpg/jpeg/png/gif/pdf 校验,其他扩展名直接通过。
+     *
+     * @param header    文件开头若干字节
+     * @param extension 扩展名(不含点)
+     * @return true 表示通过或无需校验
+     */
+    public static boolean checkMagicBytes(byte[] header, String extension)
+    {
+        if (extension == null || header == null || header.length < 4)
+        {
+            return extension == null;
+        }
+        String ext = extension.toLowerCase();
+        switch (ext)
+        {
+            case "jpg":
+            case "jpeg":
+                // JPEG: FF D8
+                return (header[0] & 0xFF) == 0xFF && (header[1] & 0xFF) == 0xD8;
+            case "png":
+                // PNG: 89 50 4E 47
+                return (header[0] & 0xFF) == 0x89 && header[1] == 0x50 && header[2] == 0x4E && header[3] == 0x47;
+            case "gif":
+                // GIF: GIF8
+                return header[0] == 'G' && header[1] == 'I' && header[2] == 'F';
+            case "pdf":
+                // PDF: %PDF
+                return header[0] == '%' && header[1] == 'P' && header[2] == 'D' && header[3] == 'F';
+            default:
+                // 未纳入魔数校验的类型放行
+                return true;
+        }
+    }
 }

+ 2 - 0
fs-company-app/src/main/java/com/fs/app/controller/CommonController.java

@@ -126,6 +126,7 @@ public class CommonController extends AppBaseController {
 	/**
 	 * 通用上传请求
 	 */
+	@Login
 	@ApiOperation("上传")
 	@PostMapping("/upload")
 	public R uploadFile(MultipartFile file) throws Exception
@@ -211,6 +212,7 @@ public class CommonController extends AppBaseController {
 		return R.ok().put("data",list);
 	}
 
+	@Login
 	@ApiOperation("上传")
 	@PostMapping("uploadOSS")
 	public R uploadOSS(@RequestParam("file") MultipartFile file) throws Exception

+ 5 - 0
fs-company-app/src/main/java/com/fs/app/controller/CompanyUserController.java

@@ -16,6 +16,7 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.PatternUtils;
+import com.fs.common.utils.UrlSecurityUtils;
 import com.fs.common.utils.bean.BeanUtils;
 import com.fs.company.domain.*;
 import com.fs.company.mapper.CompanyRoleMapper;
@@ -474,6 +475,8 @@ public class CompanyUserController extends AppBaseController {
         if(userId==null){
             return R.error(403,"用户失效");
         }
+        // SSRF防护:校验声纹URL
+        UrlSecurityUtils.validate(param.getVoicePrintUrl());
         CompanyUser companyUser = new CompanyUser();
         companyUser.setUserId(userId);
         companyUser.setVoicePrintUrl(param.getVoicePrintUrl());
@@ -541,6 +544,8 @@ public class CompanyUserController extends AppBaseController {
             }
         }
 
+        // SSRF防护:校验语音URL
+        UrlSecurityUtils.validate(userVoiceUrl);
         QwSopTempVoice qwSopTempVoice = voiceService.selectQwSopTempVoiceByIdAndUserVoiceUrl(id);
         if(qwSopTempVoice != null && qwSopTempVoice.getId() != null){
             audioVO = AudioUtils.createVoiceUrl(qwSopTempVoice.getCompanyUserId(), userVoiceUrl);

+ 11 - 0
fs-company-app/src/main/java/com/fs/app/controller/FsUserController.java

@@ -140,6 +140,13 @@ public class FsUserController extends AppBaseController {
     @ApiOperation("批量禁用会员")
     public ResponseResult<Boolean> disabledUser(@ApiParam(value = "联系人id集合", required = true) @RequestBody List<Long> userCompanyUserIds) {
         log.debug("批量禁用会员 ids: {}", JSON.toJSONString(userCompanyUserIds));
+        Long companyId = getCompanyId();
+        for (Long id : userCompanyUserIds) {
+            FsUserCompanyUser ucUser = userCompanyUserService.selectFsUserCompanyUserById(id);
+            if (ucUser == null || !companyId.equals(ucUser.getCompanyId())) {
+                throw new ServiceException("无权操作该会员数据");
+            }
+        }
         Boolean r = userCompanyUserService.batchUpdateStatus(userCompanyUserIds, 2);
         return ResponseResult.ok(r);
     }
@@ -267,6 +274,10 @@ public class FsUserController extends AppBaseController {
         if (Objects.isNull(fsUser)) {
             throw new ServiceException("用户不存在");
         }
+        Long companyId = getCompanyId();
+        if (fsUser.getCompanyId() != null && !companyId.equals(fsUser.getCompanyId())) {
+            throw new ServiceException("无权修改该用户信息");
+        }
 
         fsUser.setNickName(param.getNickName());
         fsUser.setRemark(param.getRemark());

+ 7 - 0
fs-company-app/src/main/java/com/fs/app/controller/UserController.java

@@ -544,6 +544,13 @@ public class UserController extends AppBaseController {
     public R delMyCard(@RequestParam("cardId") Long cardId) {
         try {
             CompanyUserCard companyUserCard = companyUserCardService.selectCompanyUserCardById(cardId);
+            if (companyUserCard == null) {
+                return R.error("名片不存在");
+            }
+            // 验证名片归属:只能删除自己的名片
+            if (!getUserId().equals(companyUserCard.getUserId().toString())) {
+                return R.error("无权操作该名片");
+            }
             String shortUrl = companyUserCard.getShortUrl();
             String[] chainArr = shortUrl.split(".cn/");
             String chain = "";

+ 1 - 3
fs-company-app/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java

@@ -39,10 +39,8 @@ public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
             return true;
         }
 
+        // 仅从请求头读取 Token,禁止 URL/表单参数回退(防 Token 泄露到日志与 Referer)
         String token = request.getHeader(jwtUtils.getHeader());
-        if (StringUtils.isBlank(token)) {
-            token = request.getParameter(jwtUtils.getHeader());
-        }
         if (StringUtils.isBlank(token)) {
             throw new FSException(jwtUtils.getHeader() + "不能为空", HttpStatus.UNAUTHORIZED.value());
         }

+ 13 - 0
fs-company-app/src/main/java/com/fs/app/utils/JwtUtils.java

@@ -11,11 +11,13 @@ import org.slf4j.LoggerFactory;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;
 
+import javax.annotation.PostConstruct;
 import java.util.Date;
 import java.util.concurrent.TimeUnit;
 
 /**
  * 公司 App JWT;{@link #APP_REDIS_TOKEN_KEY_PREFIX} 仅 /app/user/login 登录写入,用于 Redis 滑动续期。
+ * 密钥从配置 fs.jwt.secret(环境变量 FS_JWT_SECRET / Nacos)读取,无硬编码弱默认值。
  */
 @ConfigurationProperties(prefix = "fs.jwt")
 @Component
@@ -26,11 +28,22 @@ public class JwtUtils {
 
     private final Logger logger = LoggerFactory.getLogger(getClass());
 
+    /** 从 fs.jwt.secret 注入,禁止在代码中硬编码弱密钥 */
     private String secret;
     /** 会话滑动时长(秒),默认 7 天 */
     private long expire = 604800L;
     private String header;
 
+    /**
+     * 启动时检查密钥强度:过短或未配置仅告警,不拒绝启动
+     */
+    @PostConstruct
+    public void checkSecret() {
+        if (secret == null || secret.length() < 32) {
+            logger.warn("fs.jwt.secret 未配置或长度不足32,存在伪造Token风险,生产环境请通过 FS_JWT_SECRET / Nacos 覆盖");
+        }
+    }
+
     public String generateToken(long userId) {
         Date nowDate = new Date();
         Date expireDate = new Date(nowDate.getTime() + expire * 1000);

+ 1 - 1
fs-company-app/src/main/java/com/fs/core/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 21 - 9
fs-company-app/src/main/java/com/fs/core/config/ResourcesConfig.java

@@ -4,6 +4,7 @@ import com.fs.common.config.FSConfig;
 import com.fs.common.constant.Constants;
 import com.fs.core.interceptor.RepeatSubmitInterceptor;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.cors.CorsConfiguration;
@@ -24,6 +25,10 @@ public class ResourcesConfig implements WebMvcConfigurer
     @Autowired
     private RepeatSubmitInterceptor repeatSubmitInterceptor;
 
+    /** CORS允许来源,逗号分隔多域名,默认*;生产建议通过CORS_ALLOWED_ORIGINS/Nacos配置具体域名 */
+    @Value("${cors.allowed-origins:*}")
+    private String allowedOrigins;
+
     @Override
     public void addResourceHandlers(ResourceHandlerRegistry registry)
     {
@@ -45,29 +50,37 @@ public class ResourcesConfig implements WebMvcConfigurer
     }
 
     /**
-     * 跨域配置
+     * 跨域配置:从 cors.allowed-origins 读取,支持逗号分隔多域名
      */
     @Bean
     public CorsFilter corsFilter() {
         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
         CorsConfiguration config = new CorsConfiguration();
         config.setAllowCredentials(true);
-        // 设置访问源地址
-        config.addAllowedOrigin("*");
-        // 设置访问源请求头
+        if (allowedOrigins != null) {
+            for (String origin : allowedOrigins.split(",")) {
+                String trimmed = origin.trim();
+                if (!trimmed.isEmpty()) {
+                    config.addAllowedOrigin(trimmed);
+                }
+            }
+        }
+        if (config.getAllowedOrigins() == null || config.getAllowedOrigins().isEmpty()) {
+            config.addAllowedOrigin("*");
+        }
         config.addAllowedHeader("*");
-        // 设置访问源请求方法
         config.addAllowedMethod("*");
-        // 对接口配置跨域设置
         source.registerCorsConfiguration("/**", config);
         return new CorsFilter(source);
     }
 
     @Override
     public void addCorsMappings(CorsRegistry registry) {
+        String[] patterns = (allowedOrigins == null || allowedOrigins.trim().isEmpty())
+                ? new String[]{"*"}
+                : allowedOrigins.split("\\s*,\\s*");
         registry.addMapping("/**")
-                .allowedOrigins("*")
-                //.allowedOriginPatterns("*")
+                .allowedOrigins(patterns)
                 .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                 .allowCredentials(true)
                 .maxAge(3600)
@@ -75,4 +88,3 @@ public class ResourcesConfig implements WebMvcConfigurer
     }
 
 }
-

+ 23 - 0
fs-company/src/main/java/com/fs/company/controller/common/CommonController.java

@@ -24,6 +24,8 @@ import com.fs.qw.service.IQwWorkTaskService;
 import com.fs.qwApi.service.QwApiService;
 import com.fs.system.oss.CloudStorageService;
 import com.fs.system.oss.OSSFactory;
+import com.fs.system.oss.ObsStsService;
+import com.fs.system.oss.ObsStsVO;
 import com.fs.system.service.ISysConfigService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -70,6 +72,9 @@ public class CommonController
 
     @Autowired
     private TokenService tokenService;
+
+    @Autowired
+    private ObsStsService obsStsService;
 //    @Autowired
 //    private TestTwoService testService2;
 ////
@@ -301,6 +306,24 @@ public class CommonController
         return AjaxResult.success(inquirySubType);
     }
 
+    /**
+     * 获取华为云 OBS 上传凭证(后端持有 AK/SK,避免前端长期密钥泄露)
+     */
+    @GetMapping("/common/sts/obs")
+    public R getObsSts()
+    {
+        try
+        {
+            ObsStsVO sts = obsStsService.generateTempCredential();
+            return R.ok().put("data", sts);
+        }
+        catch (Exception e)
+        {
+            log.error("获取 OBS 凭证失败", e);
+            return R.error(e.getMessage());
+        }
+    }
+
     //分享会诊
     @PostMapping(value = "/common/sendInquiry")
     public AjaxResult sendInquiry(@RequestBody Map<String,String> map){

+ 7 - 0
fs-company/src/main/java/com/fs/company/controller/store/FsUserController.java

@@ -180,6 +180,13 @@ public class FsUserController extends BaseController
     public AjaxResult getInfo(@PathVariable("userId") Long userId)
     {
         FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+        if (fsUser == null) {
+            return AjaxResult.error("用户不存在");
+        }
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (!loginUser.getCompany().getCompanyId().equals(fsUser.getCompanyId())) {
+            return AjaxResult.error("无权访问该用户数据");
+        }
         fsUser.setPhone(decryptAutoPhoneMk(fsUser.getPhone()));
         return AjaxResult.success(fsUser);
     }

+ 25 - 18
fs-company/src/main/java/com/fs/company/utils/AudioUtils.java

@@ -157,28 +157,35 @@ public class AudioUtils {
      * -quiet               : Print only some basic values
      * -tencent             : Compatible with QQ/Wechat
      */
+    /**
+     * 校验路径,拒绝命令注入相关特殊字符
+     */
+    private static void validateExecPath(String... paths) {
+        if (paths == null) {
+            return;
+        }
+        for (String p : paths) {
+            if (p != null && p.matches(".*[&|;>`$].*")) {
+                throw new ServiceException("非法路径字符");
+            }
+        }
+    }
+
     public static void transferPcmSilk(String pcmPath, String target) {
         Process process = null;
         try {
-            /**
-             // 1、这一节的,语音长度太长会使音频长度丢失
-             List<String> commend = new ArrayList<>();
-             // 指令,可参照方法注释, 请不要在commend.add()里同时写【-参数 值】
-             commend.add(path + "silk_v3_encoder.exe");
-             commend.add(pcmPath);
-             commend.add(target);
-             commend.add("-tencent");
-             ProcessBuilder builder = new ProcessBuilder();
-             builder.command(commend);
-             process = builder.start();
-             // 如果删除下班这行写process.waitFor() ,太长的语音会阻塞,BufferedReader 打印出来太长的语音也会阻塞
-             process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
-             */
-            // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
-            process = Runtime.getRuntime().exec("cmd /c start " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
-            process .waitFor();
+            // 使用 ProcessBuilder 数组参数,避免 Runtime.exec 字符串拼接带来的命令注入风险
+            validateExecPath(path, pcmPath, target);
+            List<String> command = new ArrayList<>();
+            command.add(path + "silk_v3_encoder.exe");
+            command.add(pcmPath);
+            command.add(target);
+            command.add("-tencent");
+            ProcessBuilder builder = new ProcessBuilder(command);
+            builder.redirectErrorStream(true);
+            process = builder.start();
+            process.waitFor();
             Thread.sleep(1000);
-            // 有更好的方法会后续慢慢更新..
         } catch (Exception e) {
             e.printStackTrace();
         } finally {

+ 1 - 1
fs-company/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 5
fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -118,13 +118,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/msg/**/**").anonymous()
                 .antMatchers("/msg").anonymous()
                 .antMatchers("/common/getId**").anonymous()
-                .antMatchers("/common/uploadOSS**").anonymous()
-                .antMatchers("/company/user/common/uploadOSS").anonymous()
                 .antMatchers("/pay/wxPay/payNotify**").anonymous()
-                .antMatchers("/common/uploadWang**").anonymous()
-                .antMatchers("/common/download**").anonymous()
                 .antMatchers("/common/test").anonymous()
-                .antMatchers("/common/download/resource**").anonymous()
+                // 以下 swagger 路径仅便于本地/开发调试;生产环境 swagger.enabled=false,文档本身不启用,故保留放行以免本地调试挂掉
                 .antMatchers("/swagger-ui.html").anonymous()
                 .antMatchers("/swagger-resources/**").anonymous()
                 .antMatchers("/webjars/**").anonymous()

+ 0 - 3
fs-company/src/main/java/com/fs/framework/security/SecurityUtils.java

@@ -87,7 +87,4 @@ public class SecurityUtils
         return userId != null && 1L == userId;
     }
 
-    public static void main(String[] args) {
-        System.out.println(encryptPassword("ylrz147.."));
-    }
 }

+ 2 - 1
fs-company/src/main/resources/logback.xml

@@ -72,9 +72,10 @@
     </appender>
 
 	<!-- 系统模块日志级别控制  -->
-	<logger name="com.fs" level="debug" />
+	<logger name="com.fs" level="info" />
 	<!-- Spring日志级别控制  -->
 	<logger name="org.springframework" level="warn" />
+	<logger name="org.apache.ibatis" level="warn" />
 
 	<root level="info">
 		<appender-ref ref="console" />

+ 1 - 0
fs-doctor-app/src/main/java/com/fs/app/controller/CommonController.java

@@ -159,6 +159,7 @@ public class CommonController {
 
 
 
+	@Login
 	@PostMapping("uploadOSS")
 	public R uploadOSS(@RequestParam("file") MultipartFile file) throws Exception
 	{

+ 1 - 0
fs-doctor-app/src/main/java/com/fs/app/controller/InquiryOrderController.java

@@ -217,6 +217,7 @@ public class InquiryOrderController extends  AppBaseController {
 
 
 
+    @Login
     @PutMapping("/updateRemark")
     public R edit(@RequestBody FsInquiryOrder fsInquiryOrder)
     {

+ 1 - 4
fs-doctor-app/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java

@@ -40,11 +40,8 @@ public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
             return true;
         }
 
-        //获取用户凭证
+        // 仅从请求头读取 Token,禁止 URL/表单参数回退(防 Token 泄露到日志与 Referer)
         String token = request.getHeader(jwtUtils.getHeader());
-        if(StringUtils.isBlank(token)){
-            token = request.getParameter(jwtUtils.getHeader());
-        }
 
         //凭证为空
         if(StringUtils.isBlank(token)){

+ 13 - 3
fs-doctor-app/src/main/java/com/fs/app/utils/JwtUtils.java

@@ -8,22 +8,32 @@ import org.slf4j.LoggerFactory;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;
 
+import javax.annotation.PostConstruct;
 import java.util.Date;
 
 /**
- * jwt工具类
-
+ * jwt工具类;密钥从 fs.jwt.secret(FS_JWT_SECRET / Nacos)读取,无硬编码弱默认值。
  */
 @ConfigurationProperties(prefix = "fs.jwt")
 @Component
 public class JwtUtils {
     private Logger logger = LoggerFactory.getLogger(getClass());
 
-
+    /** 从 fs.jwt.secret 注入,禁止在代码中硬编码弱密钥 */
     private String secret;
     private long expire;
     private String header;
 
+    /**
+     * 启动时检查密钥强度:过短或未配置仅告警,不拒绝启动
+     */
+    @PostConstruct
+    public void checkSecret() {
+        if (secret == null || secret.length() < 32) {
+            logger.warn("fs.jwt.secret 未配置或长度不足32,存在伪造Token风险,生产环境请通过 FS_JWT_SECRET / Nacos 覆盖");
+        }
+    }
+
     /**
      * 生成jwt token
      */

+ 1 - 1
fs-doctor-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-framework/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -27,7 +27,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 18 - 3
fs-framework/src/main/java/com/fs/framework/config/ResourcesConfig.java

@@ -1,6 +1,7 @@
 package com.fs.framework.config;
 
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.format.FormatterRegistry;
@@ -30,6 +31,10 @@ public class ResourcesConfig implements WebMvcConfigurer
     @Autowired
     private LogInterceptor logInterceptor;
 
+    /** CORS允许来源,逗号分隔多域名,默认*;生产建议通过CORS_ALLOWED_ORIGINS/Nacos配置具体域名 */
+    @Value("${cors.allowed-origins:*}")
+    private String allowedOrigins;
+
     @Override
     public void addResourceHandlers(ResourceHandlerRegistry registry)
     {
@@ -53,7 +58,7 @@ public class ResourcesConfig implements WebMvcConfigurer
     }
 
     /**
-     * 跨域配置
+     * 跨域配置:从 cors.allowed-origins 读取,支持逗号分隔多域名
      */
     @Bean
     public CorsFilter corsFilter()
@@ -61,8 +66,18 @@ public class ResourcesConfig implements WebMvcConfigurer
         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
         CorsConfiguration config = new CorsConfiguration();
         config.setAllowCredentials(true);
-        // 设置访问源地址
-        config.addAllowedOrigin("*");
+        // 设置访问源地址(使用 addAllowedOrigin(兼容 Spring Boot 2.2),避免 credentials+* 冲突)
+        if (allowedOrigins != null) {
+            for (String origin : allowedOrigins.split(",")) {
+                String trimmed = origin.trim();
+                if (!trimmed.isEmpty()) {
+                    config.addAllowedOrigin(trimmed);
+                }
+            }
+        }
+        if (config.getAllowedOrigins() == null || config.getAllowedOrigins().isEmpty()) {
+            config.addAllowedOrigin("*");
+        }
         // 设置访问源请求头
         config.addAllowedHeader("*");
         // 设置访问源请求方法

+ 3 - 7
fs-framework/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -132,17 +132,13 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/tzPay/*").anonymous()
                 .antMatchers("//his/pay/*").anonymous()
                 .antMatchers("/common/getId**").anonymous()
-                .antMatchers("/common/uploadOSS**").anonymous()
-                .antMatchers("/common/uploadImage**").permitAll()
-                .antMatchers("/chat/upload/uploadFile**").anonymous()
-                .antMatchers("/common/uploadWang**").anonymous()
-                .antMatchers("/common/download**").anonymous()
-                .antMatchers("/common/download/resource**").anonymous()
+                // 敏感上传/下载接口需认证,不得匿名放行
+                // 以下 swagger 路径仅便于本地/开发调试;生产环境 swagger.enabled=false,文档本身不启用,故保留放行以免本地调试挂掉
                 .antMatchers("/swagger-ui.html").anonymous()
                 .antMatchers("/swagger-resources/**").anonymous()
                 .antMatchers("/webjars/**").anonymous()
                 .antMatchers("/*/api-docs").anonymous()
-                .antMatchers("/druid/**").anonymous()
+                .antMatchers("/druid/**").authenticated()
                 .antMatchers("/course/userVideo/videoTranscode").anonymous()
                 .antMatchers("/erp/call/**").anonymous()
                 // 除上面外的所有请求全部需要鉴权认证

+ 2 - 2
fs-framework/src/main/java/com/fs/framework/web/exception/GlobalExceptionHandler.java

@@ -81,14 +81,14 @@ public class GlobalExceptionHandler
     }
 
     /**
-     * 系统异常
+     * 系统异常:对外隐藏详细信息,仅记录完整堆栈
      */
     @ExceptionHandler(Exception.class)
     public AjaxResult handleException(Exception e, HttpServletRequest request)
     {
         String requestURI = request.getRequestURI();
         log.error("请求地址'{}',发生系统异常.", requestURI, e);
-        return AjaxResult.error(e.getMessage());
+        return AjaxResult.error("系统繁忙,请稍后重试");
     }
 
     /**

+ 1 - 1
fs-ipad-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 0 - 4
fs-ipad-task/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -116,10 +116,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/msg").anonymous()
                 .antMatchers("/app/common/**").anonymous()
                 .antMatchers("/common/getId**").anonymous()
-                .antMatchers("/common/uploadOSS**").anonymous()
-                .antMatchers("/common/uploadWang**").anonymous()
-                .antMatchers("/common/download**").anonymous()
-                .antMatchers("/common/download/resource**").anonymous()
                 .antMatchers("/swagger-ui.html").anonymous()
                 .antMatchers("/swagger-resources/**").anonymous()
                 .antMatchers("/webjars/**").anonymous()

+ 1 - 1
fs-live-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-qw-api-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 0 - 4
fs-qw-api-msg/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -116,10 +116,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/msg").anonymous()
                 .antMatchers("/testSop").anonymous()
                 .antMatchers("/common/getId**").anonymous()
-                .antMatchers("/common/uploadOSS**").anonymous()
-                .antMatchers("/common/uploadWang**").anonymous()
-                .antMatchers("/common/download**").anonymous()
-                .antMatchers("/common/download/resource**").anonymous()
                 .antMatchers("/swagger-ui.html").anonymous()
                 .antMatchers("/swagger-resources/**").anonymous()
                 .antMatchers("/webjars/**").anonymous()

+ 4 - 0
fs-qw-api/src/main/java/com/fs/app/controller/QwController.java

@@ -103,6 +103,10 @@ public class QwController {
             // TODO: 解析出明文xml标签的内容进行处理
             // For example:
             DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+            // 禁用外部实体,防止 XXE
+            dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+            dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
+            dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
             DocumentBuilder db = dbf.newDocumentBuilder();
             StringReader sr = new StringReader(sMsg);
             InputSource is = new InputSource(sr);

+ 1 - 1
fs-qw-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 0 - 4
fs-qw-api/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -115,10 +115,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/msg").anonymous()
                 .antMatchers("/testSop").anonymous()
                 .antMatchers("/common/getId**").anonymous()
-                .antMatchers("/common/uploadOSS**").anonymous()
-                .antMatchers("/common/uploadWang**").anonymous()
-                .antMatchers("/common/download**").anonymous()
-                .antMatchers("/common/download/resource**").anonymous()
                 .antMatchers("/swagger-ui.html").anonymous()
                 .antMatchers("/swagger-resources/**").anonymous()
                 .antMatchers("/webjars/**").anonymous()

+ 23 - 0
fs-qw-company-api/src/main/java/com/fs/app/controller/QwController.java

@@ -33,9 +33,13 @@ import org.springframework.web.multipart.MultipartFile;
 
 import java.io.InputStream;
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
@@ -47,8 +51,25 @@ public class QwController {
     private final IQwCompanyService qwCompanyService;
     private final RedisCacheT<String> redisCache;
 
+    private static final Set<String> ALLOWED_HOSTS = new HashSet<>(Arrays.asList(
+        "qyapi.weixin.qq.com"
+    ));
+
+    private void validateUrl(String url) {
+        try {
+            URI uri = new URI(url);
+            String host = uri.getHost();
+            if (host == null || !ALLOWED_HOSTS.contains(host)) {
+                throw new FSException("不允许访问的域名: " + host);
+            }
+        } catch (URISyntaxException e) {
+            throw new FSException("无效的URL格式: " + url);
+        }
+    }
+
     @PostMapping("/post")
     public QwApiResult post(@RequestBody QwApiParam param) throws Exception {
+        validateUrl(param.getUrl());
         String uuid = UUID.randomUUID().toString();
         log.info("{},POST接口请求获取到数据:{}", uuid, JSON.toJSONString(param));
         QwCompany qwCompany = qwCompanyService.selectQwCompanyByCorpId(param.getCorpId());
@@ -78,6 +99,7 @@ public class QwController {
 
     @PostMapping("/get")
     public QwApiResult get(@RequestBody QwApiParam param) throws Exception {
+        validateUrl(param.getUrl());
         String uuid = UUID.randomUUID().toString();
         log.info("{},GET接口请求获取到数据:{}", uuid, JSON.toJSONString(param));
         QwCompany qwCompany = qwCompanyService.selectQwCompanyByCorpId(param.getCorpId());
@@ -103,6 +125,7 @@ public class QwController {
 
     @PostMapping("/uploadImg")
     public QwApiResult uploadImg(@RequestBody QwApiParam param) throws Exception {
+        validateUrl(param.getUrl());
         String uuid = UUID.randomUUID().toString();
         log.info("{},POST接口请求获取到数据:{}", uuid, JSON.toJSONString(param));
         QwCompany qwCompany = qwCompanyService.selectQwCompanyByCorpId(param.getCorpId());

+ 1 - 1
fs-qw-company-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 0 - 4
fs-qw-company-api/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -115,10 +115,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/msg").anonymous()
                 .antMatchers("/testSop").anonymous()
                 .antMatchers("/common/getId**").anonymous()
-                .antMatchers("/common/uploadOSS**").anonymous()
-                .antMatchers("/common/uploadWang**").anonymous()
-                .antMatchers("/common/download**").anonymous()
-                .antMatchers("/common/download/resource**").anonymous()
                 .antMatchers("/swagger-ui.html").anonymous()
                 .antMatchers("/swagger-resources/**").anonymous()
                 .antMatchers("/webjars/**").anonymous()

+ 1 - 1
fs-qw-mq/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 18 - 8
fs-qw-mq/src/main/java/com/fs/framework/config/ResourcesConfig.java

@@ -4,6 +4,7 @@ import com.fs.common.config.FSConfig;
 import com.fs.common.constant.Constants;
 import com.fs.framework.interceptor.RepeatSubmitInterceptor;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.cors.CorsConfiguration;
@@ -15,8 +16,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 /**
  * 通用配置
- * 
-
+ *
  */
 @Configuration
 public class ResourcesConfig implements WebMvcConfigurer
@@ -24,6 +24,10 @@ public class ResourcesConfig implements WebMvcConfigurer
     @Autowired
     private RepeatSubmitInterceptor repeatSubmitInterceptor;
 
+    /** CORS允许来源,逗号分隔多域名,默认* */
+    @Value("${cors.allowed-origins:*}")
+    private String allowedOrigins;
+
     @Override
     public void addResourceHandlers(ResourceHandlerRegistry registry)
     {
@@ -44,7 +48,7 @@ public class ResourcesConfig implements WebMvcConfigurer
     }
 
     /**
-     * 跨域配置
+     * 跨域配置:使用 addAllowedOrigin(兼容 Spring Boot 2.2),支持配置化多域名
      */
     @Bean
     public CorsFilter corsFilter()
@@ -52,13 +56,19 @@ public class ResourcesConfig implements WebMvcConfigurer
         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
         CorsConfiguration config = new CorsConfiguration();
         config.setAllowCredentials(true);
-        // 设置访问源地址
-        config.addAllowedOrigin("*");
-        // 设置访问源请求头
+        if (allowedOrigins != null) {
+            for (String origin : allowedOrigins.split(",")) {
+                String trimmed = origin.trim();
+                if (!trimmed.isEmpty()) {
+                    config.addAllowedOrigin(trimmed);
+                }
+            }
+        }
+        if (config.getAllowedOrigins() == null || config.getAllowedOrigins().isEmpty()) {
+            config.addAllowedOrigin("*");
+        }
         config.addAllowedHeader("*");
-        // 设置访问源请求方法
         config.addAllowedMethod("*");
-        // 对接口配置跨域设置
         source.registerCorsConfiguration("/**", config);
         return new CorsFilter(source);
     }

+ 1 - 1
fs-qw-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 0 - 4
fs-qw-task/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -115,10 +115,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/msg").anonymous()
                 .antMatchers("/app/common/**").anonymous()
                 .antMatchers("/common/getId**").anonymous()
-                .antMatchers("/common/uploadOSS**").anonymous()
-                .antMatchers("/common/uploadWang**").anonymous()
-                .antMatchers("/common/download**").anonymous()
-                .antMatchers("/common/download/resource**").anonymous()
                 .antMatchers("/swagger-ui.html").anonymous()
                 .antMatchers("/swagger-resources/**").anonymous()
                 .antMatchers("/webjars/**").anonymous()

+ 3 - 0
fs-qw-voice/src/main/java/com/fs/app/controller/CommonController.java

@@ -4,6 +4,7 @@ package com.fs.app.controller;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.UrlSecurityUtils;
 import com.fs.fastgptApi.util.AudioUtils;
 import com.fs.fastgptApi.vo.AudioVO;
 import com.fs.sop.domain.QwSopTempVoice;
@@ -62,6 +63,8 @@ public class CommonController extends BaseController {
      */
     @GetMapping("/createVoiceUrl")
     public R createVoiceUrl( @RequestParam("id") Long id,@RequestParam("userVoiceUrl") String userVoiceUrl){
+        // SSRF防护:禁止内网/云元数据地址
+        UrlSecurityUtils.validate(userVoiceUrl);
         AudioVO audioVO = AudioUtils.transferAudioSilkFromUrl(userVoiceUrl,  false);
         return R.ok().put("data", audioVO);
     }

+ 1 - 1
fs-qw-voice/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 25 - 18
fs-qwhook-msg/src/main/java/com/fs/app/utils/AudioUtils.java

@@ -154,28 +154,35 @@ public class AudioUtils {
      * -quiet               : Print only some basic values
      * -tencent             : Compatible with QQ/Wechat
      */
+    /**
+     * 校验路径,拒绝命令注入相关特殊字符
+     */
+    private static void validateExecPath(String... paths) {
+        if (paths == null) {
+            return;
+        }
+        for (String p : paths) {
+            if (p != null && p.matches(".*[&|;>`$].*")) {
+                throw new ServiceException("非法路径字符");
+            }
+        }
+    }
+
     public static void transferPcmSilk(String pcmPath, String target) {
         Process process = null;
         try {
-            /**
-             // 1、这一节的,语音长度太长会使音频长度丢失
-             List<String> commend = new ArrayList<>();
-             // 指令,可参照方法注释, 请不要在commend.add()里同时写【-参数 值】
-             commend.add(path + "silk_v3_encoder.exe");
-             commend.add(pcmPath);
-             commend.add(target);
-             commend.add("-tencent");
-             ProcessBuilder builder = new ProcessBuilder();
-             builder.command(commend);
-             process = builder.start();
-             // 如果删除下班这行写process.waitFor() ,太长的语音会阻塞,BufferedReader 打印出来太长的语音也会阻塞
-             process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
-             */
-            // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
-            process = Runtime.getRuntime().exec("cmd /c start " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
-            process .waitFor();
+            // 使用 ProcessBuilder 数组参数,避免 Runtime.exec 字符串拼接带来的命令注入风险
+            validateExecPath(path, pcmPath, target);
+            List<String> command = new ArrayList<>();
+            command.add(path + "silk_v3_encoder.exe");
+            command.add(pcmPath);
+            command.add(target);
+            command.add("-tencent");
+            ProcessBuilder builder = new ProcessBuilder(command);
+            builder.redirectErrorStream(true);
+            process = builder.start();
+            process.waitFor();
             Thread.sleep(1000);
-            // 有更好的方法会后续慢慢更新..
         } catch (Exception e) {
             e.printStackTrace();
         } finally {

+ 1 - 1
fs-qwhook-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 25 - 18
fs-qwhook-sop/src/main/java/com/fs/app/utils/AudioUtils.java

@@ -155,28 +155,35 @@ public class AudioUtils {
      * -quiet               : Print only some basic values
      * -tencent             : Compatible with QQ/Wechat
      */
+    /**
+     * 校验路径,拒绝命令注入相关特殊字符
+     */
+    private static void validateExecPath(String... paths) {
+        if (paths == null) {
+            return;
+        }
+        for (String p : paths) {
+            if (p != null && p.matches(".*[&|;>`$].*")) {
+                throw new ServiceException("非法路径字符");
+            }
+        }
+    }
+
     public static void transferPcmSilk(String pcmPath, String target) {
         Process process = null;
         try {
-            /**
-             // 1、这一节的,语音长度太长会使音频长度丢失
-             List<String> commend = new ArrayList<>();
-             // 指令,可参照方法注释, 请不要在commend.add()里同时写【-参数 值】
-             commend.add(path + "silk_v3_encoder.exe");
-             commend.add(pcmPath);
-             commend.add(target);
-             commend.add("-tencent");
-             ProcessBuilder builder = new ProcessBuilder();
-             builder.command(commend);
-             process = builder.start();
-             // 如果删除下班这行写process.waitFor() ,太长的语音会阻塞,BufferedReader 打印出来太长的语音也会阻塞
-             process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
-             */
-            // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
-            process = Runtime.getRuntime().exec("cmd /c start " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
-            process .waitFor();
+            // 使用 ProcessBuilder 数组参数,避免 Runtime.exec 字符串拼接带来的命令注入风险
+            validateExecPath(path, pcmPath, target);
+            List<String> command = new ArrayList<>();
+            command.add(path + "silk_v3_encoder.exe");
+            command.add(pcmPath);
+            command.add(target);
+            command.add("-tencent");
+            ProcessBuilder builder = new ProcessBuilder(command);
+            builder.redirectErrorStream(true);
+            process = builder.start();
+            process.waitFor();
             Thread.sleep(1000);
-            // 有更好的方法会后续慢慢更新..
         } catch (Exception e) {
             e.printStackTrace();
         } finally {

+ 1 - 1
fs-qwhook-sop/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 25 - 18
fs-qwhook/src/main/java/com/fs/app/utils/AudioUtils.java

@@ -154,28 +154,35 @@ public class AudioUtils {
      * -quiet               : Print only some basic values
      * -tencent             : Compatible with QQ/Wechat
      */
+    /**
+     * 校验路径,拒绝命令注入相关特殊字符
+     */
+    private static void validateExecPath(String... paths) {
+        if (paths == null) {
+            return;
+        }
+        for (String p : paths) {
+            if (p != null && p.matches(".*[&|;>`$].*")) {
+                throw new ServiceException("非法路径字符");
+            }
+        }
+    }
+
     public static void transferPcmSilk(String pcmPath, String target) {
         Process process = null;
         try {
-            /**
-             // 1、这一节的,语音长度太长会使音频长度丢失
-             List<String> commend = new ArrayList<>();
-             // 指令,可参照方法注释, 请不要在commend.add()里同时写【-参数 值】
-             commend.add(path + "silk_v3_encoder.exe");
-             commend.add(pcmPath);
-             commend.add(target);
-             commend.add("-tencent");
-             ProcessBuilder builder = new ProcessBuilder();
-             builder.command(commend);
-             process = builder.start();
-             // 如果删除下班这行写process.waitFor() ,太长的语音会阻塞,BufferedReader 打印出来太长的语音也会阻塞
-             process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
-             */
-            // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
-            process = Runtime.getRuntime().exec("cmd /c start " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
-            process .waitFor();
+            // 使用 ProcessBuilder 数组参数,避免 Runtime.exec 字符串拼接带来的命令注入风险
+            validateExecPath(path, pcmPath, target);
+            List<String> command = new ArrayList<>();
+            command.add(path + "silk_v3_encoder.exe");
+            command.add(pcmPath);
+            command.add(target);
+            command.add("-tencent");
+            ProcessBuilder builder = new ProcessBuilder(command);
+            builder.redirectErrorStream(true);
+            process = builder.start();
+            process.waitFor();
             Thread.sleep(1000);
-            // 有更好的方法会后续慢慢更新..
         } catch (Exception e) {
             e.printStackTrace();
         } finally {

+ 1 - 1
fs-qwhook/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-redis/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 19 - 9
fs-redis/src/main/java/com/fs/framework/config/ResourcesConfig.java

@@ -4,6 +4,7 @@ import com.fs.common.config.FSConfig;
 import com.fs.common.constant.Constants;
 import com.fs.framework.interceptor.RepeatSubmitInterceptor;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.cors.CorsConfiguration;
@@ -15,8 +16,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 /**
  * 通用配置
- * 
-
+ *
  */
 @Configuration
 public class ResourcesConfig implements WebMvcConfigurer
@@ -24,6 +24,10 @@ public class ResourcesConfig implements WebMvcConfigurer
     @Autowired
     private RepeatSubmitInterceptor repeatSubmitInterceptor;
 
+    /** CORS允许来源,逗号分隔多域名,默认* */
+    @Value("${cors.allowed-origins:*}")
+    private String allowedOrigins;
+
     @Override
     public void addResourceHandlers(ResourceHandlerRegistry registry)
     {
@@ -44,7 +48,7 @@ public class ResourcesConfig implements WebMvcConfigurer
     }
 
     /**
-     * 跨域配置
+     * 跨域配置:使用 addAllowedOrigin(兼容 Spring Boot 2.2),支持配置化多域名
      */
     @Bean
     public CorsFilter corsFilter()
@@ -52,14 +56,20 @@ public class ResourcesConfig implements WebMvcConfigurer
         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
         CorsConfiguration config = new CorsConfiguration();
         config.setAllowCredentials(true);
-        // 设置访问源地址
-        config.addAllowedOrigin("*");
-        // 设置访问源请求头
+        if (allowedOrigins != null) {
+            for (String origin : allowedOrigins.split(",")) {
+                String trimmed = origin.trim();
+                if (!trimmed.isEmpty()) {
+                    config.addAllowedOrigin(trimmed);
+                }
+            }
+        }
+        if (config.getAllowedOrigins() == null || config.getAllowedOrigins().isEmpty()) {
+            config.addAllowedOrigin("*");
+        }
         config.addAllowedHeader("*");
-        // 设置访问源请求方法
         config.addAllowedMethod("*");
-        // 对接口配置跨域设置
         source.registerCorsConfiguration("/**", config);
         return new CorsFilter(source);
     }
-}
+}

+ 1 - 1
fs-repeat-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().addAccept("com.fs.");
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 16 - 5
fs-service/src/main/java/com/fs/ai/service/impl/BaiduAIServiceImpl.java

@@ -17,6 +17,7 @@ import com.fs.ai.vo.BaiduAITokenVO;
 import com.fs.chat.domain.ChatDataset;
 import com.fs.common.utils.StringUtils;
 import com.fs.erp.utils.CommonUtils;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import java.io.File;
@@ -27,13 +28,23 @@ import java.util.*;
 @Service
 public class BaiduAIServiceImpl implements IBaiduAIService {
 
-    String clientId="LUP3P6NA0q9CEAmUhjnWXt4Q";
-    String clientSecret="Esxtj5QVM2klCZV1HfIRYutW5mJ2AP5G";
-    String grantType="client_credentials";
-    String plugins="4v4ki9a5t1hxdv92";
+    /** 百度 AI clientId,可通过配置/环境变量覆盖 */
+    @Value("${baidu.ai.client-id:LUP3P6NA0q9CEAmUhjnWXt4Q}")
+    private String clientId;
 
+    /** 百度 AI clientSecret,可通过配置/环境变量覆盖 */
+    @Value("${baidu.ai.client-secret:Esxtj5QVM2klCZV1HfIRYutW5mJ2AP5G}")
+    private String clientSecret;
 
-    String secretKey="bce-v3/ALTAK-P03N0q1ET2QKrbq3RGdgB/456be350fac6b3fe41ced46cada2cc287e04f5fc";
+    String grantType = "client_credentials";
+
+    /** 百度插件 ID,可通过配置覆盖 */
+    @Value("${baidu.ai.plugins:4v4ki9a5t1hxdv92}")
+    private String plugins;
+
+    /** AppBuilder secretKey,可通过配置/环境变量覆盖 */
+    @Value("${baidu.ai.secret-key:bce-v3/ALTAK-P03N0q1ET2QKrbq3RGdgB/456be350fac6b3fe41ced46cada2cc287e04f5fc}")
+    private String secretKey;
 
     public static  BaiduAITokenVO vo=null;
 

+ 1 - 1
fs-service/src/main/java/com/fs/company/mapper/CompanyDeptMapper.java

@@ -102,7 +102,7 @@ public interface CompanyDeptMapper
      * @param companyId 公司id
      * @return 部门数据
      */
-    @Select("select * from company_dept where company_id = ${companyId}")
+    @Select("select * from company_dept where company_id = #{companyId}")
     List<CompanyDept> selectDeptDataByCompanyId(@Param("companyId") Long companyId);
 
     @Select("select dept_id from company_dept where parent_id=#{parentId}")

+ 2 - 2
fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java

@@ -187,10 +187,10 @@ public interface CompanyUserMapper
     @Select("select * from company_user where user_id=#{userId}")
     CompanyUser selectCompanyUserByUserId(Long userId);
 
-    @Select("select concat(nick_name,'_',user_name) from company_user where user_id=${userId} limit 1")
+    @Select("select concat(nick_name,'_',user_name) from company_user where user_id=#{userId} limit 1")
     String selectCompanyUserNameUserById(Long userId);
 
-    @Select("select distinct user_id from company_user where parent_id=${companyUserId}")
+    @Select("select distinct user_id from company_user where parent_id=#{companyUserId}")
     List<Long> selectUserAllCompanyUserId(@Param("companyUserId") Long companyUserId);
 
     List<CompanyUser> getAllUserListLimit(@Param("companyId") Long companyId,

+ 1 - 1
fs-service/src/main/java/com/fs/course/mapper/FsUserCoursePeriodMapper.java

@@ -132,7 +132,7 @@ public interface FsUserCoursePeriodMapper
     void endPeriod(@Param("now") LocalDate now);
 
 
-    @Select("select * from fs_user_course_period where find_in_set(${companyId},company_id) and #{previousDay} >= period_starting_time and #{previousDay} <= period_end_time ")
+    @Select("select * from fs_user_course_period where find_in_set(#{companyId},company_id) and #{previousDay} >= period_starting_time and #{previousDay} <= period_end_time ")
     List<Long> queryPeriod(@Param("companyId") Long companyId,
                            @Param("previousDay") LocalDate previousDay);
 

+ 3 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseAnswerLogsParam.java

@@ -36,6 +36,9 @@ public class FsCourseAnswerLogsParam  extends BaseEntity  {
     private Long pageNum;
     private Long pageSize;
 
+    /** 分页偏移量,由 Service 在查询前计算:offset = (pageNum-1)*pageSize */
+    private Integer offset;
+
     /**
      * 用户(昵称_id组合)
      */

+ 5 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseAnswerLogsServiceImpl.java

@@ -140,6 +140,11 @@ public class FsCourseAnswerLogsServiceImpl implements IFsCourseAnswerLogsService
             param.setUserIds(userIds);
         }
 
+        // 预计算分页偏移,避免 Mapper XML 中使用 ${} 表达式
+        if (param.getPageNum() != null && param.getPageSize() != null) {
+            param.setOffset((int) ((param.getPageNum() - 1) * param.getPageSize()));
+        }
+
         List<FsCourseAnswerLogsListVO> data = fsCourseAnswerLogsMapper.selectFsCourseAnswerLogsListVONew(param);
         for (FsCourseAnswerLogsListVO datum : data) {
             FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoCacheService.selectFsUserCourseVideoByVideoId(datum.getVideoId());

+ 5 - 0
fs-service/src/main/java/com/fs/fastGpt/service/impl/FastGptCollectionServiceImpl.java

@@ -20,6 +20,7 @@ import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.net.URI;
 import java.net.URL;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
@@ -348,6 +349,10 @@ public class FastGptCollectionServiceImpl implements IFastGptCollectionService
      *  url转临时文件filr
      */
     public static File urlToFile(String fileUrl) throws Exception {
+        // SSRF防护:禁止内网/元数据,并限制可信域名白名单
+        com.fs.common.utils.UrlSecurityUtils.validate(fileUrl, java.util.Arrays.asList(
+                "qlogo.cn", "weixin.qq.com", "myqcloud.com", "cos.ap-guangzhou.myqcloud.com"
+        ));
 
         // 从URL中提取文件名,文件后缀
         String fileExtension = getFileExtension(new File(fileUrl).getName());

+ 26 - 18
fs-service/src/main/java/com/fs/fastgptApi/util/AudioUtils.java

@@ -619,28 +619,35 @@ public class AudioUtils {
      * -quiet               : Print only some basic values
      * -tencent             : Compatible with QQ/Wechat
      */
+    /**
+     * 校验路径,拒绝命令注入相关特殊字符
+     */
+    private static void validateExecPath(String... paths) {
+        if (paths == null) {
+            return;
+        }
+        for (String p : paths) {
+            if (p != null && p.matches(".*[&|;>`$].*")) {
+                throw new ServiceException("非法路径字符");
+            }
+        }
+    }
+
     public static void transferPcmSilk(String pcmPath, String target) {
         Process process = null;
         try {
-            /**
-             // 1、这一节的,语音长度太长会使音频长度丢失
-             List<String> commend = new ArrayList<>();
-             // 指令,可参照方法注释, 请不要在commend.add()里同时写【-参数 值】
-             commend.add(path + "silk_v3_encoder.exe");
-             commend.add(pcmPath);
-             commend.add(target);
-             commend.add("-tencent");
-             ProcessBuilder builder = new ProcessBuilder();
-             builder.command(commend);
-             process = builder.start();
-             // 如果删除下班这行写process.waitFor() ,太长的语音会阻塞,BufferedReader 打印出来太长的语音也会阻塞
-             process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
-             */
-            // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
-            process = Runtime.getRuntime().exec("cmd /c start  " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
-            process .waitFor();
+            // 使用 ProcessBuilder 数组参数,避免 Runtime.exec 字符串拼接带来的命令注入风险
+            validateExecPath(path, pcmPath, target);
+            List<String> command = new ArrayList<>();
+            command.add(path + "silk_v3_encoder.exe");
+            command.add(pcmPath);
+            command.add(target);
+            command.add("-tencent");
+            ProcessBuilder builder = new ProcessBuilder(command);
+            builder.redirectErrorStream(true);
+            process = builder.start();
+            process.waitFor();
             Thread.sleep(1000);
-            // 有更好的方法会后续慢慢更新..
         } catch (Exception e) {
             e.printStackTrace();
         } finally {
@@ -658,6 +665,7 @@ public class AudioUtils {
     public static void transferPcmSilkSecond(String pcmPath, String target) {
         Process process = null;
         try {
+            validateExecPath(path, pcmPath, target);
             // 使用 ProcessBuilder 替代 Runtime.exec 提高可靠性
             List<String> command = new ArrayList<>();
             command.add("cmd");

+ 8 - 0
fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java

@@ -84,6 +84,14 @@ public class PhoneUtil {
      * @return
      */
     private static String OLD_KEY = "2c8d1a7f4e9b3c6ae6d5c4b3a291f8c9";
+
+    /**
+     * 手机号脱敏:13812345678 → 138****1234
+     */
+    public static String maskPhone(String phone) {
+        if (phone == null || phone.length() < 7) return "***";
+        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
+    }
     public static String encryptPhoneOldKey(String text) {
         String encryptedText=null;
         try {

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/param/SelectCusListPageParam.java

@@ -23,4 +23,7 @@ public class SelectCusListPageParam extends BaseEntity {
 
     private Long pageNum;
     private Long pageSize;
+
+    /** 分页偏移量,由 Service 在查询前计算:offset = (pageNum-1)*pageSize */
+    private Integer offset;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/param/h5/FsUserPageListParam.java

@@ -18,6 +18,9 @@ public class FsUserPageListParam implements Serializable {
     @ApiModelProperty(value = "页大小,默认为10", required = true)
     private Integer pageSize = 10;
 
+    /** 分页偏移量,由 Service 在查询前计算:offset = (pageNum-1)*pageSize */
+    private Integer offset;
+
     @ApiModelProperty(value = "模糊搜索,可以通过微信名称、电话来模糊匹配")
     private String keyword;
 

+ 8 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsUserScrmServiceImpl.java

@@ -261,6 +261,9 @@ public class FsUserScrmServiceImpl implements IFsUserScrmService
         Asserts.check(ObjectUtils.isNotNull(param.getPageNum()), "页数不能为空");
         Asserts.check(ObjectUtils.isNotNull(param.getPageSize()), "页大小不能为空");
 
+        // 预计算分页偏移,避免 Mapper XML 中使用 ${} 表达式
+        param.setOffset((int) ((param.getPageNum() - 1) * param.getPageSize()));
+
         List<FsUserScrm> fsUsers = fsUserMapper.selectCusListPage(param);
         for (FsUserScrm fsUser : fsUsers) {
             if(ObjectUtils.isNotNull(fsUser.getStatus())){
@@ -554,6 +557,11 @@ public class FsUserScrmServiceImpl implements IFsUserScrmService
             param.setCompanyUserIds(userIds);
         }
 
+        // 预计算分页偏移,避免 Mapper XML 中使用 ${} 表达式
+        if (param.getPageNum() != null && param.getPageSize() != null) {
+            param.setOffset((param.getPageNum() - 1) * param.getPageSize());
+        }
+
         List<FsUserPageListVO> fsUserPageListVOS = fsUserMapper.selectFsUserPageListNew(param);
         Map<Long, CompanyTag> tagMap = companyTagCacheService.queryAllTagMap();
         //获取会员的最新的看课状态和最后看课时间

+ 6 - 3
fs-service/src/main/java/com/fs/im/service/impl/OpenIMServiceImpl.java

@@ -509,7 +509,8 @@ public class OpenIMServiceImpl implements OpenIMService {
         log.info("发送消息的请求体:\n{}", jsonObject.toString());
         long time = new Date().getTime();
 
-        log.info("请求header: token={},operationID={}",adminToken,time);
+        String maskedToken = adminToken != null && adminToken.length() > 4 ? adminToken.substring(0, 4) + "****" : "****";
+        log.info("请求header: token={},operationID={}", maskedToken, time);
         String result = HttpRequest.post(url)
                 .header("operationID", time + "")
                 .header("token",adminToken)
@@ -1069,7 +1070,8 @@ public class OpenIMServiceImpl implements OpenIMService {
             throw new ServiceException("注册失败");
         } else {
 //            return R.error("获取管理员token失败");
-            log.error("异步执行IM注册/添加好友失败,原因:{}, adminToken:{}", "获取管理员token失败", adminToken);
+            String maskedAdminToken = adminToken != null && adminToken.length() > 4 ? adminToken.substring(0, 4) + "****" : "****";
+            log.error("异步执行IM注册/添加好友失败,原因:{}, adminToken:{}", "获取管理员token失败", maskedAdminToken);
             throw new ServiceException("获取管理员token失败");
         }
     }
@@ -1301,7 +1303,8 @@ public class OpenIMServiceImpl implements OpenIMService {
         String url = IMConfig.URL+"/msg/batch_send_msg";
         log.info("请求url: {},\n请求参数:{}", url, jsonObject);
         long timestamp = System.currentTimeMillis();
-        log.info("请求header,operationID:{},token:{}", timestamp, adminToken);
+        String maskedToken = adminToken != null && adminToken.length() > 4 ? adminToken.substring(0, 4) + "****" : "****";
+        log.info("请求header,operationID:{},token:{}", timestamp, maskedToken);
 
         String result = HttpRequest.post(url)
                 .header("operationID", timestamp + "")

+ 1 - 1
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -463,7 +463,7 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
     @DataSource(DataSourceType.SLAVE)
     List<GroupUserExternalVo> selectByGroupUser(@Param("ids") List<String> ids);
 
-    @Select("select is_repeat from qw_external_contact where user_id=${userId} limit 1")
+    @Select("select is_repeat from qw_external_contact where user_id=#{userId} limit 1")
     Integer selectQwIsRepeat(Long id);
 
     @Select("select * from qw_external_contact where fs_user_id = #{fsUserId} and status = 0 ")

+ 1 - 1
fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java

@@ -404,7 +404,7 @@ public interface QwUserMapper extends BaseMapper<QwUser>
     @Select("select qw_user_name from qw_user where qw_user_id=#{qwUserId} and corp_id=#{corpId}")
     String selectQwUserName(@Param("qwUserId") String qwUserId,@Param("corpId") String corpId);
 
-    @Select("select qw_user_id from qw_user where company_user_id = ${companyUserId}")
+    @Select("select qw_user_id from qw_user where company_user_id = #{companyUserId}")
     List<String> findQwUserIdListByCompanyUserId(@Param("companyUserId") Long companyUserId);
 
     @Select("select qw_user_id from qw_user where company_user_id = #{userId} and corp_id = #{corpId}")

+ 1 - 1
fs-service/src/main/java/com/fs/qw/mapper/QwWatchLogMapper.java

@@ -263,7 +263,7 @@ public interface QwWatchLogMapper extends BaseMapper<QwWatchLog>{
             @Param("project") Long project,
             @Param("courseId") Long courseId,
             @Param("videoId") Long videoId,
-            @Param("pageNum") Long pageNum,
+            @Param("offset") Integer offset,
             @Param("pageSize") Long pageSize
     );
 

+ 3 - 0
fs-service/src/main/java/com/fs/qw/param/QwWorkTaskListParam.java

@@ -63,6 +63,9 @@ public class QwWorkTaskListParam {
     private Long pageNum;
     private Long pageSize;
 
+    /** 分页偏移量,由 Service 在查询前计算:offset = (pageNum-1)*pageSize */
+    private Integer offset;
+
     /**
      * 销售部门
      */

+ 4 - 0
fs-service/src/main/java/com/fs/qw/service/impl/HyWorkTaskServiceImpl.java

@@ -264,6 +264,10 @@ public class HyWorkTaskServiceImpl extends ServiceImpl<HyWorkTaskMapper, HyWorkT
 
     @Override
     public List<QwWorkTaskListVO> selectHyWorkTaskListVONew(QwWorkTaskListParam qwWorkTask) {
+        // 预计算分页偏移,避免 Mapper XML 中使用 ${} 表达式
+        if (qwWorkTask.getPageNum() != null && qwWorkTask.getPageSize() != null) {
+            qwWorkTask.setOffset((int) ((qwWorkTask.getPageNum() - 1) * qwWorkTask.getPageSize()));
+        }
         List<QwWorkTaskListVO> list = hyWorkTaskMapper.selectHyWorkTaskListVONew(qwWorkTask);
         for (QwWorkTaskListVO item : list) {
             if(ObjectUtils.isNotNull(item.getCompanyId())){

+ 7 - 1
fs-service/src/main/java/com/fs/qw/service/impl/QwWatchLogServiceImpl.java

@@ -336,10 +336,16 @@ public class QwWatchLogServiceImpl extends ServiceImpl<QwWatchLogMapper, QwWatch
 
         List<QwWatchLogStatisticsListVO> list = new ArrayList<>();
 
+        // 预计算分页偏移,避免 Mapper XML 中使用 ${} 表达式
+        Integer offset = null;
+        if (param.getPageNum() != null && param.getPageSize() != null) {
+            offset = (int) ((param.getPageNum() - 1) * param.getPageSize());
+        }
+
         // 统计销售下面的所有记录
         List<QwWatchLogStatisticsListVO> vos = qwWatchLogMapper
                 .selectQwWatchLogByCompanyUserId(userIds
-                        , param.getStartDate(),param.getEndDate(),param.getProject(),param.getCourseId(),param.getVideoId(),param.getPageNum(),param.getPageSize());
+                        , param.getStartDate(),param.getEndDate(),param.getProject(),param.getCourseId(),param.getVideoId(),offset,param.getPageSize());
 
         for (QwWatchLogStatisticsListVO item : vos) {
             Company company = companyCacheService.selectCompanyById(item.getCompanyId());

+ 2 - 1
fs-service/src/main/java/com/fs/qwApi/service/impl/QwApiServiceImpl.java

@@ -968,9 +968,10 @@ public class QwApiServiceImpl implements QwApiService {
             HttpResponse response = httpClient.execute(httpGet);
 
             String tokenJson = EntityUtils.toString(response.getEntity());
-            log.info("获取token:{}", tokenJson);
             JSONObject jsonObject = JSON.parseObject(tokenJson);
             token = jsonObject.getString("access_token");
+            String maskedToken = token != null && token.length() > 4 ? token.substring(0, 4) + "****" : "****";
+            log.info("获取token成功,token={}", maskedToken);
             redisCache.setCacheObject("qwServer:corpId:"+corpId+":"+corpSecret,token,1,TimeUnit.HOURS);
         } catch (Exception e) {
             e.printStackTrace();

+ 3 - 0
fs-service/src/main/java/com/fs/store/param/SelectCusListPageParam.java

@@ -23,4 +23,7 @@ public class SelectCusListPageParam extends BaseEntity {
 
     private Long pageNum;
     private Long pageSize;
+
+    /** 分页偏移量,由 Service 在查询前计算:offset = (pageNum-1)*pageSize */
+    private Integer offset;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/store/param/h5/FsUserPageListParam.java

@@ -19,6 +19,9 @@ public class FsUserPageListParam implements Serializable {
     @ApiModelProperty(value = "页大小,默认为10", required = true)
     private Integer pageSize = 10;
 
+    /** 分页偏移量,由 Service 在查询前计算:offset = (pageNum-1)*pageSize */
+    private Integer offset;
+
     @ApiModelProperty(value = "模糊搜索,可以通过微信名称、电话来模糊匹配")
     private String keyword;
 

+ 62 - 0
fs-service/src/main/java/com/fs/system/oss/ObsStsService.java

@@ -0,0 +1,62 @@
+package com.fs.system.oss;
+
+import com.fs.common.utils.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+/**
+ * 华为云 OBS 凭证服务
+ * <p>
+ * 从配置读取 AK/SK(仅后端持有),封装前端上传所需凭证。
+ * 项目依赖 esdk-obs-java-bundle,未引入 IAM TemporaryAccessKey SDK,
+ * 故采用“后端签发上传凭证”方式;若配置中预置了 securityToken 则一并返回。
+ */
+@Service
+public class ObsStsService {
+
+    @Value("${obs.access-key-id:}")
+    private String accessKeyId;
+
+    @Value("${obs.secret-access-key:}")
+    private String secretAccessKey;
+
+    @Value("${obs.endpoint:https://obs.cn-north-4.myhuaweicloud.com}")
+    private String endpoint;
+
+    @Value("${obs.bucket:}")
+    private String bucket;
+
+    /** 可选:若使用临时凭证,可在配置中填写 */
+    @Value("${obs.security-token:}")
+    private String securityToken;
+
+    @Value("${obs.duration-seconds:3600}")
+    private Integer durationSeconds;
+
+    /**
+     * 生成(签发)前端可用的 OBS 上传凭证
+     */
+    public ObsStsVO generateTempCredential() {
+        if (StringUtils.isEmpty(accessKeyId) || StringUtils.isEmpty(secretAccessKey)) {
+            throw new IllegalArgumentException("OBS 凭证未配置,请检查 obs.access-key-id / obs.secret-access-key");
+        }
+        if (StringUtils.isEmpty(bucket)) {
+            throw new IllegalArgumentException("OBS 桶未配置,请检查 obs.bucket");
+        }
+
+        int duration = durationSeconds == null || durationSeconds <= 0 ? 3600 : durationSeconds;
+
+        ObsStsVO vo = new ObsStsVO();
+        vo.setAccessKey(accessKeyId);
+        vo.setSecretKey(secretAccessKey);
+        // 未配置 STS 时为 null;ObsClient 可对空 token 按长期密钥使用
+        if (StringUtils.isNotEmpty(securityToken)) {
+            vo.setSecurityToken(securityToken);
+        }
+        vo.setEndpoint(endpoint);
+        vo.setBucket(bucket);
+        vo.setDurationSeconds(duration);
+        vo.setExpiration(System.currentTimeMillis() + duration * 1000L);
+        return vo;
+    }
+}

+ 92 - 0
fs-service/src/main/java/com/fs/system/oss/ObsStsVO.java

@@ -0,0 +1,92 @@
+package com.fs.system.oss;
+
+import java.io.Serializable;
+
+/**
+ * 华为云 OBS 临时凭证返回结构
+ * <p>
+ * 说明:当前项目未引入华为云 IAM STS SDK,由后端持有长期 AK/SK,
+ * 经认证接口下发给前端,避免密钥打进前端 bundle。
+ * 若后续接入正式 STS,可直接填充 securityToken 与过期时间。
+ */
+public class ObsStsVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 访问密钥ID */
+    private String accessKey;
+
+    /** 秘密访问密钥 */
+    private String secretKey;
+
+    /** 安全令牌(正式 STS 时必填;当前可为 null) */
+    private String securityToken;
+
+    /** Endpoint */
+    private String endpoint;
+
+    /** 桶名称 */
+    private String bucket;
+
+    /** 有效期 */
+    private Integer durationSeconds;
+
+    /** 过期时间戳 */
+    private Long expiration;
+
+    public String getAccessKey() {
+        return accessKey;
+    }
+
+    public void setAccessKey(String accessKey) {
+        this.accessKey = accessKey;
+    }
+
+    public String getSecretKey() {
+        return secretKey;
+    }
+
+    public void setSecretKey(String secretKey) {
+        this.secretKey = secretKey;
+    }
+
+    public String getSecurityToken() {
+        return securityToken;
+    }
+
+    public void setSecurityToken(String securityToken) {
+        this.securityToken = securityToken;
+    }
+
+    public String getEndpoint() {
+        return endpoint;
+    }
+
+    public void setEndpoint(String endpoint) {
+        this.endpoint = endpoint;
+    }
+
+    public String getBucket() {
+        return bucket;
+    }
+
+    public void setBucket(String bucket) {
+        this.bucket = bucket;
+    }
+
+    public Integer getDurationSeconds() {
+        return durationSeconds;
+    }
+
+    public void setDurationSeconds(Integer durationSeconds) {
+        this.durationSeconds = durationSeconds;
+    }
+
+    public Long getExpiration() {
+        return expiration;
+    }
+
+    public void setExpiration(Long expiration) {
+        this.expiration = expiration;
+    }
+}

+ 29 - 0
fs-service/src/main/java/com/fs/tzBankPay/config/TzBankProperties.java

@@ -0,0 +1,29 @@
+package com.fs.tzBankPay.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 台州银行开放平台凭证配置;优先环境变量/Nacos 覆盖,默认值保证本地可启动
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "tzbank")
+public class TzBankProperties {
+
+    /** 获取 Token 地址 */
+    private String tokenUrl = "https://open.tzbank.com/ApiGateWay/auth/getToken";
+
+    /** 应用密钥 */
+    private String appSecret;
+
+    /** 应用 Key */
+    private String appKey;
+
+    /** 平台公钥 */
+    private String platformPublicKey;
+
+    /** 商户私钥 */
+    private String privateKey;
+}

+ 13 - 5
fs-service/src/main/java/com/fs/tzBankPay/form/GetTokenForm.java

@@ -2,14 +2,22 @@ package com.fs.tzBankPay.form;
 
 import lombok.Data;
 
+/**
+ * 台州银行获取 Token 请求表单。
+ * 敏感字段默认留空,由调用方从 tzbank.* 配置或支付配置中填充。
+ */
 @Data
 public class GetTokenForm {
 
-    private String tokenURL="https://open.tzbank.com/ApiGateWay/auth/getToken";
+    private String tokenURL = "https://open.tzbank.com/ApiGateWay/auth/getToken";
     private String param;
-    private String appSecret ="11C8BD481F1F308840827978841CFF66";
-    private String appKey ="9998e514-1958-4def-9fd7-693ea360c6f2";
-    private String platformPublicKey="0449B1AB7CAF1C843B827824377A2910A450418BC7C0AD0F0EF70CC97CC46832DD4CC7422FEACEEE6DE7FFEDE22D8783AA472378DE9341DD895FFE37E3C9D2E8CE";
-    private String privateKey ="0095D6D7D783A179AEACCA1D6883FF34160724F4B0F5FC7289B3E34FDA29F1FFB5";
+    /** 应用密钥,勿在代码中硬编码,由配置注入 */
+    private String appSecret = "";
+    /** 应用 Key,勿在代码中硬编码,由配置注入 */
+    private String appKey = "";
+    /** 平台公钥,勿在代码中硬编码,由配置注入 */
+    private String platformPublicKey = "";
+    /** 私钥,勿在代码中硬编码,由配置注入 */
+    private String privateKey = "";
 
 }

+ 36 - 5
fs-service/src/main/java/com/fs/tzBankPay/util/TzBankSendUtil.java

@@ -1,15 +1,16 @@
 package com.fs.tzBankPay.util;
 
 import com.alibaba.fastjson.JSON;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.his.domain.FsPayConfig;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
+import com.fs.tzBankPay.config.TzBankProperties;
 import com.fs.tzBankPay.doman.TokenResult;
 import com.fs.tzBankPay.doman.TzBankParam;
 import com.fs.tzBankPay.form.GetTokenForm;
 import com.fs.tzBankPay.form.RequestAPIForm;
-import com.fs.common.core.redis.RedisCache;
 import com.google.gson.Gson;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -20,6 +21,9 @@ public class TzBankSendUtil {
 //    TzBankUtil tzBankUtil;
     TzBankUtil tzBankUtil=new TzBankUtil();
 
+    @Autowired
+    private TzBankProperties tzBankProperties;
+
     public TokenResult getToken(){
 //        String token=redisCache.getCacheObject("TzBankToken");
 //        if (token!=null&&token!=""){
@@ -28,12 +32,23 @@ public class TzBankSendUtil {
 
         SysConfigMapper sysConfigMapper= SpringUtils.getBean(SysConfigMapper.class);
         GetTokenForm getTokenForm = new GetTokenForm();
+        // 先从 tzbank.* 配置填充,保证无库配置时仍可启动
+        fillFromProperties(getTokenForm);
         SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("his.pay");
         FsPayConfig fsPayConfig = new Gson().fromJson(sysConfig.getConfigValue(), FsPayConfig.class);
-        getTokenForm.setAppKey(fsPayConfig.getTzAppKey());
-        getTokenForm.setAppSecret(fsPayConfig.getTzAppSecret());
-        getTokenForm.setPlatformPublicKey(fsPayConfig.getTzPlatformPublicKey());
-        getTokenForm.setPrivateKey(fsPayConfig.getTzPrivateKey());
+        // 库中支付配置优先覆盖
+        if (StringUtils.isNotEmpty(fsPayConfig.getTzAppKey())) {
+            getTokenForm.setAppKey(fsPayConfig.getTzAppKey());
+        }
+        if (StringUtils.isNotEmpty(fsPayConfig.getTzAppSecret())) {
+            getTokenForm.setAppSecret(fsPayConfig.getTzAppSecret());
+        }
+        if (StringUtils.isNotEmpty(fsPayConfig.getTzPlatformPublicKey())) {
+            getTokenForm.setPlatformPublicKey(fsPayConfig.getTzPlatformPublicKey());
+        }
+        if (StringUtils.isNotEmpty(fsPayConfig.getTzPrivateKey())) {
+            getTokenForm.setPrivateKey(fsPayConfig.getTzPrivateKey());
+        }
         try {
             String result= tzBankUtil.tokenRequest(
                     getTokenForm.getTokenURL(),getTokenForm.getAppKey(),
@@ -48,6 +63,22 @@ public class TzBankSendUtil {
         }
     }
 
+    /**
+     * 从 Spring 配置填充 GetTokenForm 默认凭证
+     */
+    private void fillFromProperties(GetTokenForm getTokenForm) {
+        if (tzBankProperties == null) {
+            return;
+        }
+        if (StringUtils.isNotEmpty(tzBankProperties.getTokenUrl())) {
+            getTokenForm.setTokenURL(tzBankProperties.getTokenUrl());
+        }
+        getTokenForm.setAppKey(tzBankProperties.getAppKey());
+        getTokenForm.setAppSecret(tzBankProperties.getAppSecret());
+        getTokenForm.setPlatformPublicKey(tzBankProperties.getPlatformPublicKey());
+        getTokenForm.setPrivateKey(tzBankProperties.getPrivateKey());
+    }
+
     public String send(Object param,String url){
         TzBankParam tz = new TzBankParam();
         tz.setTranSeq("2023"+System.currentTimeMillis());

+ 53 - 7
fs-service/src/main/resources/application-common.yml

@@ -14,6 +14,11 @@ fs:
   addressEnabled: false
   # 验证码类型 math 数组计算 char 字符验证
   captchaType: math
+  # App 端 JWT(fs.jwt.*);生产环境务必通过环境变量 FS_JWT_SECRET 或 Nacos 覆盖,长度建议≥32
+  jwt:
+    secret: ${FS_JWT_SECRET:7af48cb753c96945816b5fdaaa01e879}
+    expire: 604800
+    header: AppToken
 
 # 开发环境配置
 server:
@@ -62,14 +67,18 @@ spring:
       enabled: true
 
 
-# token配置
+# token配置(Web 后台 Spring Security)
 token:
     # 令牌自定义标识
     header: Authorization
-    # 令牌密钥
-    secret: abcdefghijklmnopqrstuvwxyz
+    # 令牌密钥;生产环境务必通过环境变量 TOKEN_SECRET 或 Nacos 覆盖,禁止使用弱默认值
+    secret: ${TOKEN_SECRET:abcdefghijklmnopqrstuvwxyz}
     # 令牌有效期(默认30分钟)
     expireTime: 180
+
+# CORS 允许来源(逗号分隔多域名);生产建议配置具体域名,勿长期使用 *
+cors:
+  allowed-origins: ${CORS_ALLOWED_ORIGINS:*}
 mybatis-plus:
   # 搜索指定包别名
   typeAliasesPackage: com.fs.**.domain,com.fs.**.bo
@@ -142,9 +151,46 @@ wechat:
     base-url: https://api.weixin.qq.com
     upload-shipping-info: /wxa/sec/order/upload_shipping_info
 hsy:
-  access_key: AKLTZTc4YTE4ZjI2OWViNDNjZGI2NjhiYTI5Njc5ZjA1Mzk
-  secret_key: WXpjelpUYzFOakF5TUdObE5EZGtNR0ZsWXpKaU1tTmtZakk1WXpObE4yRQ==
+  access_key: ${HSY_ACCESS_KEY:AKLTZTc4YTE4ZjI2OWViNDNjZGI2NjhiYTI5Njc5ZjA1Mzk}
+  secret_key: ${HSY_SECRET_KEY:WXpjelpUYzFOakF5TUdObE5EZGtNR0ZsWXpKaU1tTmtZakk1WXpObE4yRQ==}
   region: cn-north-1
-  role_access_key: AKLTNmMwNjJkNDFhYTVjNDIzYzhhNzEyZmZmZTlmYzBhNGM
-  role_secret_key: T0RaaFl6UmhZV1V4WXpKbU5EWTBNMkZpT0RNNU9UY3daak0wTjJFd09XUQ==
+  role_access_key: ${HSY_ROLE_ACCESS_KEY:AKLTNmMwNjJkNDFhYTVjNDIzYzhhNzEyZmZmZTlmYzBhNGM}
+  role_secret_key: ${HSY_ROLE_SECRET_KEY:T0RaaFl6UmhZV1V4WXpKbU5EWTBNMkZpT0RNNU9UY3daak0wTjJFd09XUQ==}
   role_trn: trn:iam::2114522511:role/hylj
+
+# 飞鱼开放平台(默认值兼容历史硬编码,生产请用环境变量覆盖)
+feiyu:
+  app-id: ${FEIYU_APP_ID:1770748979316843}
+  app-secret: ${FEIYU_APP_SECRET:106c17623dd0eaba37dab935b9839c698c0876d1}
+
+# 百度 AI / AppBuilder
+baidu:
+  ai:
+    client-id: ${BAIDU_AI_CLIENT_ID:LUP3P6NA0q9CEAmUhjnWXt4Q}
+    client-secret: ${BAIDU_AI_CLIENT_SECRET:Esxtj5QVM2klCZV1HfIRYutW5mJ2AP5G}
+    plugins: ${BAIDU_AI_PLUGINS:4v4ki9a5t1hxdv92}
+    secret-key: ${BAIDU_AI_SECRET_KEY:bce-v3/ALTAK-P03N0q1ET2QKrbq3RGdgB/456be350fac6b3fe41ced46cada2cc287e04f5fc}
+
+# 微信公众号消息校验(与 wx.mp.configs 并存;token/aes-key 供 Controller @Value 读取)
+wx:
+  mp:
+    token: ${WX_MP_TOKEN:U2qmxEbsp0PJFoLRvUDvIjVi9XPzuVc2}
+    encoding-aes-key: ${WX_MP_ENCODING_AES_KEY:P3HE7Gd1PJVQqCLoOMop5uYfjx9LwfY53rnC3VUuLZS}
+
+# 台州银行开放平台凭证(优先环境变量;库 his.pay 配置优先于此处)
+tzbank:
+  token-url: ${TZBANK_TOKEN_URL:https://open.tzbank.com/ApiGateWay/auth/getToken}
+  app-secret: ${TZBANK_APP_SECRET:11C8BD481F1F308840827978841CFF66}
+  app-key: ${TZBANK_APP_KEY:9998e514-1958-4def-9fd7-693ea360c6f2}
+  platform-public-key: ${TZBANK_PLATFORM_PUBLIC_KEY:0449B1AB7CAF1C843B827824377A2910A450418BC7C0AD0F0EF70CC97CC46832DD4CC7422FEACEEE6DE7FFEDE22D8783AA472378DE9341DD895FFE37E3C9D2E8CE}
+  private-key: ${TZBANK_PRIVATE_KEY:0095D6D7D783A179AEACCA1D6883FF34160724F4B0F5FC7289B3E34FDA29F1FFB5}
+
+# 华为云 OBS 凭证(仅后端持有,通过 /common/sts/obs 下发给已登录前端)
+# 建议在 Nacos/环境变量中按品牌覆盖 bucket;密钥吊销后请同步更新
+obs:
+  access-key-id: ${OBS_ACCESS_KEY:K2UTJGIN7UTZJR2XMXYG}
+  secret-access-key: ${OBS_SECRET_KEY:sbyeNJLbcYmH6copxeFP9pAoksM4NIT9Zw4x0SRX}
+  endpoint: ${OBS_ENDPOINT:https://obs.cn-north-4.myhuaweicloud.com}
+  bucket: ${OBS_BUCKET:xfk-hw079058881}
+  security-token: ${OBS_SECURITY_TOKEN:}
+  duration-seconds: ${OBS_DURATION_SECONDS:3600}

+ 2 - 2
fs-service/src/main/resources/application-dev-yjb.yml

@@ -79,7 +79,7 @@ spring:
                         merge-sql: true
                     wall:
                         config:
-                            multi-statement-allow: true
+                            multi-statement-allow: false
         sop:
             type: com.alibaba.druid.pool.DruidDataSource
             driverClassName: com.mysql.cj.jdbc.Driver
@@ -133,7 +133,7 @@ spring:
                         merge-sql: true
                     wall:
                         config:
-                            multi-statement-allow: true
+                            multi-statement-allow: false
 #是否为新商户,新商户不走mpOpenId
 isNewWxMerchant: false
 #是否使用新im

+ 2 - 2
fs-service/src/main/resources/application-druid-yjb-test.yml

@@ -79,7 +79,7 @@ spring:
                         merge-sql: true
                     wall:
                         config:
-                            multi-statement-allow: true
+                            multi-statement-allow: false
         sop:
             type: com.alibaba.druid.pool.DruidDataSource
             driverClassName: com.mysql.cj.jdbc.Driver
@@ -133,7 +133,7 @@ spring:
                         merge-sql: true
                     wall:
                         config:
-                            multi-statement-allow: true
+                            multi-statement-allow: false
 
 #是否为新商户,新商户不走mpOpenId
 isNewWxMerchant: false

+ 16 - 16
fs-service/src/main/resources/application-druid-yjb.yml

@@ -10,8 +10,8 @@ spring:
         port: 6379
         # 数据库索引
         database: 0
-        # 密码
-        password: Ylrz_1q2w3e4r5t6y
+        # 密码(可用环境变量 REDIS_PASSWORD 覆盖)
+        password: ${REDIS_PASSWORD:Ylrz_1q2w3e4r5t6y}
         # 连接超时时间
         timeout: 20s
         lettuce:
@@ -33,14 +33,14 @@ spring:
                 master:
                     url: jdbc:mysql://10.206.0.9:3306/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: yjb
-                    password: Ylrz_1q2w3e4r5t6y
+                    password: ${DB_PASSWORD:Ylrz_1q2w3e4r5t6y}
                 # 从库数据源
                 slave:
                     # 从数据源开关/默认关闭
                     enabled: true
                     url: jdbc:mysql://10.206.0.12:3306/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: yjb
-                    password: Ylrz_1q2w3e4r5t6y
+                    password: ${DB_PASSWORD:Ylrz_1q2w3e4r5t6y}
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量
@@ -64,12 +64,12 @@ spring:
                     enabled: true
                 statViewServlet:
                     enabled: true
-                    # 设置白名单,不填则允许所有访问
-                    allow:
+                    # 设置白名单,默认仅本机(可用 DRUID_ALLOW 覆盖)
+                    allow: ${DRUID_ALLOW:127.0.0.1}
                     url-pattern: /druid/*
                     # 控制台管理用户名和密码
-                    login-username: fs
-                    login-password: 123456
+                    login-username: ${DRUID_USERNAME:fs}
+                    login-password: ${DRUID_PASSWORD:123456}
                 filter:
                     stat:
                         enabled: true
@@ -79,7 +79,7 @@ spring:
                         merge-sql: true
                     wall:
                         config:
-                            multi-statement-allow: true
+                            multi-statement-allow: false
         sop:
             type: com.alibaba.druid.pool.DruidDataSource
             driverClassName: com.mysql.cj.jdbc.Driver
@@ -88,13 +88,13 @@ spring:
                 master:
                     url: jdbc:mysql://10.206.0.9:3306/fs_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: yjb
-                    password: Ylrz_1q2w3e4r5t6y
+                    password: ${DB_PASSWORD:Ylrz_1q2w3e4r5t6y}
                 read:
                   # 从数据源开关/默认关闭
                   enabled: true
                   url: jdbc:mysql://10.206.0.12:3306/fs_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                   username: yjb
-                  password: Ylrz_1q2w3e4r5t6y
+                  password: ${DB_PASSWORD:Ylrz_1q2w3e4r5t6y}
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量
@@ -118,12 +118,12 @@ spring:
                     enabled: true
                 statViewServlet:
                     enabled: true
-                    # 设置白名单,不填则允许所有访问
-                    allow:
+                    # 设置白名单,默认仅本机(可用 DRUID_ALLOW 覆盖)
+                    allow: ${DRUID_ALLOW:127.0.0.1}
                     url-pattern: /druid/*
                     # 控制台管理用户名和密码
-                    login-username: fs
-                    login-password: 123456
+                    login-username: ${DRUID_USERNAME:fs}
+                    login-password: ${DRUID_PASSWORD:123456}
                 filter:
                     stat:
                         enabled: true
@@ -133,7 +133,7 @@ spring:
                         merge-sql: true
                     wall:
                         config:
-                            multi-statement-allow: true
+                            multi-statement-allow: false
 #是否为新商户,新商户不走mpOpenId
 isNewWxMerchant: false
 #是否使用新im

Некоторые файлы не были показаны из-за большого количества измененных файлов