Просмотр исходного кода

药品商品支持多店铺/安全问题修复

wangxy 1 неделя назад
Родитель
Сommit
336d3fd49a
42 измененных файлов с 921 добавлено и 263 удалено
  1. 62 53
      env.secrets.example
  2. 14 5
      fs-admin/src/main/java/com/fs/his/task/Task.java
  3. 15 0
      fs-ai-call-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  4. 15 0
      fs-cid-workflow/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  5. 24 0
      fs-common/src/main/java/com/fs/common/utils/SecretMaskUtils.java
  6. 15 0
      fs-company/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  7. 15 0
      fs-ipad-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  8. 15 0
      fs-qw-api-msg/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  9. 2 4
      fs-qw-api/src/main/java/com/fs/app/controller/QwController.java
  10. 15 0
      fs-qw-api/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  11. 15 0
      fs-qw-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  12. 0 3
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  13. 7 6
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseLinkServiceImpl.java
  14. 53 4
      fs-service/src/main/java/com/fs/erp/http/JstErpHttpServiceImpl.java
  15. 6 0
      fs-service/src/main/java/com/fs/his/domain/FsStore.java
  16. 7 0
      fs-service/src/main/java/com/fs/his/domain/FsStoreProduct.java
  17. 2 1
      fs-service/src/main/java/com/fs/his/mapper/FsPrescribeDrugMapper.java
  18. 1 1
      fs-service/src/main/java/com/fs/his/mapper/FsStoreProductAttrValueMapper.java
  19. 31 11
      fs-service/src/main/java/com/fs/his/mapper/FsStoreProductMapper.java
  20. 5 0
      fs-service/src/main/java/com/fs/his/param/FsExternalOrderAddParam.java
  21. 5 0
      fs-service/src/main/java/com/fs/his/param/FsStoreProductAddEditParam.java
  22. 1 1
      fs-service/src/main/java/com/fs/his/service/IFsStoreProductService.java
  23. 53 11
      fs-service/src/main/java/com/fs/his/service/impl/FsExternalOrderServiceImpl.java
  24. 157 47
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreProductServiceImpl.java
  25. 20 3
      fs-service/src/main/java/com/fs/im/service/impl/OpenIMServiceImpl.java
  26. 13 3
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  27. 16 1
      fs-service/src/main/java/com/fs/qwApi/util/XMLParse.java
  28. 7 1
      fs-service/src/main/resources/application-common.yml
  29. 25 25
      fs-service/src/main/resources/application-config-druid-hdt.yml
  30. 21 12
      fs-service/src/main/resources/application-druid-hdt.yml
  31. 5 1
      fs-service/src/main/resources/mapper/his/FsStoreMapper.xml
  32. 9 3
      fs-service/src/main/resources/mapper/his/FsStoreProductMapper.xml
  33. 49 0
      fs-user-app/src/main/java/com/fs/app/config/SkipAuthProperties.java
  34. 48 47
      fs-user-app/src/main/java/com/fs/app/controller/CommonController.java
  35. 15 2
      fs-user-app/src/main/java/com/fs/app/controller/CompanyUserController.java
  36. 4 4
      fs-user-app/src/main/java/com/fs/app/controller/CourseOrderController.java
  37. 11 0
      fs-user-app/src/main/java/com/fs/app/controller/HealthRecordController.java
  38. 7 7
      fs-user-app/src/main/java/com/fs/app/controller/UserVipController.java
  39. 1 7
      fs-user-app/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java
  40. 105 0
      fs-user-app/src/main/java/com/fs/app/utils/SkipAuthTicketUtils.java
  41. 15 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  42. 15 0
      fs-wx-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

+ 62 - 53
env.secrets.example

@@ -1,73 +1,82 @@
-# fs-user-app / backend C-03 secrets env file
+# fs-user-app / backend secrets env template
 # Copy to env.secrets (do not commit). Load before start.
 # IDEA: Run Configuration -> Environment variables
 # PowerShell: . .\scripts\load-env-secrets.ps1
+# Fill real values on the server only.
 
 # Huawei cloud OBS/VOD/CDN
-HUAWEI_CLOUD_AK=K2UTJGIN7UTZJR2XMXYG
-HUAWEI_CLOUD_SK=sbyeNJLbcYmH6copxeFP9pAoksM4NIT9Zw4x0SRX
+HUAWEI_CLOUD_AK=
+HUAWEI_CLOUD_SK=
 
 # Tencent COS
-TENCENT_COS_SECRET_ID=AKIDiMq9lDf2EOM9lIfqqfKo7FNgM5meD0sT
-TENCENT_COS_SECRET_KEY=u5SuS80342xzx8FRBukza9lVNHKNMSaB
+TENCENT_COS_SECRET_ID=
+TENCENT_COS_SECRET_KEY=
 
 # Tencent SMS
-SMS_TENCENT_SECRET_ID=AKIDdAdvzoQpzuOo11mq2TntjKI5eqrhNp22
-SMS_TENCENT_SECRET_KEY=zmLgpzzcwAFNJ4dx2iDTVHgw3ELCrIVg
+SMS_TENCENT_SECRET_ID=
+SMS_TENCENT_SECRET_KEY=
 
 # Baidu AI
-BAIDU_AI_CLIENT_ID=LUP3P6NA0q9CEAmUhjnWXt4Q
-BAIDU_AI_CLIENT_SECRET=Esxtj5QVM2klCZV1HfIRYutW5mJ2AP5G
-BAIDU_AI_SECRET_KEY=bce-v3/ALTAK-P03N0q1ET2QKrbq3RGdgB/456be350fac6b3fe41ced46cada2cc287e04f5fc
-BAIDU_AI_PLUGINS=4v4ki9a5t1hxdv92
+BAIDU_AI_CLIENT_ID=
+BAIDU_AI_CLIENT_SECRET=
+BAIDU_AI_SECRET_KEY=
+BAIDU_AI_PLUGINS=
+
+# H-17 Aliyun market ID card OCR(源码禁止硬编码;上线后请在控制台轮换 AppCode)
+ALIYUN_IDCARD_APP_CODE=
+# optional override
+# ALIYUN_IDCARD_OCR_URL=https://idcardside.market.alicloudapi.com/idcard_ocr
 
 # Feishu token AES (16 bytes)
-FEISHU_TOKEN_AES_KEY=9f3a7c21b8e4d6a5c2f1097e3b4a6d8f
+FEISHU_TOKEN_AES_KEY=
 
 # Tulin sync
-TULIN_SYNC_SECRET_KEY=xysync_dLsSaheCzK7RU9gd
-TULIN_SYNC_PHARMACY_ID=26ee29ce-35aa-46c8-9650-3fe46014690c
-TULIN_SYNC_CORP_ID=PNBnO6MajM0tAd8t
+TULIN_SYNC_SECRET_KEY=
+TULIN_SYNC_PHARMACY_ID=
+TULIN_SYNC_CORP_ID=
 
 # Volcengine HSY
-HSY_ACCESS_KEY=AKLTZTc4YTE4ZjI2OWViNDNjZGI2NjhiYTI5Njc5ZjA1Mzk
-HSY_SECRET_KEY=WXpjelpUYzFOakF5TUdObE5EZGtNR0ZsWXpKaU1tTmtZakk1WXpObE4yRQ==
-HSY_ROLE_ACCESS_KEY=AKLTNmMwNjJkNDFhYTVjNDIzYzhhNzEyZmZmZTlmYzBhNGM
-HSY_ROLE_SECRET_KEY=T0RaaFl6UmhZV1V4WXpKbU5EWTBNMkZpT0RNNU9UY3daak0wTjJFd09XUQ==
+HSY_ACCESS_KEY=
+HSY_SECRET_KEY=
+HSY_ROLE_ACCESS_KEY=
+HSY_ROLE_SECRET_KEY=
+
+# Admin JWT token.secret(H-09:必填,≥32位强随机,无默认值)
+TOKEN_SECRET=
 
-# Admin JWT token.secret optional
-TOKEN_SECRET=abcdefghijklmnopqrstuvwxyz
+# Druid console (disabled in hdt yml; set if re-enabled)
+DRUID_LOGIN_USERNAME=
+DRUID_LOGIN_PASSWORD=
 
-# Druid console (H-02: console disabled in hdt yml; keep strong creds if ever re-enabled)
-DRUID_LOGIN_USERNAME=druid_admin
-DRUID_LOGIN_PASSWORD=ChangeMe_Druid_Strong!
+# ===== HDT =====
+HDT_REDIS_HOST=
+HDT_REDIS_PASSWORD=
+HDT_MYSQL_PASSWORD=
+HDT_SOP_MYSQL_PASSWORD=
+HDT_EASYCALL_MYSQL_PASSWORD=
+HDT_ROCKETMQ_SECRET_KEY=
+HDT_OPENIM_SECRET=
+HDT_WX_MINIAPP_SECRET_1=
+HDT_WX_MINIAPP_SECRET_2=
+HDT_WX_MINIAPP_TOKEN=
+HDT_WX_MINIAPP_AES_KEY=
+HDT_WX_CP_SECRET=
+HDT_WX_CP_TOKEN=
+HDT_WX_CP_AES_KEY=
+HDT_WX_PAY_MCH_KEY=
+HDT_WX_MP_SECRET=
+HDT_WX_MP_TOKEN=
+HDT_WX_MP_AES_KEY=
+HDT_WX_OPEN_SECRET=
+HDT_AIFABU_APP_KEY=
+HDT_WATCH_PASSWORD1=
+HDT_WATCH_PASSWORD2=
+HDT_WATCH_PASSWORD3=
+HDT_APP_JWT_SECRET=
+HDT_NUONUO_SECRET=
+HDT_TENCENT_COS_SECRET_ID=
+HDT_TENCENT_COS_SECRET_KEY=
+HDT_FEISHU_APP_SECRET=
 
-# ===== H-01 红德堂(正式服务器用,部署时整文件覆盖即可)=====
-HDT_REDIS_HOST=192.168.0.226
-HDT_REDIS_PASSWORD=Ylrztek250218!3@.
-HDT_MYSQL_PASSWORD=Ylrztek250218!3@.
-HDT_SOP_MYSQL_PASSWORD=Ylrztek250218!3@.
-HDT_EASYCALL_MYSQL_PASSWORD=easycallcenter365
-HDT_ROCKETMQ_SECRET_KEY=aZS4z!88dNndKTfhITzTpTxRrVUShtH3
-HDT_OPENIM_SECRET=openIM123
-HDT_WX_MINIAPP_SECRET_1=58910ae743005c396012b029c7def579
-HDT_WX_MINIAPP_SECRET_2=928d2961c81610d8f64b019597212fcd
-HDT_WX_MINIAPP_TOKEN=Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
-HDT_WX_MINIAPP_AES_KEY=HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
-HDT_WX_CP_SECRET=ec7okROXJqkNafq66aKNv0asTQIG0CYrj3vyBbo
-HDT_WX_CP_TOKEN=PPKOdAoMO
-HDT_WX_CP_AES_KEY=PKvaxtpSvNGpfTDmVUHIK8Wok2ESyYX24qpXJAdMP
-HDT_WX_PAY_MCH_KEY=8cab128997a347c10898b877f38
-HDT_WX_MP_SECRET=c6e5726c7039092a907c3242c8d3e406
-HDT_WX_MP_TOKEN=PPKOdAlCoMO
-HDT_WX_MP_AES_KEY=8WCur56h3f44rJ5Dq7Ab1PKy6nbOwnUHTAnvwWNFeQT
-HDT_WX_OPEN_SECRET=602e2a9ea639b6cd18eb80e715c4b22e
-HDT_AIFABU_APP_KEY=7b471be905ab17ef358c610d117601d008
-HDT_WATCH_PASSWORD1=cp-t5or_6xw7$mt
-HDT_WATCH_PASSWORD2=mdf-m2h_6yw2$hq
-HDT_WATCH_PASSWORD3=v9xsKuqn_$d2y
-HDT_APP_JWT_SECRET=hdt-zxqzbl
-HDT_NUONUO_SECRET=A2EB20764D304D16
-HDT_TENCENT_COS_SECRET_ID=AKIDLl1tguyrZ6QddTCi2BLJ4e3OXVIuJVVK
-HDT_TENCENT_COS_SECRET_KEY=g9R6kLrMp8EDzXszylLispiQxHRN6cw5
-HDT_FEISHU_APP_SECRET=3OhIlldgTAnk8yBEwHtJpdnNY6BujFvc
+# fs-user-app X-Skip-Auth HMAC secret (C-01)
+SKIP_AUTH_SECRET=

