diff --git a/.gitignore b/.gitignore index 49b90392..0e537acc 100644 --- a/.gitignore +++ b/.gitignore @@ -47,5 +47,6 @@ plan.md ### Docker 运行时数据与敏感配置 ### .env .env.properties +tmp_*secrets*.txt config/rootCA.pem config/nginx/certs/ diff --git a/frontend/apps/site/app/pages/blog/[id].vue b/frontend/apps/site/app/pages/blog/[id].vue index f4d5b7c4..3807e3c3 100644 --- a/frontend/apps/site/app/pages/blog/[id].vue +++ b/frontend/apps/site/app/pages/blog/[id].vue @@ -83,10 +83,12 @@ useSeoMeta({ +
+ diff --git a/frontend/layers/base/composables/useSanitizedHtml.ts b/frontend/layers/base/composables/useSanitizedHtml.ts index 6f6694ba..b82d286f 100644 --- a/frontend/layers/base/composables/useSanitizedHtml.ts +++ b/frontend/layers/base/composables/useSanitizedHtml.ts @@ -1,5 +1,49 @@ import DOMPurify from 'isomorphic-dompurify' +type SanitizeElementHookData = { + tagName: string +} + +type DOMPurifyWithHooks = typeof DOMPurify & { + addHook: ( + entryPoint: 'uponSanitizeElement', + hook: (node: Node, data: SanitizeElementHookData) => void + ) => void +} + +const ALLOWED_IFRAME_HOSTS = new Set([ + 'player.bilibili.com', + 'www.youtube.com', + 'www.youtube-nocookie.com', + 'player.vimeo.com', + 'open.spotify.com', + 'music.163.com' +]) + +function isAllowedIframeSrc(src: string | null): boolean { + if (!src) { + return false + } + + try { + const url = new URL(src) + return url.protocol === 'https:' && ALLOWED_IFRAME_HOSTS.has(url.hostname) + } catch { + return false + } +} + +(DOMPurify as DOMPurifyWithHooks).addHook('uponSanitizeElement', (node, data) => { + if (data.tagName !== 'iframe') { + return + } + + const element = node as Element + if (!isAllowedIframeSrc(element.getAttribute('src'))) { + element.remove() + } +}) + /** * 对 HTML 字符串进行 XSS 清洗,保留文章渲染所需的安全标签和属性。 * @@ -20,7 +64,6 @@ export function useSanitizedHtml(html: MaybeRef): ComputedRef | // 禁止表单和脚本 FORBID_TAGS: ['form', 'input', 'textarea', 'select', 'button', 'style'], FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'], - // iframe 仅允许白名单域 ALLOW_UNKNOWN_PROTOCOLS: false, }) diff --git a/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/controller/ProductApiController.java b/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/controller/ProductApiController.java index ba685473..5f301d63 100644 --- a/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/controller/ProductApiController.java +++ b/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/controller/ProductApiController.java @@ -2,6 +2,7 @@ import com.rymcu.mortise.core.result.GlobalResult; import com.rymcu.mortise.log.annotation.ApiLog; +import com.rymcu.mortise.product.api.dto.ApiProductModels.ProductDetailVO; import com.rymcu.mortise.product.api.facade.ProductCatalogApiFacade; import com.rymcu.mortise.product.entity.Product; import com.rymcu.mortise.web.annotation.ApiController; @@ -41,7 +42,7 @@ public GlobalResult> listProducts( @GetMapping("/{id}") @ApiLog("查询产品详情") @Operation(summary = "获取上架产品详情") - public GlobalResult getProductDetail( + public GlobalResult getProductDetail( @Parameter(description = "产品ID") @PathVariable("id") Long id) { return GlobalResult.success(productCatalogApiFacade.getProductDetail(id)); } diff --git a/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/dto/ApiProductModels.java b/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/dto/ApiProductModels.java new file mode 100644 index 00000000..dae98c80 --- /dev/null +++ b/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/dto/ApiProductModels.java @@ -0,0 +1,57 @@ +package com.rymcu.mortise.product.api.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +public final class ApiProductModels { + + private ApiProductModels() { + } + + @Data + public static class ProductDetailVO { + + private Long id; + private String productCode; + private String title; + private String subtitle; + private String description; + private String shortDescription; + private String coverImageUrl; + private List galleryImages; + private String productType; + private Long categoryId; + private String[] tags; + private Map features; + private Map specifications; + private String seoTitle; + private String seoDescription; + private String seoKeywords; + private Integer status; + private Boolean isFeatured; + private Integer sortNo; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdTime; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updatedTime; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime publishedTime; + private List skuTargets; + } + + @Data + public static class SkuTargetVO { + + private Long id; + private Long productSkuId; + private String targetType; + private Long targetId; + private Integer quantity; + private Integer validityDays; + private String accessLevel; + } +} diff --git a/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/facade/ProductCatalogApiFacade.java b/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/facade/ProductCatalogApiFacade.java index 407d89a8..b3158d2a 100644 --- a/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/facade/ProductCatalogApiFacade.java +++ b/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/facade/ProductCatalogApiFacade.java @@ -1,5 +1,6 @@ package com.rymcu.mortise.product.api.facade; +import com.rymcu.mortise.product.api.dto.ApiProductModels.ProductDetailVO; import com.rymcu.mortise.product.entity.Product; import java.util.List; @@ -9,7 +10,7 @@ public interface ProductCatalogApiFacade { List listProducts(String productType); - Product getProductDetail(Long id); + ProductDetailVO getProductDetail(Long id); Map listProductTypes(); } diff --git a/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/facade/impl/ProductCatalogApiFacadeImpl.java b/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/facade/impl/ProductCatalogApiFacadeImpl.java index e12360ba..a9644d9a 100644 --- a/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/facade/impl/ProductCatalogApiFacadeImpl.java +++ b/mortise-product/mortise-product-api/src/main/java/com/rymcu/mortise/product/api/facade/impl/ProductCatalogApiFacadeImpl.java @@ -1,20 +1,40 @@ package com.rymcu.mortise.product.api.facade.impl; +import com.rymcu.mortise.common.enumerate.Status; +import com.rymcu.mortise.product.api.dto.ApiProductModels.ProductDetailVO; +import com.rymcu.mortise.product.api.dto.ApiProductModels.SkuTargetVO; import com.rymcu.mortise.product.api.facade.ProductCatalogApiFacade; import com.rymcu.mortise.product.entity.Product; +import com.rymcu.mortise.product.entity.ProductSku; import com.rymcu.mortise.product.service.query.ProductQueryService; +import com.rymcu.mortise.product.service.query.ProductSkuQueryService; +import com.rymcu.mortise.product.service.query.ProductSkuTargetQueryService; +import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Component; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Component public class ProductCatalogApiFacadeImpl implements ProductCatalogApiFacade { + private static final String ACTIVE_SKU_STATUS = "active"; + private static final Integer ENABLED_TARGET_STATUS = Status.ENABLED.getCode(); + private final ProductQueryService productQueryService; + private final ProductSkuQueryService productSkuQueryService; + private final ProductSkuTargetQueryService productSkuTargetQueryService; - public ProductCatalogApiFacadeImpl(ProductQueryService productQueryService) { + public ProductCatalogApiFacadeImpl( + ProductQueryService productQueryService, + ProductSkuQueryService productSkuQueryService, + ProductSkuTargetQueryService productSkuTargetQueryService + ) { this.productQueryService = productQueryService; + this.productSkuQueryService = productSkuQueryService; + this.productSkuTargetQueryService = productSkuTargetQueryService; } @Override @@ -23,8 +43,39 @@ public List listProducts(String productType) { } @Override - public Product getProductDetail(Long id) { - return productQueryService.findPublishedById(id); + public ProductDetailVO getProductDetail(Long id) { + Product product = productQueryService.findPublishedById(id); + if (product == null) { + return null; + } + + ProductDetailVO detailVO = new ProductDetailVO(); + BeanUtils.copyProperties(product, detailVO); + + List skuList = productSkuQueryService.findByProductId(id); + if (skuList.isEmpty()) { + detailVO.setSkuTargets(Collections.emptyList()); + return detailVO; + } + + List skuIds = skuList.stream() + .filter(sku -> ACTIVE_SKU_STATUS.equals(sku.getStatus())) + .map(ProductSku::getId) + .toList(); + if (skuIds.isEmpty()) { + detailVO.setSkuTargets(Collections.emptyList()); + return detailVO; + } + + detailVO.setSkuTargets(productSkuTargetQueryService.findByProductSkuIds(skuIds).stream() + .filter(target -> ENABLED_TARGET_STATUS.equals(target.getStatus())) + .map(target -> { + SkuTargetVO targetVO = new SkuTargetVO(); + BeanUtils.copyProperties(target, targetVO); + return targetVO; + }) + .collect(Collectors.toList())); + return detailVO; } @Override diff --git a/mortise-product/mortise-product-application/src/main/java/com/rymcu/mortise/product/service/ProductSkuTargetService.java b/mortise-product/mortise-product-application/src/main/java/com/rymcu/mortise/product/service/ProductSkuTargetService.java new file mode 100644 index 00000000..76466c8b --- /dev/null +++ b/mortise-product/mortise-product-application/src/main/java/com/rymcu/mortise/product/service/ProductSkuTargetService.java @@ -0,0 +1,19 @@ +package com.rymcu.mortise.product.service; + +import com.mybatisflex.core.service.IService; +import com.rymcu.mortise.product.entity.ProductSkuTarget; + +import java.util.Collection; +import java.util.List; + +/** + * 产品 SKU 目标映射服务 + * + * @author ronger + */ +public interface ProductSkuTargetService extends IService { + + List findByProductSkuId(Long productSkuId); + + List findByProductSkuIds(Collection productSkuIds); +} diff --git a/mortise-product/mortise-product-application/src/main/java/com/rymcu/mortise/product/service/impl/ProductSkuTargetServiceImpl.java b/mortise-product/mortise-product-application/src/main/java/com/rymcu/mortise/product/service/impl/ProductSkuTargetServiceImpl.java new file mode 100644 index 00000000..5fdb13ef --- /dev/null +++ b/mortise-product/mortise-product-application/src/main/java/com/rymcu/mortise/product/service/impl/ProductSkuTargetServiceImpl.java @@ -0,0 +1,39 @@ +package com.rymcu.mortise.product.service.impl; + +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.spring.service.impl.ServiceImpl; +import com.rymcu.mortise.product.entity.ProductSkuTarget; +import com.rymcu.mortise.product.mapper.ProductSkuTargetMapper; +import com.rymcu.mortise.product.service.ProductSkuTargetService; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; + +import static com.rymcu.mortise.product.entity.table.ProductSkuTargetTableDef.PRODUCT_SKU_TARGET; + +@Service +public class ProductSkuTargetServiceImpl extends ServiceImpl + implements ProductSkuTargetService { + + @Override + public List findByProductSkuId(Long productSkuId) { + return mapper.selectListByQuery( + QueryWrapper.create() + .where(PRODUCT_SKU_TARGET.PRODUCT_SKU_ID.eq(productSkuId)) + .orderBy(PRODUCT_SKU_TARGET.CREATED_TIME.asc()) + ); + } + + @Override + public List findByProductSkuIds(Collection productSkuIds) { + if (productSkuIds == null || productSkuIds.isEmpty()) { + return List.of(); + } + return mapper.selectListByQuery( + QueryWrapper.create() + .where(PRODUCT_SKU_TARGET.PRODUCT_SKU_ID.in(productSkuIds)) + .orderBy(PRODUCT_SKU_TARGET.PRODUCT_SKU_ID.asc(), PRODUCT_SKU_TARGET.CREATED_TIME.asc()) + ); + } +} diff --git a/mortise-product/mortise-product-application/src/main/java/com/rymcu/mortise/product/service/query/ProductSkuTargetQueryService.java b/mortise-product/mortise-product-application/src/main/java/com/rymcu/mortise/product/service/query/ProductSkuTargetQueryService.java new file mode 100644 index 00000000..467c4e3f --- /dev/null +++ b/mortise-product/mortise-product-application/src/main/java/com/rymcu/mortise/product/service/query/ProductSkuTargetQueryService.java @@ -0,0 +1,15 @@ +package com.rymcu.mortise.product.service.query; + +import com.rymcu.mortise.product.entity.ProductSkuTarget; + +import java.util.Collection; +import java.util.List; + +public interface ProductSkuTargetQueryService { + + List findByProductSkuId(Long productSkuId); + + List findByProductSkuIds(Collection productSkuIds); + + ProductSkuTarget getById(Long id); +} diff --git a/mortise-product/mortise-product-application/src/main/java/com/rymcu/mortise/product/service/query/impl/ProductSkuTargetQueryServiceImpl.java b/mortise-product/mortise-product-application/src/main/java/com/rymcu/mortise/product/service/query/impl/ProductSkuTargetQueryServiceImpl.java new file mode 100644 index 00000000..5a69d047 --- /dev/null +++ b/mortise-product/mortise-product-application/src/main/java/com/rymcu/mortise/product/service/query/impl/ProductSkuTargetQueryServiceImpl.java @@ -0,0 +1,34 @@ +package com.rymcu.mortise.product.service.query.impl; + +import com.rymcu.mortise.product.entity.ProductSkuTarget; +import com.rymcu.mortise.product.service.ProductSkuTargetService; +import com.rymcu.mortise.product.service.query.ProductSkuTargetQueryService; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; + +@Service +public class ProductSkuTargetQueryServiceImpl implements ProductSkuTargetQueryService { + + private final ProductSkuTargetService productSkuTargetService; + + public ProductSkuTargetQueryServiceImpl(ProductSkuTargetService productSkuTargetService) { + this.productSkuTargetService = productSkuTargetService; + } + + @Override + public List findByProductSkuId(Long productSkuId) { + return productSkuTargetService.findByProductSkuId(productSkuId); + } + + @Override + public List findByProductSkuIds(Collection productSkuIds) { + return productSkuTargetService.findByProductSkuIds(productSkuIds); + } + + @Override + public ProductSkuTarget getById(Long id) { + return productSkuTargetService.getById(id); + } +} diff --git a/mortise-wechat/src/main/java/com/rymcu/mortise/wechat/controller/WeChatMessageController.java b/mortise-wechat/src/main/java/com/rymcu/mortise/wechat/controller/WeChatMessageController.java index c30ec330..231a55c9 100644 --- a/mortise-wechat/src/main/java/com/rymcu/mortise/wechat/controller/WeChatMessageController.java +++ b/mortise-wechat/src/main/java/com/rymcu/mortise/wechat/controller/WeChatMessageController.java @@ -1,7 +1,7 @@ package com.rymcu.mortise.wechat.controller; -import com.rymcu.mortise.web.annotation.ApiController; import com.rymcu.mortise.core.result.GlobalResult; +import com.rymcu.mortise.web.annotation.AdminController; import com.rymcu.mortise.wechat.entity.TemplateMessage; import com.rymcu.mortise.wechat.facade.WeChatMessageFacade; import com.rymcu.mortise.wechat.facade.WeChatMessageFacade.NewsMessageRequest; @@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.Map; @@ -20,7 +21,7 @@ * @author ronger * @since 1.0.0 */ -@ApiController +@AdminController @RequestMapping("/wechat/messages") @RequiredArgsConstructor @ConditionalOnBean(WeChatMessageFacade.class) @@ -38,6 +39,7 @@ public class WeChatMessageController { */ @Operation(summary = "发送模板消息", description = "发送微信模板消息给指定用户") @PostMapping("/template") + @PreAuthorize("hasAuthority('wechat:message:send')") public GlobalResult> sendTemplateMessage( @Parameter(description = "模板消息对象", required = true) @RequestBody TemplateMessage message, @@ -56,6 +58,7 @@ public GlobalResult> sendTemplateMessage( */ @Operation(summary = "发送文本消息", description = "发送客服文本消息给指定用户") @PostMapping("/text") + @PreAuthorize("hasAuthority('wechat:message:send')") public GlobalResult> sendTextMessage( @Parameter(description = "用户OpenID", required = true) @RequestParam String openId, @@ -75,6 +78,7 @@ public GlobalResult> sendTextMessage( */ @Operation(summary = "发送图文消息", description = "发送客服图文消息给指定用户") @PostMapping("/news") + @PreAuthorize("hasAuthority('wechat:message:send')") public GlobalResult> sendNewsMessage( @Parameter(description = "图文消息请求对象", required = true) @RequestBody NewsMessageRequest request, diff --git a/mortise-wechat/src/main/java/com/rymcu/mortise/wechat/controller/WeChatMpManagementController.java b/mortise-wechat/src/main/java/com/rymcu/mortise/wechat/controller/WeChatMpManagementController.java index eccbbe9e..b780e44c 100644 --- a/mortise-wechat/src/main/java/com/rymcu/mortise/wechat/controller/WeChatMpManagementController.java +++ b/mortise-wechat/src/main/java/com/rymcu/mortise/wechat/controller/WeChatMpManagementController.java @@ -1,13 +1,13 @@ package com.rymcu.mortise.wechat.controller; +import com.rymcu.mortise.core.result.GlobalResult; import com.rymcu.mortise.web.annotation.AdminController; import com.rymcu.mortise.wechat.facade.WeChatMpManagementFacade; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; -import java.util.HashMap; import java.util.Map; /** @@ -29,17 +29,13 @@ public class WeChatMpManagementController { *

从数据库重新加载所有配置,适用于批量变更后的刷新

*/ @PostMapping("/reload-all") - public ResponseEntity> reloadAll() { + @PreAuthorize("hasAuthority('wechat:account:edit')") + public GlobalResult> reloadAll() { try { - return ResponseEntity.ok(weChatMpManagementFacade.reloadAll()); + return GlobalResult.success(weChatMpManagementFacade.reloadAll()); } catch (Exception e) { log.error("Failed to reload all WeChat MP configurations", e); - - Map result = new HashMap<>(); - result.put("success", false); - result.put("message", "重新加载失败: " + e.getMessage()); - - return ResponseEntity.internalServerError().body(result); + return GlobalResult.error("重新加载失败: " + e.getMessage()); } } @@ -48,18 +44,13 @@ public ResponseEntity> reloadAll() { *

适用于单个账号配置变更后的热更新

*/ @PostMapping("/accounts/{accountId}/reload") - public ResponseEntity> reloadAccount(@PathVariable Long accountId) { + @PreAuthorize("hasAuthority('wechat:account:edit')") + public GlobalResult> reloadAccount(@PathVariable Long accountId) { try { - return ResponseEntity.ok(weChatMpManagementFacade.reloadAccount(accountId)); + return GlobalResult.success(weChatMpManagementFacade.reloadAccount(accountId)); } catch (Exception e) { log.error("Failed to reload WeChat MP configuration for accountId: {}", accountId, e); - - Map result = new HashMap<>(); - result.put("success", false); - result.put("message", "账号配置更新失败: " + e.getMessage()); - result.put("accountId", accountId); - - return ResponseEntity.internalServerError().body(result); + return GlobalResult.error("账号配置更新失败: " + e.getMessage()); } } @@ -68,18 +59,13 @@ public ResponseEntity> reloadAccount(@PathVariable Long acco *

适用于账号禁用或删除后的热移除

*/ @DeleteMapping("/accounts/{accountId}") - public ResponseEntity> removeAccount(@PathVariable Long accountId) { + @PreAuthorize("hasAuthority('wechat:account:delete')") + public GlobalResult> removeAccount(@PathVariable Long accountId) { try { - return ResponseEntity.ok(weChatMpManagementFacade.removeAccount(accountId)); + return GlobalResult.success(weChatMpManagementFacade.removeAccount(accountId)); } catch (Exception e) { log.error("Failed to remove WeChat MP configuration for accountId: {}", accountId, e); - - Map result = new HashMap<>(); - result.put("success", false); - result.put("message", "账号配置移除失败: " + e.getMessage()); - result.put("accountId", accountId); - - return ResponseEntity.internalServerError().body(result); + return GlobalResult.error("账号配置移除失败: " + e.getMessage()); } } @@ -88,8 +74,9 @@ public ResponseEntity> removeAccount(@PathVariable Long acco *

用于管理界面显示当前配置状态

*/ @GetMapping("/status") - public ResponseEntity> getStatus() { - return ResponseEntity.ok(weChatMpManagementFacade.getStatus()); + @PreAuthorize("hasAuthority('wechat:account:query')") + public GlobalResult> getStatus() { + return GlobalResult.success(weChatMpManagementFacade.getStatus()); } /** @@ -97,8 +84,9 @@ public ResponseEntity> getStatus() { *

用于验证配置是否正确

*/ @GetMapping("/accounts/{accountId}/test") - public ResponseEntity> testAccount(@PathVariable Long accountId) { - return ResponseEntity.ok(weChatMpManagementFacade.testAccount(accountId)); + @PreAuthorize("hasAuthority('wechat:account:query')") + public GlobalResult> testAccount(@PathVariable Long accountId) { + return GlobalResult.success(weChatMpManagementFacade.testAccount(accountId)); } } diff --git a/mortise-wechat/src/main/java/com/rymcu/mortise/wechat/handler/MsgHandler.java b/mortise-wechat/src/main/java/com/rymcu/mortise/wechat/handler/MsgHandler.java index fa49b970..782e1ef4 100644 --- a/mortise-wechat/src/main/java/com/rymcu/mortise/wechat/handler/MsgHandler.java +++ b/mortise-wechat/src/main/java/com/rymcu/mortise/wechat/handler/MsgHandler.java @@ -2,11 +2,13 @@ import com.rymcu.mortise.wechat.builder.TextBuilder; import com.rymcu.mortise.wechat.utils.JsonUtils; +import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import java.util.Map; @@ -16,6 +18,7 @@ /** * @author Binary Wang */ +@Slf4j @Component public class MsgHandler extends AbstractHandler { @@ -29,8 +32,10 @@ public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, } //当用户输入关键词如“你好”,“客服”等,并且有客服在线时,把消息转发给在线客服 + String contentText = wxMessage.getContent(); try { - if ((wxMessage.getContent().startsWith("你好") || wxMessage.getContent().startsWith("客服")) + if (StringUtils.isNotBlank(contentText) + && (contentText.startsWith("你好") || contentText.startsWith("客服")) && !weixinService.getKefuService().kfOnlineList() .getKfOnlineList().isEmpty()) { return WxMpXmlOutMessage.TRANSFER_CUSTOMER_SERVICE() @@ -38,7 +43,7 @@ public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, .toUser(wxMessage.getFromUser()).build(); } } catch (WxErrorException e) { - e.printStackTrace(); + log.warn("Failed to check WeChat customer service online list", e); } //TODO 组装回复消息 diff --git a/mortise-wechat/src/main/resources/db/migration/V439__Grant_Wechat_Message_Send_Permission.sql b/mortise-wechat/src/main/resources/db/migration/V439__Grant_Wechat_Message_Send_Permission.sql new file mode 100644 index 00000000..c7a106f5 --- /dev/null +++ b/mortise-wechat/src/main/resources/db/migration/V439__Grant_Wechat_Message_Send_Permission.sql @@ -0,0 +1,19 @@ +-- 为微信消息发送接口补齐独立按钮/API 权限 +INSERT INTO mortise.mortise_menu ( + id, label, permission, icon, href, status, del_flag, menu_type, sort_no, parent_id, created_time, updated_time +) +SELECT 61000000000000106, '发送微信消息', 'wechat:message:send', 'i-lucide-send', '', 1, 0, 2, 6, parent.id, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP +FROM mortise.mortise_menu parent +WHERE parent.permission = 'wechat:account' + AND NOT EXISTS (SELECT 1 FROM mortise.mortise_menu WHERE permission = 'wechat:message:send'); + +INSERT INTO mortise.mortise_role_menu (id_mortise_role, id_mortise_menu) +SELECT role.id, menu.id +FROM mortise.mortise_role role +CROSS JOIN mortise.mortise_menu menu +WHERE role.permission = 'ADMIN' + AND menu.permission = 'wechat:message:send' + AND NOT EXISTS ( + SELECT 1 FROM mortise.mortise_role_menu rm + WHERE rm.id_mortise_role = role.id AND rm.id_mortise_menu = menu.id + );