+ 14 - 5
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -1808,11 +1808,20 @@ public class Task {
         for (Map.Entry<String, BatchSendCourseAllDTO> entry : toSendMap) {
             //执行发送消息任务
             BatchSendCourseAllDTO batchSendCourseAllDTO = entry.getValue();
-            openIMService.batchUrgeCourseTask(batchSendCourseAllDTO.getOpenImBatchMsgDTO(), batchSendCourseAllDTO.getImMsgSendDetailList());
-
-            // 执行结束,删除
-            this.redisTemplate.<String, BatchSendCourseAllDTO>opsForHash().delete(redisKey, entry.getKey());
-
+            try {
+                OpenImBatchMsgDTO msgDTO = batchSendCourseAllDTO.getOpenImBatchMsgDTO();
+                if (msgDTO == null || msgDTO.getContent() == null
+                        || StringUtils.isEmpty(msgDTO.getContent().getContent())) {
+                    logger.error("会员-IM催课跳过:催课内容为空, redisField={}", entry.getKey());
+                } else {
+                    openIMService.batchUrgeCourseTask(msgDTO, batchSendCourseAllDTO.getImMsgSendDetailList());
+                }
+            } catch (Exception e) {
+                logger.error("会员-IM催课发送异常, redisField={}", entry.getKey(), e);
+            } finally {
+                // 执行结束(成功/失败/跳过)都删除,避免空内容任务反复报错
+                this.redisTemplate.<String, BatchSendCourseAllDTO>opsForHash().delete(redisKey, entry.getKey());
+            }
         }
     }
 

+ 15 - 0
fs-ai-call-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -67,6 +67,7 @@ public class DataScopeAspect
     @Before("dataScopePointCut()")
     public void doBefore(JoinPoint point) throws Throwable
     {
+        clearDataScope(point);
         handleDataScope(point);
     }
 
@@ -168,4 +169,18 @@ public class DataScopeAspect
         }
         return null;
     }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+
 }

+ 15 - 0
fs-cid-workflow/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -67,6 +67,7 @@ public class DataScopeAspect
     @Before("dataScopePointCut()")
     public void doBefore(JoinPoint point) throws Throwable
     {
+        clearDataScope(point);
         handleDataScope(point);
     }
 
@@ -168,4 +169,18 @@ public class DataScopeAspect
         }
         return null;
     }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+
 }

+ 24 - 0
fs-common/src/main/java/com/fs/common/utils/SecretMaskUtils.java

@@ -0,0 +1,24 @@
+package com.fs.common.utils;
+
+/**
+ * 日志脱敏:Token / 密钥等敏感串最多保留前4后4。
+ */
+public final class SecretMaskUtils {
+
+    private SecretMaskUtils() {
+    }
+
+    /**
+     * 将敏感串脱敏为 abcd****wxyz;过短则全部打码。
+     */
+    public static String mask(String secret) {
+        if (secret == null || secret.isEmpty()) {
+            return "";
+        }
+        int len = secret.length();
+        if (len <= 8) {
+            return "****";
+        }
+        return secret.substring(0, 4) + "****" + secret.substring(len - 4);
+    }
+}

+ 15 - 0
fs-company/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -80,6 +80,7 @@ public class DataScopeAspect
     @Before("dataScopePointCut()")
     public void doBefore(JoinPoint point) throws Throwable
     {
+        clearDataScope(point);
         handleDataScope(point);
     }
 
@@ -234,4 +235,18 @@ public class DataScopeAspect
         }
         return null;
     }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+
 }

+ 15 - 0
fs-ipad-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -67,6 +67,7 @@ public class DataScopeAspect
     @Before("dataScopePointCut()")
     public void doBefore(JoinPoint point) throws Throwable
     {
+        clearDataScope(point);
         handleDataScope(point);
     }
 
@@ -168,4 +169,18 @@ public class DataScopeAspect
         }
         return null;
     }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+
 }

+ 15 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -67,6 +67,7 @@ public class DataScopeAspect
     @Before("dataScopePointCut()")
     public void doBefore(JoinPoint point) throws Throwable
     {
+        clearDataScope(point);
         handleDataScope(point);
     }
 
@@ -168,4 +169,18 @@ public class DataScopeAspect
         }
         return null;
     }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+
 }

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

@@ -99,10 +99,8 @@ public class QwController {
         try{
             String sMsg = wxcpt.DecryptMsg(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sData);
             logger.info("企业微信回调事件:"+sMsg);
-            // 加密成功
-            // TODO: 解析出明文xml标签的内容进行处理
-            // For example:
-            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+            // 加密成功:解析明文 XML(使用加固 Factory,防 XXE)
+            DocumentBuilderFactory dbf = XMLParse.newSecureDocumentBuilderFactory();
             DocumentBuilder db = dbf.newDocumentBuilder();
             StringReader sr = new StringReader(sMsg);
             InputSource is = new InputSource(sr);

+ 15 - 0
fs-qw-api/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -67,6 +67,7 @@ public class DataScopeAspect
     @Before("dataScopePointCut()")
     public void doBefore(JoinPoint point) throws Throwable
     {
+        clearDataScope(point);
         handleDataScope(point);
     }
 
@@ -168,4 +169,18 @@ public class DataScopeAspect
         }
         return null;
     }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+
 }

+ 15 - 0
fs-qw-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -67,6 +67,7 @@ public class DataScopeAspect
     @Before("dataScopePointCut()")
     public void doBefore(JoinPoint point) throws Throwable
     {
+        clearDataScope(point);
         handleDataScope(point);
     }
 
@@ -168,4 +169,18 @@ public class DataScopeAspect
         }
         return null;
     }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+
 }

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

@@ -347,9 +347,6 @@ public interface CompanyUserMapper
 
     String selectMpOpenIdByCompanyUserId(@Param("companyUserId") Long companyUserId);
 
-    @Update("${sql}")
-    int updateBySql(@Param("sql") String sql);
-
     @Select("SELECT * FROM company_user WHERE parent_id = #{userId} AND del_flag = '0'")
     List<CompanyUser> selectSubordinateList(@Param("userId") Long userId);
 

+ 7 - 6
fs-service/src/main/java/com/fs/course/service/impl/FsCourseLinkServiceImpl.java

@@ -11,6 +11,7 @@ import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.PubFun;
+import com.fs.common.utils.SecretMaskUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.date.DateUtil;
@@ -1027,7 +1028,7 @@ public class FsCourseLinkServiceImpl implements IFsCourseLinkService
             
             final WxMaService wxService = WxMaConfiguration.getMaService(currentAppId);
             String token = wxService.getAccessToken();
-            log.info("小程序TOKEN值-------->刷新前TOKEN:{}", token);
+            log.info("小程序TOKEN值-------->刷新前TOKEN:{}", SecretMaskUtils.mask(token));
             HttpPost httpPost = new HttpPost("https://api.weixin.qq.com/wxa/generatescheme?access_token=" + token);
             JSONObject bodyObj = new JSONObject();
             JSONObject jumpWxa = new JSONObject();
@@ -1052,10 +1053,10 @@ public class FsCourseLinkServiceImpl implements IFsCourseLinkService
                 Integer curVersion = Integer.valueOf(version);
                 synchronized (TOKEN_VALID_CODE){
                     if(curVersion.equals(version)){
-                        log.info("小程序TOKEN:40001进入强制刷新-------->刷新前TOKEN:{}", token);
+                        log.info("小程序TOKEN:40001进入强制刷新-------->刷新前TOKEN:{}", SecretMaskUtils.mask(token));
                         wxService.getAccessToken(true);
                         version = version.equals(Integer.MAX_VALUE) ? 0 : curVersion + 1;
-                        log.info("小程序TOKEN:40001进入强制刷新-------->刷新后TOKEN:{}", wxService.getAccessToken());
+                        log.info("小程序TOKEN:40001进入强制刷新-------->刷新后TOKEN:{}", SecretMaskUtils.mask(wxService.getAccessToken()));
                     }
                     return generateWxAppLinkWithRetry(linkStr, appIdList, currentIndex);
                 }
@@ -1156,7 +1157,7 @@ public class FsCourseLinkServiceImpl implements IFsCourseLinkService
 
             final WxMaService wxService = WxMaConfiguration.getMaService(currentAppId);
             String token = wxService.getAccessToken();
-            log.info("小程序TOKEN值-------->刷新前TOKEN:{}", token);
+            log.info("小程序TOKEN值-------->刷新前TOKEN:{}", SecretMaskUtils.mask(token));
             HttpPost httpPost = new HttpPost("https://api.weixin.qq.com/wxa/generate_urllink?access_token=" + token);
             JSONObject bodyObj = new JSONObject();
             bodyObj.put("path", pageUrl);
@@ -1175,10 +1176,10 @@ public class FsCourseLinkServiceImpl implements IFsCourseLinkService
                 Integer curVersion = Integer.valueOf(version);
                 synchronized (TOKEN_VALID_CODE){
                     if(curVersion.equals(version)){
-                        log.info("小程序TOKEN:40001进入强制刷新-------->刷新前TOKEN:{}", token);
+                        log.info("小程序TOKEN:40001进入强制刷新-------->刷新前TOKEN:{}", SecretMaskUtils.mask(token));
                         wxService.getAccessToken(true);
                         version = version.equals(Integer.MAX_VALUE) ? 0 : curVersion + 1;
-                        log.info("小程序TOKEN:40001进入强制刷新-------->刷新后TOKEN:{}", wxService.getAccessToken());
+                        log.info("小程序TOKEN:40001进入强制刷新-------->刷新后TOKEN:{}", SecretMaskUtils.mask(wxService.getAccessToken()));
                     }
                     return generateWxAppLinkWithRetryForPreOrder(linkStr, appIdList, currentIndex);
                 }

+ 53 - 4
fs-service/src/main/java/com/fs/erp/http/JstErpHttpServiceImpl.java

@@ -9,6 +9,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.PropertyNamingStrategy;
 import com.alibaba.fastjson.TypeReference;
 import com.alibaba.fastjson.serializer.SerializeConfig;
+import com.fs.common.utils.SecretMaskUtils;
 import com.fs.erp.dto.*;
 import com.fs.erp.service.impl.JstTokenService;
 import com.fs.erp.utils.SignUtil;
@@ -68,7 +69,7 @@ public class JstErpHttpServiceImpl implements JstErpHttpService {
     public GetInitTokenResponseDTO getInitToken(GetInitTokenRequestDTO dto) {
         // 将DTO转换为表单参数
         Map<String, Object> formParams = BeanUtil.beanToMap(dto);
-        log.info("请求初始化token - URL: {}, 参数: {}", GET_INIT_TOKEN_URL, JSON.toJSONString(formParams));
+        log.info("请求初始化token - URL: {}, 参数: {}", GET_INIT_TOKEN_URL, JSON.toJSONString(maskJstAuthParams(formParams)));
 
         HttpResponse response = executeFormPost(GET_INIT_TOKEN_URL, formParams);
         return parseResponse(response, new TypeReference<CommonResponse<GetInitTokenResponseDTO>>() {});
@@ -76,8 +77,8 @@ public class JstErpHttpServiceImpl implements JstErpHttpService {
 
     @Override
     public GetInitTokenResponseDTO refreshToken(RefreshTokenRequestDTO dto) {
-        log.info("刷新token - URL: {}, 请求体: {}", REFRESH_TOKEN_URL, JSON.toJSONString(dto));
         Map<String, Object> formParams = BeanUtil.beanToMap(dto);
+        log.info("刷新token - URL: {}, 请求体: {}", REFRESH_TOKEN_URL, JSON.toJSONString(maskJstAuthParams(formParams)));
 
         HttpResponse response = executeFormPost(REFRESH_TOKEN_URL, formParams);
         return parseResponse(response, new TypeReference<CommonResponse<GetInitTokenResponseDTO>>() {});
@@ -298,7 +299,7 @@ public class JstErpHttpServiceImpl implements JstErpHttpService {
         }
 
         T result = commonResponse.getData();
-        log.info("API请求成功 - 结果: {}", JSON.toJSONString(result));
+        log.info("API请求成功 - 结果: {}", maskSensitiveJson(result));
         return result;
     }
 
@@ -319,7 +320,55 @@ public class JstErpHttpServiceImpl implements JstErpHttpService {
             throw new RuntimeException("请求接口失败!原因:" + commonResponse.getMsg());
         }
 
-        log.info("API请求成功 - 结果: {}", JSON.toJSONString(commonResponse));
+        log.info("API请求成功 - 结果: {}", maskSensitiveJson(commonResponse));
         return commonResponse;
     }
+
+    /** 日志用:脱敏 sign / token / code 等字段,不改动原请求参数 */
+    private Map<String, Object> maskJstAuthParams(Map<String, Object> params) {
+        Map<String, Object> copy = new HashMap<>(params);
+        for (String key : Arrays.asList("sign", "code", "refresh_token", "access_token", "app_secret")) {
+            Object v = copy.get(key);
+            if (v != null) {
+                copy.put(key, SecretMaskUtils.mask(String.valueOf(v)));
+            }
+        }
+        return copy;
+    }
+
+    private String maskSensitiveJson(Object obj) {
+        if (obj == null) {
+            return "null";
+        }
+        Object json = JSON.toJSON(obj);
+        if (json instanceof JSONObject) {
+            JSONObject copy = new JSONObject((JSONObject) json);
+            for (String key : Arrays.asList("sign", "code", "refresh_token", "access_token", "app_secret", "data")) {
+                if ("data".equals(key) && copy.get(key) instanceof JSONObject) {
+                    JSONObject data = new JSONObject(copy.getJSONObject(key));
+                    for (String dk : Arrays.asList("refresh_token", "access_token")) {
+                        if (data.get(dk) != null) {
+                            data.put(dk, SecretMaskUtils.mask(String.valueOf(data.get(dk))));
+                        }
+                    }
+                    copy.put("data", data);
+                } else if (copy.get(key) != null && !"data".equals(key)) {
+                    copy.put(key, SecretMaskUtils.mask(String.valueOf(copy.get(key))));
+                }
+            }
+            return copy.toJSONString();
+        }
+        if (obj instanceof GetInitTokenResponseDTO) {
+            GetInitTokenResponseDTO dto = (GetInitTokenResponseDTO) obj;
+            JSONObject copy = (JSONObject) JSON.toJSON(dto);
+            if (copy.get("access_token") != null) {
+                copy.put("access_token", SecretMaskUtils.mask(copy.getString("access_token")));
+            }
+            if (copy.get("refresh_token") != null) {
+                copy.put("refresh_token", SecretMaskUtils.mask(copy.getString("refresh_token")));
+            }
+            return copy.toJSONString();
+        }
+        return JSON.toJSONString(obj);
+    }
 }

+ 6 - 0
fs-service/src/main/java/com/fs/his/domain/FsStore.java

@@ -118,4 +118,10 @@ public class FsStore extends BaseEntity
     @Excel(name = "可发货连锁品牌")
     private String chainBrands;
 
+    /**
+     * ERP同步品牌别名(逗号分隔,如:乘济大,乘济大品牌;用于 brand 与店名不一致时归属匹配)
+     */
+    @Excel(name = "ERP品牌别名")
+    private String erpBrandAliases;
+
 }

+ 7 - 0
fs-service/src/main/java/com/fs/his/domain/FsStoreProduct.java

@@ -256,4 +256,11 @@ public class FsStoreProduct extends BaseEntity {
      */
     @Excel(name = "连锁品牌")
     private String chainBrand;
+
+    /**
+     * 大品牌归属店铺IDs(逗号分隔)。ERP brand 与店名精确匹配失败时,
+     * 按各店 erp_brand_aliases 命中写入,支持一品多店。
+     */
+    @Excel(name = "品牌归属店铺IDs")
+    private String brandStoreIds;
 }

+ 2 - 1
fs-service/src/main/java/com/fs/his/mapper/FsPrescribeDrugMapper.java

@@ -68,7 +68,8 @@ public interface FsPrescribeDrugMapper
     List<FsPrescribeDrug> selectFsPrescribeDrugListByImg();
     @Select("SELECT * FROM fs_doctor_prescribe_drug WHERE drug_img_url is not NULL AND drug_img_url!='' AND drug_img_url LIKE '%htj-1258038825.cos.ap-beijing.myqcloud.com%' ")
     List<FsDoctorPrescribeDrug> selectFsPrescribeDrugListByImg2();
-    @Select("select * from fs_store_product where product_name=#{drugName} and store_id=#{storeId} LIMIT 1 ")
+    @Select("select * from fs_store_product where product_name=#{drugName} " +
+            "and (store_id=#{storeId} or (store_id is null and FIND_IN_SET(#{storeId}, IFNULL(brand_store_ids, '')))) LIMIT 1 ")
     FsStoreProduct selectFsPrescribeDrugCode(@Param("drugName") String drugName, @Param("storeId")Long storeId);
 
     List<FsPrescribeDrugDTO> selectDrugList(Long prescribeId);

+ 1 - 1
fs-service/src/main/java/com/fs/his/mapper/FsStoreProductAttrValueMapper.java

@@ -87,7 +87,7 @@ public interface FsStoreProductAttrValueMapper
             "and p.product_name  LIKE CONCAT('%',#{maps.keyword},'%')   " +
             "</if>" +
             "<if test = ' maps.storeId!=null  '> " +
-            "and p.store_id = #{maps.storeId} " +
+            "and (p.store_id = #{maps.storeId} or (p.store_id is null and FIND_IN_SET(#{maps.storeId}, IFNULL(p.brand_store_ids, '')))) " +
             "</if>" +
             "<if test = ' maps.productType!=null  '> " +
             "and p.product_type = #{maps.productType} " +

+ 31 - 11
fs-service/src/main/java/com/fs/his/mapper/FsStoreProductMapper.java

@@ -39,7 +39,7 @@ public interface FsStoreProductMapper {
 
 
     @Select({"<script> " +
-            "select p.*,pc.cate_name,st.store_name  from fs_store_product p left join fs_store_product_category pc on p.cate_id=pc.cate_id LEFT JOIN fs_store st ON st.store_id=p.store_id  " +
+            "select p.*,pc.cate_name,(CASE WHEN p.store_id IS NOT NULL THEN st.store_name WHEN IFNULL(p.brand_store_ids,'') != '' THEN (SELECT GROUP_CONCAT(s2.store_name ORDER BY FIND_IN_SET(s2.store_id, p.brand_store_ids) SEPARATOR ',') FROM fs_store s2 WHERE FIND_IN_SET(s2.store_id, p.brand_store_ids)) ELSE NULL END) as store_name from fs_store_product p left join fs_store_product_category pc on p.cate_id=pc.cate_id LEFT JOIN fs_store st ON st.store_id=p.store_id  " +
             "where 1=1 " +
             "<if test = 'maps.productName != null and  maps.productName !=\"\"    '> " +
             "and p.product_name like CONCAT('%',#{maps.productName},'%') " +
@@ -54,7 +54,7 @@ public interface FsStoreProductMapper {
             "and p.is_show =#{maps.isShow} " +
             "</if>" +
             "<if test = 'maps.storeId != null    '> " +
-            "and p.store_id =#{maps.storeId} " +
+            "and (p.store_id = #{maps.storeId} or (p.store_id is null and FIND_IN_SET(#{maps.storeId}, IFNULL(p.brand_store_ids, '')))) " +
             "</if>" +
             "<if test = 'maps.barCode != null    '> " +
             "and p.product_id in(SELECT product_id FROM fs_store_product_attr_value WHERE bar_code =#{maps.barCode}) " +
@@ -64,7 +64,7 @@ public interface FsStoreProductMapper {
     public List<FsStoreProductVO> selectFsStoreProductListVO(@Param("maps") FsStoreProduct fsStoreProduct);
 
     @Select({"<script> " +
-            "select p.*,pc.cate_name,st.store_name  from fs_store_product p left join fs_store_product_category pc on p.cate_id=pc.cate_id LEFT JOIN fs_store st ON st.store_id=p.store_id  " +
+            "select p.*,pc.cate_name,(CASE WHEN p.store_id IS NOT NULL THEN st.store_name WHEN IFNULL(p.brand_store_ids,'') != '' THEN (SELECT GROUP_CONCAT(s2.store_name ORDER BY FIND_IN_SET(s2.store_id, p.brand_store_ids) SEPARATOR ',') FROM fs_store s2 WHERE FIND_IN_SET(s2.store_id, p.brand_store_ids)) ELSE NULL END) as store_name from fs_store_product p left join fs_store_product_category pc on p.cate_id=pc.cate_id LEFT JOIN fs_store st ON st.store_id=p.store_id  " +
             "where 1=1 " +
             "<if test = 'maps.productName != null and  maps.productName !=\"\"    '> " +
             "and p.product_name like CONCAT('%',#{maps.productName},'%') " +
@@ -79,7 +79,7 @@ public interface FsStoreProductMapper {
             "and p.is_show =#{maps.isShow} " +
             "</if>" +
             "<if test = 'maps.storeId != null    '> " +
-            "and p.store_id =#{maps.storeId} " +
+            "and (p.store_id = #{maps.storeId} or (p.store_id is null and FIND_IN_SET(#{maps.storeId}, IFNULL(p.brand_store_ids, '')))) " +
             "</if>" +
             "<if test = 'maps.barCode != null    '> " +
             "and p.product_id in(SELECT product_id FROM fs_store_product_attr_value WHERE bar_code =#{maps.barCode}) " +
@@ -158,7 +158,7 @@ public interface FsStoreProductMapper {
             "and p.is_show =#{maps.isShow} " +
             "</if>" +
             "<if test = 'maps.storeId != null    '> " +
-            "and p.store_id =#{maps.storeId} " +
+            "and (p.store_id = #{maps.storeId} or (p.store_id is null and FIND_IN_SET(#{maps.storeId}, IFNULL(p.brand_store_ids, '')))) " +
             "</if>" +
             "<if test = 'maps.barCode != null    '> " +
             "and p.product_id in(SELECT product_id FROM fs_store_product_attr_value WHERE bar_code =#{maps.barCode}) " +
@@ -176,7 +176,7 @@ public interface FsStoreProductMapper {
     int updateFsStoreProductPrice(@Param("ids") List<Long> ids, @Param("price") double price);
 
     @Select({"<script> " +
-            "select p.*,pc.cate_name,st.store_name  from fs_store_product p left join fs_store_product_category pc on p.cate_id=pc.cate_id LEFT JOIN fs_store st ON st.store_id=p.store_id  " +
+            "select p.*,pc.cate_name,(CASE WHEN p.store_id IS NOT NULL THEN st.store_name WHEN IFNULL(p.brand_store_ids,'') != '' THEN (SELECT GROUP_CONCAT(s2.store_name ORDER BY FIND_IN_SET(s2.store_id, p.brand_store_ids) SEPARATOR ',') FROM fs_store s2 WHERE FIND_IN_SET(s2.store_id, p.brand_store_ids)) ELSE NULL END) as store_name from fs_store_product p left join fs_store_product_category pc on p.cate_id=pc.cate_id LEFT JOIN fs_store st ON st.store_id=p.store_id  " +
             "where 1=1 " +
             "<if test = 'maps.productName != null and  maps.productName !=\"\"    '> " +
             "and p.product_name like CONCAT('%',#{maps.productName},'%') " +
@@ -191,7 +191,7 @@ public interface FsStoreProductMapper {
             "and p.is_show =#{maps.isShow} " +
             "</if>" +
             "<if test = 'maps.storeId != null    '> " +
-            "and p.store_id =#{maps.storeId} " +
+            "and (p.store_id = #{maps.storeId} or (p.store_id is null and FIND_IN_SET(#{maps.storeId}, IFNULL(p.brand_store_ids, '')))) " +
             "</if>" +
             "<if test = 'maps.barCode != null    '> " +
             "and p.product_id in(SELECT product_id FROM fs_store_product_attr_value WHERE bar_code =#{maps.barCode}) " +
@@ -201,13 +201,13 @@ public interface FsStoreProductMapper {
     List<FsStoreProductExcelVO> selectFsStoreProductExcelVO(@Param("maps") FsStoreProduct fsStoreProduct);
 
     @Select({"<script> " +
-            "select v.*,p.product_name,p.product_type,p.is_drug,c.cate_name,store.store_name,p.prescribe_spec  from fs_store_product_attr_value v inner join fs_store_product p on p.product_id=v.product_id left join fs_store_product_category c on c.cate_id=p.cate_id LEFT JOIN fs_store store ON store.store_id=p.store_id      " +
+            "select v.*,p.product_name,p.product_type,p.is_drug,c.cate_name,(CASE WHEN p.store_id IS NOT NULL THEN store.store_name WHEN IFNULL(p.brand_store_ids,'') != '' THEN (SELECT GROUP_CONCAT(s2.store_name ORDER BY FIND_IN_SET(s2.store_id, p.brand_store_ids) SEPARATOR ',') FROM fs_store s2 WHERE FIND_IN_SET(s2.store_id, p.brand_store_ids)) ELSE NULL END) as store_name,p.prescribe_spec  from fs_store_product_attr_value v inner join fs_store_product p on p.product_id=v.product_id left join fs_store_product_category c on c.cate_id=p.cate_id LEFT JOIN fs_store store ON store.store_id=p.store_id      " +
             "where 1=1 and v.bar_code is not null " +
             "<if test = 'maps.productName != null and  maps.productName !=\"\"    '> " +
             "and p.product_name like CONCAT('%',#{maps.productName},'%') " +
             "</if>" +
             "<if test = 'maps.storeId != null and  maps.storeId !=\"\"    '> " +
-            "and p.store_id =#{maps.storeId} " +
+            "and (p.store_id = #{maps.storeId} or (p.store_id is null and FIND_IN_SET(#{maps.storeId}, IFNULL(p.brand_store_ids, '')))) " +
             "</if>" +
             "<if test = 'maps.productType != null    '> " +
             "and p.product_type = #{maps.productType}  " +
@@ -230,7 +230,7 @@ public interface FsStoreProductMapper {
     @Select("select * FROM fs_store_product where bar_code =#{barCode} and (store_id = 30 or store_id = 33)")
     List<FsStoreProduct> selectCode(String barCode);
 
-    @Select("select v.*,p.product_name,p.product_type,p.is_drug,c.cate_name,store.store_name,p.prescribe_spec  from fs_store_product_attr_value v inner join fs_store_product p on p.product_id=v.product_id left join fs_store_product_category c on c.cate_id=p.cate_id LEFT JOIN fs_store store ON store.store_id=p.store_id\n" +
+    @Select("select v.*,p.product_name,p.product_type,p.is_drug,c.cate_name,(CASE WHEN p.store_id IS NOT NULL THEN store.store_name WHEN IFNULL(p.brand_store_ids,'') != '' THEN (SELECT GROUP_CONCAT(s2.store_name ORDER BY FIND_IN_SET(s2.store_id, p.brand_store_ids) SEPARATOR ',') FROM fs_store s2 WHERE FIND_IN_SET(s2.store_id, p.brand_store_ids)) ELSE NULL END) as store_name,p.prescribe_spec  from fs_store_product_attr_value v inner join fs_store_product p on p.product_id=v.product_id left join fs_store_product_category c on c.cate_id=p.cate_id LEFT JOIN fs_store store ON store.store_id=p.store_id\n" +
             "where 1=1 and v.product_id=#{productId} limit 1")
     FsStoreProductAttrValueVO selectFsStoreProductAttrValueVOByProdId(Long productId);
 
@@ -261,7 +261,8 @@ public interface FsStoreProductMapper {
             "</script>"})
     Long selectFsStoreProductCount(int type);
 
-    @Select(" select * from fs_store_product where product_id = #{productId} and store_id = #{storeId}")
+    @Select("select * from fs_store_product where product_id = #{productId} " +
+            "and (store_id = #{storeId} or (store_id is null and FIND_IN_SET(#{storeId}, IFNULL(brand_store_ids, ''))))")
     FsStoreProduct getStoreProductByProductIdAndStoreId(@Param("productId") Long productId, @Param("storeId") Long storeId);
 
     @Update({"UPDATE fs_store_product SET stock=#{stock}  WHERE product_id = #{productId} "})
@@ -297,4 +298,23 @@ public interface FsStoreProductMapper {
             "SET p.store_id = s.store_id " +
             "WHERE p.brand IS NOT NULL AND p.brand != ''")
     int updateStoreIdByBrandMatch();
+
+    /**
+     * ERP同步:仅按店名命中时更新 store_id(不改 brand_store_ids)
+     */
+    @Update("UPDATE fs_store_product SET store_id = #{storeId} WHERE product_id = #{productId}")
+    int updateErpStoreIdOnly(@Param("productId") Long productId, @Param("storeId") Long storeId);
+
+    /**
+     * ERP同步:大包别名多店命中时写 brand_store_ids,并清空 store_id
+     */
+    @Update("UPDATE fs_store_product SET store_id = NULL, brand_store_ids = #{brandStoreIds} WHERE product_id = #{productId}")
+    int updateErpBrandStoreIdsClearStoreId(@Param("productId") Long productId,
+                                          @Param("brandStoreIds") String brandStoreIds);
+
+    /**
+     * 后台手动归属:单店写 store_id,并清空 brand_store_ids
+     */
+    @Update("UPDATE fs_store_product SET store_id = #{storeId}, brand_store_ids = NULL WHERE product_id = #{productId}")
+    int updateStoreIdClearBrandStoreIds(@Param("productId") Long productId, @Param("storeId") Long storeId);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/his/param/FsExternalOrderAddParam.java

@@ -43,4 +43,9 @@ public class FsExternalOrderAddParam {
 
     private  BigDecimal totalPrice;
 
+    /**
+     * 下单时选择的店铺(大品牌商品 product.storeId 可能为空,需用此字段写订单店铺)
+     */
+    private Long storeId;
+
 }

+ 5 - 0
fs-service/src/main/java/com/fs/his/param/FsStoreProductAddEditParam.java

@@ -168,6 +168,11 @@ public class FsStoreProductAddEditParam implements Serializable {
     @Excel(name = "连锁品牌")
     private String chainBrand;
 
+    /**
+     * 大品牌归属店铺IDs(逗号分隔)。有值时 storeId 为空,表示一品多店。
+     */
+    private String brandStoreIds;
+
     //属性项目
     private List<ProductArrtDTO> items;
     //sku结果集

+ 1 - 1
fs-service/src/main/java/com/fs/his/service/IFsStoreProductService.java

@@ -125,7 +125,7 @@ public interface IFsStoreProductService
     int replaceErpImageUrls();
 
     /**
-     * 根据商品brand字段匹配店铺名称,更新商品的store_id
+     * 根据商品brand字段匹配店铺:店名精确只写 store_id;大包别名写 brand_store_ids 并清空 store_id
      * @return 更新成功的记录数
      */
     int updateStoreIdByBrandMatch();

+ 53 - 11
fs-service/src/main/java/com/fs/his/service/impl/FsExternalOrderServiceImpl.java

@@ -344,7 +344,6 @@ public class FsExternalOrderServiceImpl implements IFsExternalOrderService {
         Long totalNum = 0L;
         BigDecimal totalPrice = BigDecimal.ZERO;
         List<FsExternalOrderItem> items = new ArrayList<>();
-        Long storeId = 0L;
         for (FsStoreProduct product : storeProductList) {
             Long num = productNumMap.getOrDefault(product.getProductId(), 1L);
             BigDecimal price = productPriceMap.get(product.getProductId());
@@ -368,16 +367,22 @@ public class FsExternalOrderServiceImpl implements IFsExternalOrderService {
             item.setCostPrice(product.getCostPrice());
             item.setDeliveryStatus(0);
             items.add(item);
-            storeId = product.getStoreId();
         }
-        if(storeId !=null){
-            FsStore fsStore = fsStoreMapper.selectFsStoreByStoreId(storeId);
-            String orderCode = null;
-            if(fsStore !=null){
-                orderCode=fsStore.getShopCode();
+        // 下单选店优先;大品牌商品 store_id 为空,不能从商品反推店铺
+        Long resolveStoreId = param.getStoreId();
+        if (resolveStoreId == null) {
+            for (FsStoreProduct product : storeProductList) {
+                if (product.getStoreId() != null) {
+                    resolveStoreId = product.getStoreId();
+                    break;
+                }
+            }
+        }
+        if (resolveStoreId != null) {
+            FsStore fsStore = fsStoreMapper.selectFsStoreByStoreId(resolveStoreId);
+            if (fsStore != null && StringUtils.isNotBlank(fsStore.getShopCode())) {
+                order.setStoreId(Long.valueOf(fsStore.getShopCode()));
             }
-            assert orderCode != null;
-            order.setStoreId(Long.valueOf(orderCode));
         }
         order.setTotalNum(totalNum);
         order.setTotalPrice(param.getTotalPrice()!=null?param.getTotalPrice():totalPrice);
@@ -690,7 +695,7 @@ public class FsExternalOrderServiceImpl implements IFsExternalOrderService {
                 request.setShop_id(fsStore.getShopCode());
                 request.setO_ids(Collections.singletonList(Long.valueOf(order.getExtendOrderId())));
 
-                ErpOrderQueryResponse response = jstOrderService.getOrder(request);
+                ErpOrderQueryResponse response = queryJstOrderWithRetry(request, order.getOrderCode());
                 if (response == null || response.getOrders() == null) {
                     log.warn("查询聚水潭订单失败: {}", order.getOrderCode());
                     continue;
@@ -712,15 +717,52 @@ public class FsExternalOrderServiceImpl implements IFsExternalOrderService {
                     }
                 }
 
-                Thread.sleep(200);
+                // 控制调用频率,降低聚水潭 199「调用太频繁」
+                Thread.sleep(800);
+            } catch (InterruptedException ie) {
+                Thread.currentThread().interrupt();
+                log.warn("同步外部订单状态被中断");
+                break;
             } catch (Exception e) {
                 log.error("同步外部订单状态异常: {}", order.getOrderCode(), e);
+                try {
+                    Thread.sleep(800);
+                } catch (InterruptedException ie) {
+                    Thread.currentThread().interrupt();
+                    break;
+                }
             }
         }
 
         log.info("外部订单状态同步完成");
     }
 
+    /**
+     * 查询聚水潭订单:遇到限流(199/调用太频繁)时退避重试
+     */
+    private ErpOrderQueryResponse queryJstOrderWithRetry(ErpOrderQueryRequert request, String orderCode) throws Exception {
+        final int maxRetry = 3;
+        long backoffMs = 2000L;
+        Exception last = null;
+        for (int attempt = 1; attempt <= maxRetry; attempt++) {
+            try {
+                return jstOrderService.getOrder(request);
+            } catch (Exception e) {
+                last = e;
+                String msg = e.getMessage() == null ? "" : e.getMessage();
+                boolean rateLimited = msg.contains("调用太频繁") || msg.contains("199");
+                if (!rateLimited || attempt == maxRetry) {
+                    throw e;
+                }
+                log.warn("聚水潭限流,第{}次重试前等待{}ms,订单: {}, 原因: {}",
+                        attempt, backoffMs, orderCode, msg);
+                Thread.sleep(backoffMs);
+                backoffMs = Math.min(backoffMs * 2, 8000L);
+            }
+        }
+        throw last != null ? last : new RuntimeException("查询聚水潭订单失败: " + orderCode);
+    }
+
     private void updateOrderDelivery(FsExternalOrder localOrder, ErpDeliverys delivery) {
         if (delivery == null || StringUtils.isBlank(delivery.getMail_no())) {
             return;

+ 157 - 47
fs-service/src/main/java/com/fs/his/service/impl/FsStoreProductServiceImpl.java

@@ -53,6 +53,8 @@ import org.springframework.stereotype.Service;
 import com.fs.his.mapper.FsStoreProductMapper;
 import com.fs.his.mapper.FsStoreMapper;
 import com.fs.his.service.IFsStoreProductService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.transaction.annotation.Transactional;
 
 /**
@@ -63,6 +65,7 @@ import org.springframework.transaction.annotation.Transactional;
  */
 @Service
 public class FsStoreProductServiceImpl implements IFsStoreProductService {
+    private static final Logger log = LoggerFactory.getLogger(FsStoreProductServiceImpl.class);
     private static final DateTimeFormatter ERP_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
     private static final int MAX_QUERY_RANGE_DAYS = 7;
     private static final int DEFAULT_ERP_PAGE_SIZE = 100;
@@ -265,24 +268,21 @@ public class FsStoreProductServiceImpl implements IFsStoreProductService {
                 List<FsStoreProductAttrValue> values = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueList(attrValueMap);
 
                 if (values != null && values.size() == 1) {
-                    valueMap.put("id", values.get(0).getId());
-                    valueMap.put("sku", values.get(0).getSku());
-                    valueMap.put("image", values.get(0).getImage());
-                    valueMap.put("price", values.get(0).getPrice());
-                    valueMap.put("cost", values.get(0).getCost());
-                    valueMap.put("otPrice", values.get(0).getOtPrice());
-                    valueMap.put("stock", values.get(0).getStock());
-                    valueMap.put("barCode", values.get(0).getBarCode());
-                    valueMap.put("groupBarCode", values.get(0).getGroupBarCode());
-                    valueMap.put("weight", values.get(0).getWeight());
-                    valueMap.put("volume", values.get(0).getVolume());
-                    valueMap.put("doctorBrokerage", values.get(0).getDoctorBrokerage());
-
-                    valueMap.put("brokerage", values.get(0).getBrokerage());
-//                    valueMap.put("brokerageTwo", values.get(0).getBrokerageTwo());
-//                    valueMap.put("brokerageThree", values.get(0).getBrokerageThree());
-                    System.out.println("积分 :" + values.get(0).getGiveIntegral());
-                    valueMap.put("giveIntegral", values.get(0).getGiveIntegral());
+                    FsStoreProductAttrValue dbVal = values.get(0);
+                    valueMap.put("id", dbVal.getId());
+                    valueMap.put("sku", dbVal.getSku());
+                    valueMap.put("image", dbVal.getImage());
+                    valueMap.put("price", dbVal.getPrice() != null ? dbVal.getPrice() : 0);
+                    valueMap.put("cost", dbVal.getCost() != null ? dbVal.getCost() : 0);
+                    valueMap.put("otPrice", dbVal.getOtPrice() != null ? dbVal.getOtPrice() : 0);
+                    valueMap.put("stock", dbVal.getStock() != null ? dbVal.getStock() : 0);
+                    valueMap.put("barCode", dbVal.getBarCode());
+                    valueMap.put("groupBarCode", dbVal.getGroupBarCode());
+                    valueMap.put("weight", dbVal.getWeight() != null ? dbVal.getWeight() : 0);
+                    valueMap.put("volume", dbVal.getVolume() != null ? dbVal.getVolume() : 0);
+                    valueMap.put("doctorBrokerage", dbVal.getDoctorBrokerage() != null ? dbVal.getDoctorBrokerage() : 0);
+                    valueMap.put("brokerage", dbVal.getBrokerage() != null ? dbVal.getBrokerage() : 0);
+                    valueMap.put("giveIntegral", dbVal.getGiveIntegral() != null ? dbVal.getGiveIntegral() : 0);
                 }
             }
             valueMapList.add(ObjectUtil.clone(valueMap));
@@ -494,7 +494,9 @@ public class FsStoreProductServiceImpl implements IFsStoreProductService {
             product.setBarCode(param.getValues().get(0).getBarCode());
             product.setGiveIntegral(new BigDecimal(countDto.getMinIntegral()));
             product.setStock(countDto.getStock());
+            normalizeStoreAttributionOnProduct(product, param);
             fsStoreProductMapper.updateFsStoreProduct(product);
+            syncStoreAttributionAfterSave(product.getProductId(), param);
             if (param.getSpecType().equals(0)) {
                 ProductArrtDTO fromatDetailDto = ProductArrtDTO.builder()
                         .value("规格")
@@ -564,6 +566,7 @@ public class FsStoreProductServiceImpl implements IFsStoreProductService {
             product.setBarCode(param.getValues().get(0).getBarCode());
             product.setGiveIntegral(new BigDecimal(countDto.getMinIntegral()));
             product.setStock(countDto.getStock());
+            normalizeStoreAttributionOnProduct(product, param);
             fsStoreProductMapper.insertFsStoreProduct(product);
             if (param.getSpecType().equals(0)) {
                 ProductArrtDTO fromatDetailDto = ProductArrtDTO.builder()
@@ -637,40 +640,47 @@ public class FsStoreProductServiceImpl implements IFsStoreProductService {
     }
 
     private ProductAttrCountDTO computedProductCount(List<FsStoreProductAttrValue> values) {
-        BigDecimal val = new BigDecimal(0);
-        //取最小价格
-        BigDecimal minPrice = values
-                .stream()
+        BigDecimal zero = BigDecimal.ZERO;
+        if (values == null || values.isEmpty()) {
+            return ProductAttrCountDTO.builder()
+                    .minPrice(zero)
+                    .minOtPrice(zero)
+                    .minCost(zero)
+                    .stock(0)
+                    .minIntegral(0)
+                    .build();
+        }
+        // 规格字段可能为 null,Comparator.naturalOrder().min 遇 null 会 NPE
+        BigDecimal minPrice = values.stream()
                 .map(FsStoreProductAttrValue::getPrice)
+                .filter(Objects::nonNull)
                 .min(Comparator.naturalOrder())
-                .orElse(val);
+                .orElse(zero);
 
-        //取最小积分
-        Integer minIntegral = values
-                .stream()
+        Integer minIntegral = values.stream()
                 .map(FsStoreProductAttrValue::getGiveIntegral)
+                .filter(Objects::nonNull)
                 .min(Comparator.naturalOrder())
                 .orElse(0);
 
-        BigDecimal minOtPrice = values
-                .stream()
+        BigDecimal minOtPrice = values.stream()
                 .map(FsStoreProductAttrValue::getOtPrice)
+                .filter(Objects::nonNull)
                 .min(Comparator.naturalOrder())
-                .orElse(val);
+                .orElse(zero);
 
-        BigDecimal minCost = values
-                .stream()
+        BigDecimal minCost = values.stream()
                 .map(FsStoreProductAttrValue::getCost)
+                .filter(Objects::nonNull)
                 .min(Comparator.naturalOrder())
-                .orElse(val);
-        //计算库存
-        Integer stock = values
-                .stream()
+                .orElse(zero);
+
+        Integer stock = values.stream()
                 .map(FsStoreProductAttrValue::getStock)
+                .filter(Objects::nonNull)
                 .reduce(Integer::sum)
                 .orElse(0);
 
-
         return ProductAttrCountDTO.builder()
                 .minPrice(minPrice)
                 .minOtPrice(minOtPrice)
@@ -1000,7 +1010,7 @@ public class FsStoreProductServiceImpl implements IFsStoreProductService {
         String syncedImageUrl = normalizeErpImageUrl(erpProduct.getPic());
 
         FsStoreProduct newProduct = new FsStoreProduct();
-        newProduct.setStoreId(getStoreIdByErpBrand(erpProduct.getBrand()));
+        applyErpBrandStoreMatch(newProduct, resolveErpBrandStoreMatch(erpProduct.getBrand()));
         newProduct.setBarCode(erpProduct.getSkuId());
         newProduct.setProductName(erpProduct.getName());
         newProduct.setPrice(erpProduct.getSalePrice());
@@ -1056,7 +1066,6 @@ public class FsStoreProductServiceImpl implements IFsStoreProductService {
     private void updateProductByErpData(FsStoreProduct product, ProductResponseDTO.ProductInfo erpProduct) {
         FsStoreProduct updateProduct = new FsStoreProduct();
         updateProduct.setProductId(product.getProductId());
-        updateProduct.setStoreId(getStoreIdByErpBrand(erpProduct.getBrand()));
         updateProduct.setProductName(erpProduct.getName());
         updateProduct.setPrice(erpProduct.getSalePrice());
         updateProduct.setCostPrice(erpProduct.getCostPrice());
@@ -1068,6 +1077,13 @@ public class FsStoreProductServiceImpl implements IFsStoreProductService {
         updateProduct.setPrescribeSpec(erpProduct.getPropertiesValue());
         updateProduct.setPrescribeFactory(erpProduct.getSupplierName());
         fsStoreProductMapper.updateFsStoreProduct(updateProduct);
+
+        ErpBrandStoreMatch match = resolveErpBrandStoreMatch(erpProduct.getBrand());
+        if (match.type == ErpBrandMatchType.STORE_NAME) {
+            fsStoreProductMapper.updateErpStoreIdOnly(product.getProductId(), match.storeId);
+        } else if (match.type == ErpBrandMatchType.ALIAS) {
+            fsStoreProductMapper.updateErpBrandStoreIdsClearStoreId(product.getProductId(), match.brandStoreIds);
+        }
     }
 
     private void updateAttrValueByErpData(FsStoreProduct product,FsStoreProductAttrValue attr, ProductResponseDTO.ProductInfo erpProduct) {
@@ -1094,22 +1110,116 @@ public class FsStoreProductServiceImpl implements IFsStoreProductService {
                 || erpProduct.getWeight() != null;
     }
 
-    private Long getStoreIdByErpBrand(String brand) {
+    /**
+     * 后台保存时店铺归属:
+     * brandStoreIds 有值 → 大品牌归属(storeId 置空,即使只有一个店)
+     * 仅 storeId → 店名精确归属
+     */
+    private void normalizeStoreAttributionOnProduct(FsStoreProduct product, FsStoreProductAddEditParam param) {
+        if (product == null || param == null) {
+            return;
+        }
+        if (StringUtils.isNotBlank(param.getBrandStoreIds())) {
+            product.setStoreId(null);
+            product.setBrandStoreIds(param.getBrandStoreIds().trim());
+        } else if (param.getStoreId() != null) {
+            product.setBrandStoreIds(null);
+        }
+        // storeId、brandStoreIds 都空:不在这里强改,避免误清库里已有归属
+    }
+
+    /**
+     * 动态 update 无法把 store_id 置 NULL,编辑保存后再用专用 SQL 同步归属。
+     * 仅当请求明确带了 brandStoreIds 或 storeId 时才覆盖。
+     */
+    private void syncStoreAttributionAfterSave(Long productId, FsStoreProductAddEditParam param) {
+        if (productId == null || param == null) {
+            return;
+        }
+        if (StringUtils.isNotBlank(param.getBrandStoreIds())) {
+            fsStoreProductMapper.updateErpBrandStoreIdsClearStoreId(productId, param.getBrandStoreIds().trim());
+        } else if (param.getStoreId() != null && StringUtils.isBlank(param.getBrandStoreIds())) {
+            // 仅单店模式才清 brand_store_ids;前端大品牌归属必须带 brandStoreIds
+            fsStoreProductMapper.updateStoreIdClearBrandStoreIds(productId, param.getStoreId());
+        }
+    }
+
+    /**
+     * ERP brand 归属解析:
+     * 1) brand 精确等于店名 → 只写 store_id,不改 brand_store_ids(保持 null)
+     * 2) 否则按各店 erp_brand_aliases 精确命中 → 写 brand_store_ids,store_id 置 null
+     */
+    private ErpBrandStoreMatch resolveErpBrandStoreMatch(String brand) {
+        ErpBrandStoreMatch match = new ErpBrandStoreMatch();
         if (StringUtils.isBlank(brand)) {
-            return null;
+            return match;
         }
+        String brandTrim = brand.trim();
         FsStore query = new FsStore();
-        query.setStoreName(brand.trim());
+        query.setStoreName(brandTrim);
         List<FsStore> stores = fsStoreMapper.selectFsStoreList(query);
-        if (stores == null || stores.isEmpty()) {
-            return null;
+        if (stores != null) {
+            for (FsStore store : stores) {
+                if (StringUtils.equals(StringUtils.trim(store.getStoreName()), brandTrim)) {
+                    match.type = ErpBrandMatchType.STORE_NAME;
+                    match.storeId = store.getStoreId();
+                    return match;
+                }
+            }
         }
-        for (FsStore store : stores) {
-            if (StringUtils.equals(StringUtils.trim(store.getStoreName()), brand.trim())) {
-                return store.getStoreId();
+        List<FsStore> allStores = fsStoreMapper.selectFsStoreList(new FsStore());
+        if (allStores == null || allStores.isEmpty()) {
+            return match;
+        }
+        List<Long> aliasStoreIds = new ArrayList<>();
+        for (FsStore store : allStores) {
+            if (store.getStoreId() == null || StringUtils.isBlank(store.getErpBrandAliases())) {
+                continue;
             }
+            String[] aliases = store.getErpBrandAliases().split("[,、]");
+            for (String alias : aliases) {
+                if (StringUtils.equals(StringUtils.trim(alias), brandTrim)) {
+                    aliasStoreIds.add(store.getStoreId());
+                    break;
+                }
+            }
+        }
+        if (aliasStoreIds.isEmpty()) {
+            return match;
+        }
+        aliasStoreIds = aliasStoreIds.stream().distinct().sorted().collect(Collectors.toList());
+        match.type = ErpBrandMatchType.ALIAS;
+        match.storeId = null;
+        match.brandStoreIds = aliasStoreIds.stream().map(String::valueOf).collect(Collectors.joining(","));
+        log.info("ERP大品牌别名命中多店, brand={}, brandStoreIds={}", brandTrim, match.brandStoreIds);
+        return match;
+    }
+
+    private void applyErpBrandStoreMatch(FsStoreProduct product, ErpBrandStoreMatch match) {
+        if (product == null || match == null || match.type == ErpBrandMatchType.NONE) {
+            return;
+        }
+        if (match.type == ErpBrandMatchType.STORE_NAME) {
+            product.setStoreId(match.storeId);
+            // 不设置 brandStoreIds,插入时保持 null
+            return;
         }
-        return stores.get(0).getStoreId();
+        if (match.type == ErpBrandMatchType.ALIAS) {
+            product.setStoreId(null);
+            product.setBrandStoreIds(match.brandStoreIds);
+        }
+    }
+
+    private enum ErpBrandMatchType {
+        NONE,
+        STORE_NAME,
+        ALIAS
+    }
+
+    private static class ErpBrandStoreMatch {
+        private ErpBrandMatchType type = ErpBrandMatchType.NONE;
+        private Long storeId;
+        private String brandStoreIds;
     }
 
     private void syncProductAttrDefinition(Long productId) {

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

@@ -1256,6 +1256,10 @@ public class OpenIMServiceImpl implements OpenIMService {
 
                 //是否催课
                 if(batchSendCourseDTO.getIsUrgeCourse()){
+                    if (StringUtils.isEmpty(batchSendCourseDTO.getUrgeContent())
+                            || StringUtils.isEmpty(batchSendCourseDTO.getUrgeContent().trim())) {
+                        throw new ServiceException("开启催课时,催课内容不能为空");
+                    }
                     // 组装催课消息数据
                     OpenImBatchMsgDTO openImBatchUrgeCourse = makeOpenImBatchMsgDTO(batchSendCourseDTO, courseUrl, objectMapper, batchUserIds, planSendTimeStamp, "催课");
 
@@ -1636,10 +1640,12 @@ public class OpenIMServiceImpl implements OpenIMService {
             openImBatchMsgDTO.setContentType(110);
         } else {
             // 催课
-//            extension.setTitle(batchSendCourseDTO.getUrgeContent());
-//            extension.setSendTime(batchSendCourseDTO.getUrgeTime() != null ? batchSendCourseDTO.getUrgeTime() : new Date());
+            if (StringUtils.isEmpty(batchSendCourseDTO.getUrgeContent())
+                    || StringUtils.isEmpty(batchSendCourseDTO.getUrgeContent().trim())) {
+                throw new ServiceException("催课内容不能为空");
+            }
             OpenImBatchMsgDTO.Content content = new OpenImBatchMsgDTO.Content();
-            content.setContent(batchSendCourseDTO.getUrgeContent());
+            content.setContent(batchSendCourseDTO.getUrgeContent().trim());
             openImBatchMsgDTO.setContent(content);
             openImBatchMsgDTO.setContentType(101);
             openImBatchMsgDTO.setSendTime(batchSendCourseDTO.getUrgeTime() != null ? batchSendCourseDTO.getUrgeTime().getTime() : System.currentTimeMillis());
@@ -1701,6 +1707,13 @@ public class OpenIMServiceImpl implements OpenIMService {
     @Override
     @Transactional
     public OpenImResponseDTO batchUrgeCourseTaskCreate(BatchUrgeCourseTaskDTO batchUrgeCourseTaskDTO) throws JsonProcessingException {
+        if (StringUtils.isEmpty(batchUrgeCourseTaskDTO.getUrgeContent())
+                || StringUtils.isEmpty(batchUrgeCourseTaskDTO.getUrgeContent().trim())) {
+            throw new ServiceException("催课内容不能为空");
+        }
+        if (batchUrgeCourseTaskDTO.getUrgeTime() == null) {
+            throw new ServiceException("催课时间不能为空");
+        }
         ObjectMapper objectMapper = new ObjectMapper();
         objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
         Map<String, Object> param = new HashMap<>();
@@ -1904,6 +1917,10 @@ public class OpenIMServiceImpl implements OpenIMService {
         }
 
          // 催课
+        if (StringUtils.isEmpty(batchUrgeCourseDTO.getUrgeContent())
+                || StringUtils.isEmpty(batchUrgeCourseDTO.getUrgeContent().trim())) {
+            throw new ServiceException("催课内容不能为空");
+        }
         // 组装催课消息
         BatchSendCourseDTO batchSendCourseDTO = new BatchSendCourseDTO();
         BeanUtils.copyProperties(fsImMsgSendLog, batchSendCourseDTO);

+ 13 - 3
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -56,7 +56,7 @@ import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
 import org.apache.http.util.EntityUtils;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.env.Environment;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import com.fs.common.utils.sign.Md5Utils;
@@ -129,6 +129,9 @@ public class LiveServiceImpl implements ILiveService
     @Autowired
     LiveTagConfigMapper liveTagConfigMapper;
 
+    @Autowired
+    private Environment environment;
+
     private static String TOKEN_VALID_CODE = "40001";
 
     private static volatile Integer version = 0;
@@ -1457,8 +1460,15 @@ public class LiveServiceImpl implements ILiveService
         if (live == null) {
             return R.error("直播间不存在");
         }
-        String url = "https://idcardside.market.alicloudapi.com/idcard_ocr";
-        String appCode = "0a01beb5322e49ca802423fe29df9582";
+        // H-17: AppCode 仅从环境变量/配置注入,禁止硬编码
+        String appCode = environment.getProperty("aliyun.market.idcard-app-code");
+        if (StringUtils.isEmpty(appCode)) {
+            log.error("verifyIdInfo: aliyun.market.idcard-app-code (ALIYUN_IDCARD_APP_CODE) 未配置");
+            return R.error("身份证核验服务未配置");
+        }
+        String url = environment.getProperty(
+                "aliyun.market.idcard-ocr-url",
+                "https://idcardside.market.alicloudapi.com/idcard_ocr");
         try {
             Map<String, String> params = new HashMap<>();
             //image 和 url 二选一 ,选择其中一个时,请注释另外一个参数,默认使用image

+ 16 - 1
fs-service/src/main/java/com/fs/qwApi/util/XMLParse.java

@@ -15,6 +15,7 @@ import org.xml.sax.InputSource;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
 import java.io.StringReader;
 
 /**
@@ -24,6 +25,20 @@ import java.io.StringReader;
  */
 public  class XMLParse {
 
+	/**
+	 * 创建已加固的 DocumentBuilderFactory,禁用 DOCTYPE / 外部实体,防 XXE。
+	 */
+	public static DocumentBuilderFactory newSecureDocumentBuilderFactory() throws ParserConfigurationException {
+		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+		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);
+		dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+		dbf.setXIncludeAware(false);
+		dbf.setExpandEntityReferences(false);
+		return dbf;
+	}
+
 	/**
 	 * 提取出xml数据包中的加密消息
 	 * @param xmltext 待提取的xml字符串
@@ -33,7 +48,7 @@ public  class XMLParse {
 	public static Object[] extract(String xmltext) throws AesException     {
 		Object[] result = new Object[3];
 		try {
-			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+			DocumentBuilderFactory dbf = newSecureDocumentBuilderFactory();
 			DocumentBuilder db = dbf.newDocumentBuilder();
 			StringReader sr = new StringReader(xmltext);
 			InputSource is = new InputSource(sr);

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

@@ -71,7 +71,7 @@ spring:
 token:
     # 令牌自定义标识
     header: Authorization
-    # 令牌密钥(可用环境变量 TOKEN_SECRET 覆盖
+    # 令牌密钥(生产务必用环境变量 TOKEN_SECRET 覆盖;本地未配置时用下方默认以便启动
     secret: ${TOKEN_SECRET:abcdefghijklmnopqrstuvwxyz}
     # 令牌有效期(默认30分钟)
     expireTime: 720
@@ -81,6 +81,12 @@ huawei_cloud:
   ak: ${HUAWEI_CLOUD_AK:}
   sk: ${HUAWEI_CLOUD_SK:}
 
+# H-17: 阿里云市场身份证 OCR AppCode(禁止写进源码)
+aliyun:
+  market:
+    idcard-app-code: ${ALIYUN_IDCARD_APP_CODE:}
+    idcard-ocr-url: ${ALIYUN_IDCARD_OCR_URL:https://idcardside.market.alicloudapi.com/idcard_ocr}
+
 sms:
   tencent:
     secret-id: ${SMS_TENCENT_SECRET_ID:}

+ 25 - 25
fs-service/src/main/resources/application-config-druid-hdt.yml

@@ -11,26 +11,26 @@ wx:
   miniapp:
     configs:
       - appid: wx4115995705bb0ea0   #中康智慧
-        secret: ${HDT_WX_MINIAPP_SECRET_1:58910ae743005c396012b029c7def579}
-        token: ${HDT_WX_MINIAPP_TOKEN:Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC}
-        aesKey: ${HDT_WX_MINIAPP_AES_KEY:HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E}
+        secret: ${HDT_WX_MINIAPP_SECRET_1}
+        token: ${HDT_WX_MINIAPP_TOKEN}
+        aesKey: ${HDT_WX_MINIAPP_AES_KEY}
         msgDataFormat: JSON
       - appid: wxedde588767b358b1   #中康未来智慧药房
-        secret: ${HDT_WX_MINIAPP_SECRET_2:928d2961c81610d8f64b019597212fcd}
-        token: ${HDT_WX_MINIAPP_TOKEN:Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC}
-        aesKey: ${HDT_WX_MINIAPP_AES_KEY:HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E}
+        secret: ${HDT_WX_MINIAPP_SECRET_2}
+        token: ${HDT_WX_MINIAPP_TOKEN}
+        aesKey: ${HDT_WX_MINIAPP_AES_KEY}
         msgDataFormat: JSON
   cp:
     corpId: wwb2a10556ca7c2
     appConfigs:
       - agentId: 10005
-        secret: ${HDT_WX_CP_SECRET:ec7okROXJqkNafq66aKNv0asTQIG0CYrj3vyBbo}
-        token: ${HDT_WX_CP_TOKEN:PPKOdAoMO}
-        aesKey: ${HDT_WX_CP_AES_KEY:PKvaxtpSvNGpfTDmVUHIK8Wok2ESyYX24qpXJAdMP}
+        secret: ${HDT_WX_CP_SECRET}
+        token: ${HDT_WX_CP_TOKEN}
+        aesKey: ${HDT_WX_CP_AES_KEY}
   pay:
     appId: wx73f85fd6119 #微信公众号或者小程序等的appid
     mchId: 161145 #微信支付商户号
-    mchKey: ${HDT_WX_PAY_MCH_KEY:8cab128997a347c10898b877f38} #微信支付商户密钥
+    mchKey: ${HDT_WX_PAY_MCH_KEY} #微信支付商户密钥
     subAppId:  #服务商模式下的子商户公众账号ID
     subMchId:  #服务商模式下的子商户号
     keyPath: c:\\cert\\apiclient_cert.p12 # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
@@ -44,25 +44,25 @@ wx:
     # 兜底/老授权号;业务多服务号以 sys_config.courseMa.config(type=2) 为准,Portal: /store/app/wx/portal/{appId}
     configs:
       - appId: wx7670b3b1b1cfcd47 # 第一个公众号的appid  //公众号名称:成都九州在线互联网医院
-        secret: ${HDT_WX_MP_SECRET:c6e5726c7039092a907c3242c8d3e406} # 公众号的appsecret
-        token: ${HDT_WX_MP_TOKEN:PPKOdAlCoMO} # 接口配置里的Token值抖音免领及代运营
-        aesKey: ${HDT_WX_MP_AES_KEY:8WCur56h3f44rJ5Dq7Ab1PKy6nbOwnUHTAnvwWNFeQT} # 接口配置里的EncodingAESKey值
+        secret: ${HDT_WX_MP_SECRET} # 公众号的appsecret
+        token: ${HDT_WX_MP_TOKEN} # 接口配置里的Token值抖音免领及代运营
+        aesKey: ${HDT_WX_MP_AES_KEY} # 接口配置里的EncodingAESKey值
   open:
     appId: wx0943a6f6d040ca3a
-    secret: ${HDT_WX_OPEN_SECRET:602e2a9ea639b6cd18eb80e715c4b22e}
+    secret: ${HDT_WX_OPEN_SECRET}
 aifabu:  #爱链接
-  appKey: ${HDT_AIFABU_APP_KEY:7b471be905ab17ef358c610d117601d008}
+  appKey: ${HDT_AIFABU_APP_KEY}
 
 watch:
   watchUrl: watch.ylrzcloud.com/prod-api
 #  account: tcloud
 #  password: mdf-m2h_6yw2$hq
   account1: ccif #866655060138751
-  password1: ${HDT_WATCH_PASSWORD1:cp-t5or_6xw7$$mt}
+  password1: ${HDT_WATCH_PASSWORD1}
   account2: tcloud #rt500台
-  password2: ${HDT_WATCH_PASSWORD2:mdf-m2h_6yw2$$hq}
+  password2: ${HDT_WATCH_PASSWORD2}
   account3: whr
-  password3: ${HDT_WATCH_PASSWORD3:v9xsKuqn_$$d2y}
+  password3: ${HDT_WATCH_PASSWORD3}
 
 fs :
   commonApi: http://192.168.0.70:8010
@@ -70,17 +70,17 @@ fs :
   jwt:
     # 加密秘钥
 #    secret: f4e2e52034348f86b67cde581c0f9eb5
-    secret: ${HDT_APP_JWT_SECRET:hdt-zxqzbl}
+    secret: ${HDT_APP_JWT_SECRET}
     # token有效时长,7天,单位秒
     expire: 31536000
     header: AppToken
 nuonuo:
   key: 10924508
-  secret: ${HDT_NUONUO_SECRET:A2EB20764D304D16}
+  secret: ${HDT_NUONUO_SECRET}
 # 存储捅配置
 tencent_cloud_config:
-  secret_id: ${HDT_TENCENT_COS_SECRET_ID:AKIDLl1tguyrZ6QddTCi2BLJ4e3OXVIuJVVK}
-  secret_key: ${HDT_TENCENT_COS_SECRET_KEY:g9R6kLrMp8EDzXszylLispiQxHRN6cw5}
+  secret_id: ${HDT_TENCENT_COS_SECRET_ID}
+  secret_key: ${HDT_TENCENT_COS_SECRET_KEY}
   bucket: hdt-1323137866
   app_id: 1323137866
   region: ap-chongqing
@@ -109,11 +109,11 @@ feishu:
 #  appId: "cli_aaa27d4aed395bee"
 #  appSecret: "DYh8JVjwnoGT4p4ykHjK7AiA6AzH1x6e"
 #  appId: "cli_aafa8f356e799bc1"
-#  appSecret: "${HDT_FEISHU_APP_SECRET:nBpdLLciDGkqAF2FcM2n2cMVkdtJXd5R}"
+#  appSecret: "${HDT_FEISHU_APP_SECRET}"
 #  appId: "cli_aaf3ca6700799be9"
-#  appSecret: "${HDT_FEISHU_APP_SECRET:46gtmBi6GVCHv2ozmpuWPeQ52DXFdGm4}"
+#  appSecret: "${HDT_FEISHU_APP_SECRET}"
   appId: "cli_aaf6bb3c0be29bb3"
-  appSecret: "${HDT_FEISHU_APP_SECRET:3OhIlldgTAnk8yBEwHtJpdnNY6BujFvc}"
+  appSecret: "${HDT_FEISHU_APP_SECRET}"
 # sip外呼配置
 sip:
   call:

+ 21 - 12
fs-service/src/main/resources/application-druid-hdt.yml

@@ -45,7 +45,7 @@ spring:
                 master:
                     url: jdbc:mysql://192.168.0.141:3306/hdt_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: ${HDT_MYSQL_PASSWORD:Ylrztek250218!3@.}
+                    password: ${HDT_MYSQL_PASSWORD}
                 # 从库数据源
                 slave:
                     # 从数据源开关/默认关闭
@@ -80,8 +80,8 @@ spring:
                     allow: 127.0.0.1
                     url-pattern: /druid/*
                     # 控制台管理用户名和密码
-                    login-username: ${DRUID_LOGIN_USERNAME:druid_admin}
-                    login-password: ${DRUID_LOGIN_PASSWORD:ChangeMe_Druid_Strong!}
+                    login-username: ${DRUID_LOGIN_USERNAME}
+                    login-password: ${DRUID_LOGIN_PASSWORD}
                 filter:
                     stat:
                         enabled: true
@@ -100,7 +100,7 @@ spring:
                 master:
                     url: jdbc:mysql://192.168.0.141:3306/fs_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: ${HDT_SOP_MYSQL_PASSWORD:Ylrztek250218!3@.}
+                    password: ${HDT_SOP_MYSQL_PASSWORD}
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量
@@ -128,8 +128,8 @@ spring:
                     allow: 127.0.0.1
                     url-pattern: /druid/*
                     # 控制台管理用户名和密码
-                    login-username: ${DRUID_LOGIN_USERNAME:druid_admin}
-                    login-password: ${DRUID_LOGIN_PASSWORD:ChangeMe_Druid_Strong!}
+                    login-username: ${DRUID_LOGIN_USERNAME}
+                    login-password: ${DRUID_LOGIN_PASSWORD}
                 filter:
                     stat:
                         enabled: true
@@ -148,7 +148,7 @@ spring:
         master:
           url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
           username: root
-          password: ${HDT_EASYCALL_MYSQL_PASSWORD:easycallcenter365}
+          password: ${HDT_EASYCALL_MYSQL_PASSWORD}
         # 初始连接数
         initialSize: 5
         # 最小连接池数量
@@ -176,8 +176,8 @@ spring:
           allow: 127.0.0.1
           url-pattern: /druid/*
           # 控制台管理用户名和密码
-          login-username: ${DRUID_LOGIN_USERNAME:druid_admin}
-          login-password: ${DRUID_LOGIN_PASSWORD:ChangeMe_Druid_Strong!}
+          login-username: ${DRUID_LOGIN_USERNAME}
+          login-password: ${DRUID_LOGIN_PASSWORD}
         filter:
           stat:
             enabled: true
@@ -193,13 +193,13 @@ rocketmq:
     producer:
         group: my-producer-group   # 生产者组名(必须唯一)
         access-key: default
-        secret-key: ${HDT_ROCKETMQ_SECRET_KEY:aZS4z!88dNndKTfhITzTpTxRrVUShtH3}
+        secret-key: ${HDT_ROCKETMQ_SECRET_KEY}
     consumer:
         group: common-group
         access-key: default
-        secret-key: ${HDT_ROCKETMQ_SECRET_KEY:aZS4z!88dNndKTfhITzTpTxRrVUShtH3}
+        secret-key: ${HDT_ROCKETMQ_SECRET_KEY}
 openIM:
-    secret: ${HDT_OPENIM_SECRET:openIM123}
+    secret: ${HDT_OPENIM_SECRET}
     userID: imAdmin
     url: https://webim.hbhdt.top/api
 push:
@@ -212,3 +212,12 @@ isNewWxMerchant: false
 # 聚水潭API配置
 jst:
   shop_code: "21602201"
+
+# C-01 X-Skip-Auth 加固:HMAC 短期票据 + 接口路径白名单(头非空不再直接放行)
+# 调用方:X-Skip-Auth = SkipAuthTicketUtils.create(userId, ttlSeconds, secret)
+# 格式:userId.expireAtMs.hmacHex
+skip-auth:
+  secret: ${SKIP_AUTH_SECRET:}
+  ticket-max-ttl-seconds: 300
+  # 按实际内部调用接口补充路径前缀;未配置则拒绝一切 Skip-Auth
+  allowed-path-prefixes: []

+ 5 - 1
fs-service/src/main/resources/mapper/his/FsStoreMapper.xml

@@ -36,10 +36,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="sendPhone"    column="send_phone"    />
         <result property="shopCode"    column="shop_code"    />
         <result property="chainBrands"    column="chain_brands"    />
+        <result property="erpBrandAliases"    column="erp_brand_aliases"    />
     </resultMap>
 
     <sql id="selectFsStoreVo">
-        select store_id,full_name,delivery_type,send_phone, brokerage_rate,refund_phone,refund_address,refund_consignee,city_ids, store_name, descs, brokerage_type,logo_url, address, lng, lat, phone, license_images, product_count, status, create_time, update_time, sales_count, balance, total_money, is_audit, account, password, shipping_type, shop_code, chain_brands from fs_store
+        select store_id,full_name,delivery_type,send_phone, brokerage_rate,refund_phone,refund_address,refund_consignee,city_ids, store_name, descs, brokerage_type,logo_url, address, lng, lat, phone, license_images, product_count, status, create_time, update_time, sales_count, balance, total_money, is_audit, account, password, shipping_type, shop_code, chain_brands, erp_brand_aliases from fs_store
     </sql>
 
     <select id="selectFsStoreList" parameterType="FsStore" resultMap="FsStoreResult">
@@ -93,6 +94,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="sendPhone != null">send_phone,</if>
             <if test="shopCode != null">shop_code,</if>
             <if test="chainBrands != null">chain_brands,</if>
+            <if test="erpBrandAliases != null">erp_brand_aliases,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="cityIds != null">#{cityIds},</if>
@@ -125,6 +127,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="sendPhone != null">#{sendPhone},</if>
             <if test="shopCode != null">#{shopCode},</if>
             <if test="chainBrands != null">#{chainBrands},</if>
+            <if test="erpBrandAliases != null">#{erpBrandAliases},</if>
         </trim>
     </insert>
 
@@ -161,6 +164,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="sendPhone != null">send_phone = #{sendPhone},</if>
             <if test="shopCode != null">shop_code = #{shopCode},</if>
             <if test="chainBrands != null">chain_brands = #{chainBrands},</if>
+            <if test="erpBrandAliases != null">erp_brand_aliases = #{erpBrandAliases},</if>
         </trim>
         where store_id = #{storeId}
     </update>

+ 9 - 3
fs-service/src/main/resources/mapper/his/FsStoreProductMapper.xml

@@ -48,16 +48,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="brand"    column="brand"    />
         <result property="productSourceType"    column="product_source_type"    />
         <result property="chainBrand"    column="chain_brand"    />
+        <result property="brandStoreIds"    column="brand_store_ids"    />
     </resultMap>
 
     <sql id="selectFsStoreProductVo">
-        select product_id,is_prescribe,is_drug, store_id, img_url, erp_img_url, images, product_name, product_introduce, keyword, bar_code, cate_id, price, ot_price, unit_name, sort, sales, stock, is_show, is_hot, is_benefit, is_best, is_new, `desc`, create_time, update_time, is_postage, is_del, give_integral, cost_price, views, code_url, spec_type, product_type, prescribe_code, prescribe_spec, prescribe_factory, prescribe_name, is_display, temp_id,brand,product_source_type,chain_brand from fs_store_product
+        select product_id,is_prescribe,is_drug, store_id, img_url, erp_img_url, images, product_name, product_introduce, keyword, bar_code, cate_id, price, ot_price, unit_name, sort, sales, stock, is_show, is_hot, is_benefit, is_best, is_new, `desc`, create_time, update_time, is_postage, is_del, give_integral, cost_price, views, code_url, spec_type, product_type, prescribe_code, prescribe_spec, prescribe_factory, prescribe_name, is_display, temp_id,brand,product_source_type,chain_brand,brand_store_ids from fs_store_product
     </sql>
 
     <select id="selectFsStoreProductList" parameterType="FsStoreProduct" resultMap="FsStoreProductResult">
         <include refid="selectFsStoreProductVo"/>
         <where>
-            <if test="storeId != null "> and store_id = #{storeId}</if>
+            <if test="storeId != null ">
+                and (store_id = #{storeId} or (store_id is null and FIND_IN_SET(#{storeId}, IFNULL(brand_store_ids, ''))))
+            </if>
             <if test="imgUrl != null  and imgUrl != ''"> and img_url = #{imgUrl}</if>
             <if test="images != null  and images != ''"> and images = #{images}</if>
             <if test="productName != null  and productName != ''"> and product_name like concat('%', #{productName}, '%')</if>
@@ -146,8 +149,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isPrescribe != null">is_prescribe,</if>
             <if test="isDrug != null">is_drug,</if>
             <if test="brand != null">brand,</if>
-            <if test="productSourceType !=null">product_source_type</if>
+            <if test="productSourceType !=null">product_source_type,</if>
             <if test="chainBrand != null">chain_brand,</if>
+            <if test="brandStoreIds != null">brand_store_ids,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="storeId != null">#{storeId},</if>
@@ -192,6 +196,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="brand != null">#{brand},</if>
             <if test="productSourceType !=null">#{productSourceType},</if>
             <if test="chainBrand != null">#{chainBrand},</if>
+            <if test="brandStoreIds != null">#{brandStoreIds},</if>
          </trim>
     </insert>
 
@@ -240,6 +245,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="brand != null">brand = #{brand},</if>
             <if test="productSourceType !=null">product_source_type = #{productSourceType},</if>
             <if test="chainBrand != null">chain_brand = #{chainBrand},</if>
+            <if test="brandStoreIds != null">brand_store_ids = #{brandStoreIds},</if>
         </trim>
         where product_id = #{productId}
     </update>

+ 49 - 0
fs-user-app/src/main/java/com/fs/app/config/SkipAuthProperties.java

@@ -0,0 +1,49 @@
+package com.fs.app.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * X-Skip-Auth 加固配置(仅 fs-user-app)。
+ * 未配置 secret / 接口路径白名单时,拒绝一切 Skip-Auth。
+ */
+@Component
+@ConfigurationProperties(prefix = "skip-auth")
+public class SkipAuthProperties {
+
+    /** HMAC 密钥,建议环境变量 SKIP_AUTH_SECRET */
+    private String secret = "";
+
+    /** 票据最大有效期(秒) */
+    private long ticketMaxTtlSeconds = 300;
+
+    /** 允许使用 Skip-Auth 的接口路径前缀,如 /app/course/ */
+    private List<String> allowedPathPrefixes = new ArrayList<>();
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public void setSecret(String secret) {
+        this.secret = secret;
+    }
+
+    public long getTicketMaxTtlSeconds() {
+        return ticketMaxTtlSeconds;
+    }
+
+    public void setTicketMaxTtlSeconds(long ticketMaxTtlSeconds) {
+        this.ticketMaxTtlSeconds = ticketMaxTtlSeconds;
+    }
+
+    public List<String> getAllowedPathPrefixes() {
+        return allowedPathPrefixes;
+    }
+
+    public void setAllowedPathPrefixes(List<String> allowedPathPrefixes) {
+        this.allowedPathPrefixes = allowedPathPrefixes != null ? allowedPathPrefixes : new ArrayList<>();
+    }
+}

+ 48 - 47
fs-user-app/src/main/java/com/fs/app/controller/CommonController.java

@@ -8,6 +8,7 @@ import com.baidubce.appbuilder.model.dataset.DocumentListResponse;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fs.ai.service.IBaiduAIService;
 import com.fs.ai.vo.BaiduAIMsgResultVO;
+import com.fs.app.annotation.Login;
 import com.fs.app.param.SignParam;
 import com.fs.app.utils.CityTreeUtil;
 import com.fs.app.utils.JwtUtils;
@@ -333,16 +334,16 @@ public class CommonController {
 		return R.ok().put("url",url);
 	}
 
-	@PostMapping("/uploadHuaWeiVod")
-	public R uploadHuaWeiVod()
-	{
-		// 本地要上传的媒资路径
-		String filePath = "D:\\WeChat\\file\\WeChat Files\\wxid_cnp9jj0rcnr821\\FileStorage\\Video\\2024-07\\4faef56eb3d8c22f33f5d619a20232fb.mp4";
-
-		// 上传媒资文件
-//		huaweiVodService.uploadPartFile(filePath);
-		return R.ok();
-	}
+//	@PostMapping("/uploadHuaWeiVod")
+//	public R uploadHuaWeiVod()
+//	{
+//		// 本地要上传的媒资路径
+//		String filePath = "D:\\WeChat\\file\\WeChat Files\\wxid_cnp9jj0rcnr821\\FileStorage\\Video\\2024-07\\4faef56eb3d8c22f33f5d619a20232fb.mp4";
+//
+//		// 上传媒资文件
+////		huaweiVodService.uploadPartFile(filePath);
+//		return R.ok();
+//	}
 
 	@GetMapping("/getHuaWeiUrl")
 	public R getHuaWeiUrl()
@@ -370,48 +371,48 @@ public class CommonController {
 		String sign=api.genUserSig(signParam.getUserId(),180*86400);
 		return R.ok().put("data",sign);
 	}
-	@GetMapping(value = "/testSend")
-	@ApiOperation("testSend")
-	public R testSend( )
-	{
+//	@GetMapping(value = "/testSend")
+//	@ApiOperation("testSend")
+//	public R testSend( )
+//	{
+////		MsgDTO msgDTO=new MsgDTO();
+////		msgDTO.setFrom_Account("D-6");
+////		msgDTO.setTo_Account("U-40487");
+////		List<MsgDataDTO> msgs=new ArrayList<>();
+////		MsgDataDTO msg=new MsgDataDTO();
+////		FsInquiryOrderReport report=reportService.selectFsInquiryOrderReportByOrderId(252l);
+////		String ext= JSONUtil.toJsonStr(report);
+////		String orderId="252";
+////		msg.setMsgContent(new MsgDataFormatDTO("report",ext,orderId));
+////		msg.setMsgType("TIMCustomElem");//TIMCustomElem
+////		msgs.add(msg);
+////		msgDTO.setMsgBody(msgs);
+//
 //		MsgDTO msgDTO=new MsgDTO();
-//		msgDTO.setFrom_Account("D-6");
-//		msgDTO.setTo_Account("U-40487");
+//
+//		MsgCustomDTO customDTO=new MsgCustomDTO();
+//		customDTO.setType("startInquiry");
+//		customDTO.setOrderId("279");
+//		customDTO.setFollowId("1");
+//		customDTO.setImType(1);
+//		msgDTO.setCloudCustomData(JSONUtil.toJsonStr(customDTO));
+//
+//
+//		msgDTO.setTo_Account("D-63");
+//		msgDTO.setFrom_Account("U-40479");
 //		List<MsgDataDTO> msgs=new ArrayList<>();
 //		MsgDataDTO msg=new MsgDataDTO();
-//		FsInquiryOrderReport report=reportService.selectFsInquiryOrderReportByOrderId(252l);
-//		String ext= JSONUtil.toJsonStr(report);
-//		String orderId="252";
-//		msg.setMsgContent(new MsgDataFormatDTO("report",ext,orderId));
-//		msg.setMsgType("TIMCustomElem");//TIMCustomElem
+//		InquiryOrderMsgDTO startDTO=new InquiryOrderMsgDTO();
+//		startDTO.setTitle("张医生为您服务");
+//		String ext= JSONUtil.toJsonStr(startDTO);
+//		String orderId="231";
+//		msg.setMsgContent(new MsgDataFormatDTO("test"));
+//		msg.setMsgType("TIMTextElem");//TIMCustomElem
 //		msgs.add(msg);
 //		msgDTO.setMsgBody(msgs);
-
-		MsgDTO msgDTO=new MsgDTO();
-
-		MsgCustomDTO customDTO=new MsgCustomDTO();
-		customDTO.setType("startInquiry");
-		customDTO.setOrderId("279");
-		customDTO.setFollowId("1");
-		customDTO.setImType(1);
-		msgDTO.setCloudCustomData(JSONUtil.toJsonStr(customDTO));
-
-
-		msgDTO.setTo_Account("D-63");
-		msgDTO.setFrom_Account("U-40479");
-		List<MsgDataDTO> msgs=new ArrayList<>();
-		MsgDataDTO msg=new MsgDataDTO();
-		InquiryOrderMsgDTO startDTO=new InquiryOrderMsgDTO();
-		startDTO.setTitle("张医生为您服务");
-		String ext= JSONUtil.toJsonStr(startDTO);
-		String orderId="231";
-		msg.setMsgContent(new MsgDataFormatDTO("test"));
-		msg.setMsgType("TIMTextElem");//TIMCustomElem
-		msgs.add(msg);
-		msgDTO.setMsgBody(msgs);
-		imService.sendMsg(msgDTO);
-		return R.ok();
-	}
+//		imService.sendMsg(msgDTO);
+//		return R.ok();
+//	}
 
 	@ApiOperation("获取城市数据")
 	@GetMapping("/getCitys")

+ 15 - 2
fs-user-app/src/main/java/com/fs/app/controller/CompanyUserController.java

@@ -440,14 +440,27 @@ public class CompanyUserController extends AppBaseController {
     @Log(title = "小程序销售绑定医生", businessType = BusinessType.UPDATE)
     @PostMapping("/bindDoctorId")
     public R binDoctor(@RequestBody CompanyUser companyUser) {
-        return companyUserService.bindDoctor(companyUser);
+        // 强制销售登录;禁止用请求体 userId 冒充他人绑定
+        Long companyUserId = getCompanyUserId();
+        if (companyUser == null || companyUser.getDoctorId() == null) {
+            return R.error("医生ID不能为空");
+        }
+        CompanyUser update = new CompanyUser();
+        update.setUserId(companyUserId);
+        update.setDoctorId(companyUser.getDoctorId());
+        return companyUserService.bindDoctor(update);
     }
 
     @ApiOperation("小程序销售解除绑定医生")
     @Log(title = "小程序销售解除绑定医生", businessType = BusinessType.UPDATE)
     @GetMapping("/unBindDoctorId/{userId}")
     public R unBinDoctor(@PathVariable("userId") Long userId) {
-        return companyUserService.unBindDoctor(userId);
+        // 强制销售登录;路径 userId 必须等于 Token 身份
+        Long companyUserId = getCompanyUserId();
+        if (userId == null || !companyUserId.equals(userId)) {
+            return R.error(HttpStatus.FORBIDDEN, "无权操作");
+        }
+        return companyUserService.unBindDoctor(companyUserId);
     }
 
     @ApiOperation("获取公司收款码")

+ 4 - 4
fs-user-app/src/main/java/com/fs/app/controller/CourseOrderController.java

@@ -65,10 +65,10 @@ public class CourseOrderController extends  AppBaseController{
         return courseOrderService.createIntegralOrder(param);
     }
 
-    @PostMapping("/testPay")
-    public R testPay(@Validated @RequestBody FsUserCourseOrderDoPayParam param, HttpServletRequest request){
-        return courseOrderService.test(param.getOrderId());
-    }
+//    @PostMapping("/testPay")
+//    public R testPay(@Validated @RequestBody FsUserCourseOrderDoPayParam param, HttpServletRequest request){
+//        return courseOrderService.test(param.getOrderId());
+//    }
 
 
     @Login

+ 11 - 0
fs-user-app/src/main/java/com/fs/app/controller/HealthRecordController.java

@@ -105,7 +105,18 @@ public class HealthRecordController extends AppBaseController
     @PutMapping("/editRecord")
     public R edit(@RequestBody FsHealthRecordAddEditParam param)
     {
+        Long userId = Long.parseLong(getUserId());
+        // 只允许修改当前登录用户自己的档案,禁止按请求体 id 越权改他人数据
+        FsHealthRecord owned = fsHealthRecordService.selectFsHealthRecordByUserId(userId);
+        if (owned == null) {
+            return R.error("健康档案不存在");
+        }
+        if (param.getId() != null && !owned.getId().equals(param.getId())) {
+            return R.error("无权修改该健康档案");
+        }
         FsHealthRecord fsHealthRecord = BeanCopyUtils.copy(param, FsHealthRecord.class);
+        fsHealthRecord.setId(owned.getId());
+        fsHealthRecord.setUserId(userId);
         fsHealthRecordService.updateFsHealthRecord(fsHealthRecord);
         return R.ok();
     }

+ 7 - 7
fs-user-app/src/main/java/com/fs/app/controller/UserVipController.java

@@ -90,13 +90,13 @@ public class UserVipController extends  AppBaseController{
     }
 
 
-    @ApiOperation("测试支付成功")
-    @PostMapping("/test")
-    public R test(@RequestParam String payCode,@RequestParam String tradeNo)
-    {
-        System.out.println("payCode:"+payCode);
-        return fsUserVipOrderService.payConfirm("",payCode,tradeNo,"",1,null,null);
-    }
+//    @ApiOperation("测试支付成功")
+//    @PostMapping("/test")
+//    public R test(@RequestParam String payCode,@RequestParam String tradeNo)
+//    {
+//        System.out.println("payCode:"+payCode);
+//        return fsUserVipOrderService.payConfirm("",payCode,tradeNo,"",1,null,null);
+//    }
 
     @Login
     @ApiOperation("清除vip")

+ 1 - 7
fs-user-app/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java

@@ -46,7 +46,7 @@ public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
             return true;
         }
 
-        // 业务场景:请求头存在跳过认证标识,直接放行
+        // 业务场景:请求头存在跳过认证标识,直接放行(白名单/票据加固待接口清单确认后再启用)
         String skipAuth = request.getHeader(SKIP_AUTH_HEADER);
         if (StringUtils.isNotEmpty(skipAuth)) {
             return true;
@@ -67,12 +67,6 @@ public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
         if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
             throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
         }
-        
-        //查询用户的TOKEN是否和REDIS中的一样
-//        String redisToken=redisCache.getCacheObject("token:"+ Long.parseLong(claims.getSubject()));
-//        if(redisToken==null||!redisToken.equals(token)){
-//            throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
-//        }
 
         Long userId = Long.parseLong(claims.getSubject());
         FsUser fsUser = fsUserMapper.selectFsUserByUserIdNotDel(userId);

+ 105 - 0
fs-user-app/src/main/java/com/fs/app/utils/SkipAuthTicketUtils.java

@@ -0,0 +1,105 @@
+package com.fs.app.utils;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Locale;
+
+/**
+ * X-Skip-Auth 短期票据:{@code userId.expireAtMs.hmacHex}
+ * <p>
+ * hmac = HMAC-SHA256(secret, userId + "." + expireAtMs) 的小写 hex。
+ */
+public final class SkipAuthTicketUtils {
+
+    private static final String HMAC_ALG = "HmacSHA256";
+
+    private SkipAuthTicketUtils() {
+    }
+
+    /**
+     * 内部调用方签发票据。
+     *
+     * @param userId     代操作的用户 ID
+     * @param ttlSeconds 有效期秒数
+     * @param secret     共享密钥
+     */
+    public static String create(long userId, long ttlSeconds, String secret) {
+        if (secret == null || secret.trim().isEmpty()) {
+            throw new IllegalArgumentException("skip-auth secret 未配置");
+        }
+        if (ttlSeconds <= 0) {
+            throw new IllegalArgumentException("ttlSeconds 必须大于 0");
+        }
+        long expireAtMs = System.currentTimeMillis() + ttlSeconds * 1000L;
+        String payload = userId + "." + expireAtMs;
+        return payload + "." + hmacHex(secret, payload);
+    }
+
+    /**
+     * 校验票据,成功返回 userId;失败抛 IllegalArgumentException。
+     *
+     * @param ticket           请求头值
+     * @param secret           共享密钥
+     * @param maxTtlSeconds    允许的最大有效期(防止签发过长)
+     */
+    public static long verifyAndGetUserId(String ticket, String secret, long maxTtlSeconds) {
+        if (ticket == null || ticket.trim().isEmpty()) {
+            throw new IllegalArgumentException("票据为空");
+        }
+        if (secret == null || secret.trim().isEmpty()) {
+            throw new IllegalArgumentException("skip-auth secret 未配置");
+        }
+        String[] parts = ticket.trim().split("\\.");
+        if (parts.length != 3) {
+            throw new IllegalArgumentException("票据格式错误");
+        }
+        final long userId;
+        final long expireAtMs;
+        try {
+            userId = Long.parseLong(parts[0]);
+            expireAtMs = Long.parseLong(parts[1]);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("票据字段非法");
+        }
+        long now = System.currentTimeMillis();
+        if (expireAtMs < now) {
+            throw new IllegalArgumentException("票据已过期");
+        }
+        // 剩余有效期不得超过配置的最大 TTL(防止签发过长票据)
+        if (maxTtlSeconds > 0 && expireAtMs - now > maxTtlSeconds * 1000L) {
+            throw new IllegalArgumentException("票据有效期超出允许范围");
+        }
+        String payload = parts[0] + "." + parts[1];
+        String expect = hmacHex(secret, payload);
+        if (!constantTimeEquals(expect, parts[2].toLowerCase(Locale.ROOT))) {
+            throw new IllegalArgumentException("票据签名无效");
+        }
+        return userId;
+    }
+
+    private static String hmacHex(String secret, String payload) {
+        try {
+            Mac mac = Mac.getInstance(HMAC_ALG);
+            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALG));
+            byte[] raw = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
+            StringBuilder sb = new StringBuilder(raw.length * 2);
+            for (byte b : raw) {
+                sb.append(String.format(Locale.ROOT, "%02x", b));
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            throw new IllegalStateException("HMAC 计算失败", e);
+        }
+    }
+
+    private static boolean constantTimeEquals(String a, String b) {
+        if (a == null || b == null) {
+            return false;
+        }
+        return MessageDigest.isEqual(
+                a.getBytes(StandardCharsets.UTF_8),
+                b.getBytes(StandardCharsets.UTF_8));
+    }
+}

+ 15 - 0
fs-wx-ipad-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -67,6 +67,7 @@ public class DataScopeAspect
     @Before("dataScopePointCut()")
     public void doBefore(JoinPoint point) throws Throwable
     {
+        clearDataScope(point);
         handleDataScope(point);
     }
 
@@ -168,4 +169,18 @@ public class DataScopeAspect
         }
         return null;
     }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+
 }

+ 15 - 0
fs-wx-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -67,6 +67,7 @@ public class DataScopeAspect
     @Before("dataScopePointCut()")
     public void doBefore(JoinPoint point) throws Throwable
     {
+        clearDataScope(point);
         handleDataScope(point);
     }
 
@@ -168,4 +169,18 @@ public class DataScopeAspect
         }
         return null;
     }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+
 }