Browse Source

初始化项目

yanxh 2 days ago
commit
7a35ff4354
100 changed files with 6081 additions and 0 deletions
  1. 148 0
      .gitignore
  2. 452 0
      README.md
  3. 57 0
      docker/.env.example
  4. 24 0
      docker/Dockerfile
  5. 34 0
      docker/config/application.yaml.example
  6. 45 0
      docker/docker-compose.yml
  7. 10 0
      docker/entrypoint.sh
  8. 411 0
      docker/start.sh
  9. 237 0
      jeesharp-business/DELIVERY_SUMMARY.md
  10. 388 0
      jeesharp-business/HOTEL_SCENIC_DEPLOYMENT.md
  11. 293 0
      jeesharp-business/MINIPROGRAM_BUSINESS_CHECKLIST.md
  12. 31 0
      jeesharp-business/business-ai/business-ai-application/pom.xml
  13. 160 0
      jeesharp-business/business-ai/business-ai-application/src/main/java/com/jeesharp/business/modules/ai/application/AiChatFeedbackApplicationService.java
  14. 136 0
      jeesharp-business/business-ai/business-ai-application/src/main/java/com/jeesharp/business/modules/ai/application/AiChatMessageApplicationService.java
  15. 221 0
      jeesharp-business/business-ai/business-ai-application/src/main/java/com/jeesharp/business/modules/ai/application/AiChatMobileApplicationService.java
  16. 110 0
      jeesharp-business/business-ai/business-ai-application/src/main/java/com/jeesharp/business/modules/ai/application/AiChatSessionApplicationService.java
  17. 132 0
      jeesharp-business/business-ai/business-ai-application/src/main/java/com/jeesharp/business/modules/ai/application/AiFaqApplicationService.java
  18. 47 0
      jeesharp-business/business-ai/business-ai-application/src/main/java/com/jeesharp/business/modules/ai/application/AiFaqCategoryApplicationService.java
  19. 30 0
      jeesharp-business/business-ai/business-ai-bean/pom.xml
  20. 16 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/convert/AiChatFeedbackConvert.java
  21. 16 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/convert/AiChatMessageConvert.java
  22. 22 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/convert/AiChatSessionConvert.java
  23. 16 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/convert/AiFaqCategoryConvert.java
  24. 16 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/convert/AiFaqConvert.java
  25. 23 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiChatFeedbackDTO.java
  26. 25 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiChatFeedbackSaveDTO.java
  27. 18 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiChatMessageDTO.java
  28. 31 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiChatMessageSaveDTO.java
  29. 12 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiChatSessionCreateDTO.java
  30. 28 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiChatSessionSaveDTO.java
  31. 20 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiFaqCategorySaveDTO.java
  32. 28 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiFaqSaveDTO.java
  33. 21 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/entity/AiChatFeedback.java
  34. 25 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/entity/AiChatMessage.java
  35. 28 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/entity/AiChatSession.java
  36. 22 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/entity/AiFaq.java
  37. 19 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/entity/AiFaqCategory.java
  38. 25 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/support/AiPlatformTenantConstants.java
  39. 15 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatAnswerVO.java
  40. 39 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatFeedbackInfoVO.java
  41. 39 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatFeedbackListVO.java
  42. 47 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatFeedbackQueryVO.java
  43. 16 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatHistoryVO.java
  44. 39 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatMessageInfoVO.java
  45. 39 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatMessageListVO.java
  46. 43 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatMessageQueryVO.java
  47. 11 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatSessionCreateVO.java
  48. 36 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatSessionInfoVO.java
  49. 36 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatSessionListVO.java
  50. 34 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatSessionQueryVO.java
  51. 18 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiFaqCategoryInfoVO.java
  52. 18 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiFaqCategoryListVO.java
  53. 13 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiFaqCategoryQueryVO.java
  54. 21 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiFaqInfoVO.java
  55. 23 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiFaqListVO.java
  56. 30 0
      jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiFaqQueryVO.java
  57. 30 0
      jeesharp-business/business-ai/business-ai-controller/pom.xml
  58. 57 0
      jeesharp-business/business-ai/business-ai-controller/src/main/java/com/jeesharp/business/modules/ai/controller/AiChatFeedbackController.java
  59. 57 0
      jeesharp-business/business-ai/business-ai-controller/src/main/java/com/jeesharp/business/modules/ai/controller/AiChatMessageController.java
  60. 57 0
      jeesharp-business/business-ai/business-ai-controller/src/main/java/com/jeesharp/business/modules/ai/controller/AiChatSessionController.java
  61. 57 0
      jeesharp-business/business-ai/business-ai-controller/src/main/java/com/jeesharp/business/modules/ai/controller/AiFaqCategoryController.java
  62. 57 0
      jeesharp-business/business-ai/business-ai-controller/src/main/java/com/jeesharp/business/modules/ai/controller/AiFaqController.java
  63. 26 0
      jeesharp-business/business-ai/business-ai-service/pom.xml
  64. 10 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/AiChatFeedbackMapper.java
  65. 10 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/AiChatMessageMapper.java
  66. 10 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/AiChatSessionMapper.java
  67. 10 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/AiFaqCategoryMapper.java
  68. 10 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/AiFaqMapper.java
  69. 80 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/xml/AiChatFeedbackMapper.xml
  70. 85 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/xml/AiChatMessageMapper.xml
  71. 81 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/xml/AiChatSessionMapper.xml
  72. 58 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/xml/AiFaqCategoryMapper.xml
  73. 69 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/xml/AiFaqMapper.xml
  74. 12 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/AiChatFeedbackService.java
  75. 30 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/AiChatMessageService.java
  76. 30 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/AiChatSessionService.java
  77. 24 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/AiFaqCategoryService.java
  78. 15 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/AiFaqService.java
  79. 32 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/impl/AiChatFeedbackServiceImpl.java
  80. 92 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/impl/AiChatMessageServiceImpl.java
  81. 88 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/impl/AiChatSessionServiceImpl.java
  82. 62 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/impl/AiFaqCategoryServiceImpl.java
  83. 72 0
      jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/impl/AiFaqServiceImpl.java
  84. 29 0
      jeesharp-business/business-ai/pom.xml
  85. 70 0
      jeesharp-business/business-common/README.md
  86. 23 0
      jeesharp-business/business-common/business-common-api/pom.xml
  87. 29 0
      jeesharp-business/business-common/business-common-api/src/main/java/com/jeesharp/business/modules/common/api/BizSceneApi.java
  88. 39 0
      jeesharp-business/business-common/business-common-application/pom.xml
  89. 37 0
      jeesharp-business/business-common/business-common-application/src/main/java/com/jeesharp/business/modules/common/application/api/BizSceneApiImpl.java
  90. 119 0
      jeesharp-business/business-common/business-common-application/src/main/java/com/jeesharp/business/modules/common/bizscene/application/BizSceneApplicationService.java
  91. 29 0
      jeesharp-business/business-common/business-common-bean/pom.xml
  92. 22 0
      jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/convert/BizSceneConvert.java
  93. 59 0
      jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/dto/BizSceneSaveDTO.java
  94. 60 0
      jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/entity/BizScene.java
  95. 17 0
      jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/enums/BizSceneCapabilityEnum.java
  96. 41 0
      jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/enums/BizSceneGroupEnum.java
  97. 31 0
      jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/enums/BizSceneStatusEnum.java
  98. 152 0
      jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/support/BizSceneAdminListRecordSupport.java
  99. 130 0
      jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/support/BizSceneCouponLegacySupport.java
  100. 79 0
      jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/support/BizSceneMobileJumpSupport.java

+ 148 - 0
.gitignore

@@ -0,0 +1,148 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+.kotlin
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+.idea/**
+.trae/**
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
+
+
+doc/
+.idea/
+
+business-test/**
+/.cursor/debug.log
+/log/
+/log/debug/log-debug-2026-05-09.0.log
+/log/debug/log-debug-2026-05-11.0.log
+/log/debug/log-debug-2026-05-12.0.log
+/log/debug/log-debug-2026-05-13.0.log
+/log/debug/log-debug-2026-05-14.0.log
+/log/debug/log-debug-2026-05-15.0.log
+/log/debug/log-debug-2026-05-16.0.log
+/log/debug/log-debug-2026-05-18.0.log
+/log/debug/log-debug-2026-05-19.0.log
+/log/debug/log-debug-2026-05-20.0.log
+/log/debug/log-debug-2026-05-21.0.log
+/log/debug/log-debug-2026-05-25.0.log
+/log/debug/log-debug-2026-05-26.0.log
+/log/debug/log-debug-2026-05-27.0.log
+/log/debug/log-debug-2026-05-28.0.log
+/log/debug/log-debug-2026-05-29.0.log
+/log/debug/log-debug-2026-06-01.0.log
+/log/debug/log-debug-2026-06-02.0.log
+/log/debug/log-debug-2026-06-04.0.log
+/log/debug/log-debug-2026-06-05.0.log
+/log/debug/log-debug-2026-06-06.0.log
+/log/error/log-error-2026-05-09.0.log
+/log/error/log-error-2026-05-10.0.log
+/log/error/log-error-2026-05-11.0.log
+/log/error/log-error-2026-05-12.0.log
+/log/error/log-error-2026-05-13.0.log
+/log/error/log-error-2026-05-14.0.log
+/log/error/log-error-2026-05-15.0.log
+/log/error/log-error-2026-05-16.0.log
+/log/error/log-error-2026-05-18.0.log
+/log/error/log-error-2026-05-19.0.log
+/log/error/log-error-2026-05-20.0.log
+/log/error/log-error-2026-05-21.0.log
+/log/error/log-error-2026-05-25.0.log
+/log/error/log-error-2026-05-26.0.log
+/log/error/log-error-2026-05-27.0.log
+/log/error/log-error-2026-05-28.0.log
+/log/error/log-error-2026-05-29.0.log
+/log/error/log-error-2026-06-01.0.log
+/log/error/log-error-2026-06-02.0.log
+/log/error/log-error-2026-06-04.0.log
+/log/error/log-error-2026-06-05.0.log
+/log/error/log-error-2026-06-06.0.log
+/log/info/log-info-2026-05-09.0.log
+/log/info/log-info-2026-05-10.0.log
+/log/info/log-info-2026-05-11.0.log
+/log/info/log-info-2026-05-12.0.log
+/log/info/log-info-2026-05-13.0.log
+/log/info/log-info-2026-05-14.0.log
+/log/info/log-info-2026-05-15.0.log
+/log/info/log-info-2026-05-16.0.log
+/log/info/log-info-2026-05-18.0.log
+/log/info/log-info-2026-05-19.0.log
+/log/info/log-info-2026-05-20.0.log
+/log/info/log-info-2026-05-21.0.log
+/log/info/log-info-2026-05-25.0.log
+/log/info/log-info-2026-05-26.0.log
+/log/info/log-info-2026-05-27.0.log
+/log/info/log-info-2026-05-28.0.log
+/log/info/log-info-2026-05-29.0.log
+/log/info/log-info-2026-05-30.0.log
+/log/info/log-info-2026-06-01.0.log
+/log/info/log-info-2026-06-02.0.log
+/log/info/log-info-2026-06-04.0.log
+/log/info/log-info-2026-06-05.0.log
+/log/info/log-info-2026-06-06.0.log
+/log/warn/log-warn-2026-05-09.0.log
+/log/warn/log-warn-2026-05-11.0.log
+/log/warn/log-warn-2026-05-12.0.log
+/log/warn/log-warn-2026-05-13.0.log
+/log/warn/log-warn-2026-05-14.0.log
+/log/warn/log-warn-2026-05-15.0.log
+/log/warn/log-warn-2026-05-16.0.log
+/log/warn/log-warn-2026-05-18.0.log
+/log/warn/log-warn-2026-05-19.0.log
+/log/warn/log-warn-2026-05-20.0.log
+/log/warn/log-warn-2026-05-21.0.log
+/log/warn/log-warn-2026-05-25.0.log
+/log/warn/log-warn-2026-05-26.0.log
+/log/warn/log-warn-2026-05-27.0.log
+/log/warn/log-warn-2026-05-28.0.log
+/log/warn/log-warn-2026-05-29.0.log
+/log/warn/log-warn-2026-06-01.0.log
+/log/warn/log-warn-2026-06-02.0.log
+/log/warn/log-warn-2026-06-04.0.log
+/log/warn/log-warn-2026-06-05.0.log
+/log/warn/log-warn-2026-06-06.0.log
+/log/log_debug.log
+/log/log_error.log
+/log/log_info.log
+/log/log_warn.log
+/docker/.env
+/docker/data/
+/docker/.build.log
+/docker/.compose.log
+
+/log/
+/web-starter/log/
+

+ 452 - 0
README.md

@@ -0,0 +1,452 @@
+apifox:afxp_dbfc98PyOcegSserl9U9k1wRhLfZJ5MGBUIA
+
+---
+
+## 待办清单
+
+### XXL-Job 定时任务
+
+> 代码已从 Spring `@Scheduled` 改造为 `@XxlJob`;Cron 改由 **XXL-Job 调度中心** 配置。  
+> 依赖:`jeesharp-spring-boot-starter-job`(业务 application 模块 + `web-starter`)。  
+> 管理端页面 API 需开启:`jeesharp.xxl.job.admin-client.enabled=true`。
+
+#### 一、已在代码中实现的 JobHandler(需在调度中心注册)
+
+| 状态 | JobHandler | 模块 | 类 | 建议 Cron | 说明 |
+|------|------------|------|-----|-----------|------|
+| [ ] | `cmsContentScheduledPublishJob` | portal-cms | `CmsContentScheduledPublishJob` | `0 */1 * * * ?` | CMS 定时发布(每分钟扫描到期内容) |
+| [ ] | `cmsStaticNightlyGenerateJob` | portal-cms | `CmsStaticNightlyGenerateJob` | `0 0 2 * * ?` | CMS 夜间全站静态化 |
+| [ ] | `mallOrderPayTimeoutCancelJob` | mall | `MallOrderPayTimeoutCancelJob` | `0 */5 * * * ?` | 商城待支付订单超时取消 |
+| [ ] | `commissionSettleJob` | commission | `CommissionSettleJob` | `0 */10 * * * ?` | 佣金冷静期到期结算(任务参数可传 batchSize,默认 50) |
+| [ ] | `profitSharingRetryJob` | finance | `ProfitSharingRetryJob` | `0 */10 * * * ?` | 分账失败自动重试 |
+| [ ] | `platformRevenueRollupJob` | finance | `PlatformRevenueRollupJob` | `0 15 0 * * ?` | 平台收入日汇总(昨日) |
+| [ ] | `yearcardBenefitWalletPeriodResetJob` | yearcard | `YearcardBenefitWalletPeriodResetJob` | `0 5 0 * * ?` | 年卡权益钱包周期配额重置 |
+
+**调度中心注册建议**:路由策略「第一个」、阻塞策略「单机串行」。
+
+#### 二、环境与配置待办
+
+| 状态 | 事项 |
+|------|------|
+| [ ] | `jeesharp.xxl.job.enabled=true`(执行器注册,任务才会被触发) |
+| [ ] | `jeesharp.xxl.job.admin-client.enabled=true`(管理端「定时任务」页面 API) |
+| [ ] | `jeesharp.xxl.job.admin.addresses` 指向可访问的 XXL-Job Admin |
+| [ ] | `admin-client.username/password` 与 XXL-Job Admin 登录账号一致 |
+| [ ] | 执行器 `app-name` 与调度中心执行器管理中的 AppName 一致 |
+| [ ] | 角色菜单权限:`manager:system:scheduleTask:list` 等(页面 403 时检查) |
+
+#### 三、可选扩展(代码预留,尚未挂 Job)
+
+| 状态 | 建议 JobHandler | 模块 | 说明 |
+|------|-----------------|------|------|
+| [ ] | `mallSeckillSessionStatusJob` | mall | `MallSeckillSessionApplicationService#updateSessionStatus` |
+| [ ] | `foodTradeOrderShadowVerifyJob` | food | `FoodTradeOrderShadowService` 影子核对(需封装批量扫描) |
+| [ ] | `leisureTradeOrderShadowVerifyJob` | leisure | `LeisureTradeOrderShadowService` |
+| [ ] | `mallTradeOrderShadowVerifyJob` | mall | `MallTradeOrderShadowService` |
+| [ ] | `hotelTradeOrderShadowVerifyJob` | hotel | `HotelTradeOrderShadowService` |
+| [ ] | `scenicTradeOrderShadowVerifyJob` | scenic | `ScenicTradeOrderShadowService` |
+
+---
+
+## Cursor AI 提示词使用指南
+
+> **说明**:本文档提供常用的 Cursor AI 提示词模板,帮助快速生成代码、文档和数据库脚本。  
+> **详细规则**:请参考 `.cursor/rules/` 目录下的规则文件
+
+---
+
+## 📋 常用提示词模板
+
+### 1. 开发完整模块
+
+**提示词模板**:
+```
+基于项目规则,在 business-{模块名} 下开发一个 {功能名称} 模块,包含:
+1. 数据库表设计(遵循 mysql-rules.mdc 规范)
+2. Bean 层代码(Entity、DTO、VO、Convert、Enum)
+3. Service 层代码(Domain Service、Mapper)
+4. Application 层代码(Application Service)
+5. Controller 层代码(后台管理端 Controller)
+6. 移动端 Controller(如需要)
+7. 后台管理端接口文档
+8. 移动端接口文档(如需要)
+9. 模块 README 文档
+
+模块名:{模块名}
+类名:{类名}
+功能说明:{功能描述}
+```
+
+**示例**:
+```
+基于项目规则,在 business-member 下开发一个会员管理模块,包含:
+1. 数据库表设计(遵循 mysql-rules.mdc 规范)
+2. Bean 层代码(Entity、DTO、VO、Convert、Enum)
+3. Service 层代码(Domain Service、Mapper)
+4. Application 层代码(Application Service)
+5. Controller 层代码(后台管理端 Controller)
+6. 移动端 Controller
+7. 后台管理端接口文档
+8. 移动端接口文档
+9. 模块 README 文档
+
+模块名:member
+类名:member
+功能说明:会员信息管理,包括会员注册、登录、信息维护等功能
+```
+
+---
+
+### 2. 代码生成
+
+#### 2.1 生成后台管理端代码
+
+**提示词模板**:
+```
+根据 pc-java-code-implementation-rules.mdc 规则,生成 {功能名称} 的后台管理端代码:
+- 模块名:{模块名}
+- 类名:{类名}
+- 包含:Entity、DTO、VO(QueryVO、InfoVO、ExportVO、ImportVO)、Convert、Enum、Service、Application Service、Controller
+- 代码位置:jeesharp-business/business-{模块名}-*
+```
+
+**示例**:
+```
+根据 pc-java-code-implementation-rules.mdc 规则,生成订单管理的后台管理端代码:
+- 模块名:mall
+- 类名:order
+- 包含:Entity、DTO、VO(QueryVO、InfoVO、ExportVO、ImportVO)、Convert、Enum、Service、Application Service、Controller
+- 代码位置:jeesharp-business/business-mall-*
+```
+
+#### 2.2 生成移动端代码
+
+**提示词模板**:
+```
+根据 mobile-java-generation-code-rules.mdc 规则,生成 {功能名称} 的移动端代码:
+- 模块名:{模块名}
+- 类名:{类名}
+- 包含:Controller(继承 BaseMobileController)、DTO、VO
+- 代码位置:business-mobile-gateway
+```
+
+**示例**:
+```
+根据 mobile-java-generation-code-rules.mdc 规则,生成会员信息的移动端代码:
+- 模块名:member
+- 类名:member
+- 包含:Controller(继承 BaseMobileController)、DTO、VO
+- 代码位置:business-mobile-gateway
+```
+
+#### 2.3 生成 Bean 层代码
+
+**提示词模板**:
+```
+根据 pc-java-code-implementation-rules.mdc 第 6.5 节规则,生成 {功能名称} 的 Bean 层代码:
+- 模块名:{模块名}
+- 类名:{类名}
+- 包含:Entity、DTO(SaveDTO)、VO(QueryVO、InfoVO、ExportVO、ImportVO)、Convert、Enum
+- 代码位置:business-{模块名}-bean
+```
+
+**示例**:
+```
+根据 pc-java-code-implementation-rules.mdc 第 6.5 节规则,生成订单的 Bean 层代码:
+- 模块名:mall
+- 类名:order
+- 包含:Entity、DTO(SaveDTO)、VO(QueryVO、InfoVO、ExportVO、ImportVO)、Convert、Enum
+- 代码位置:business-mall-bean
+```
+
+---
+
+### 3. 数据库建表
+
+**提示词模板**:
+```
+根据 mysql-rules.mdc 规则,创建 {表名} 数据库表:
+- 表名:{表前缀}_{表名}(如 biz_order、app_member)
+- 功能说明:{功能描述}
+- 包含字段:{字段列表}
+- 必需基础字段:id、create_by、create_date、update_by、update_date、del_flag、remarks、tenant_code、tenant_id
+- 索引:idx_tenant_id、idx_status(如有)、idx_create_date
+```
+
+**示例**:
+```
+根据 mysql-rules.mdc 规则,创建订单数据库表:
+- 表名:biz_order
+- 功能说明:订单主表,存储订单基本信息
+- 包含字段:order_no(订单编号)、user_id(用户ID)、product_id(商品ID)、quantity(数量)、total_amount(总金额)、status(订单状态:0-待支付 1-已支付 2-已发货 3-已完成 4-已取消)
+- 必需基础字段:id、create_by、create_date、update_by、update_date、del_flag、remarks、tenant_code、tenant_id
+- 索引:idx_tenant_id、idx_status、idx_create_date、idx_user_id
+```
+
+---
+
+### 4. 接口文档生成
+
+#### 4.1 生成后台管理端接口文档
+
+**提示词模板**:
+```
+根据 frontend-api-rules.mdc 规则,在 {模块目录}/doc/ 目录下生成 {功能名称} 的后台管理端接口文档:
+- 模块名:{模块名}
+- 类名:{类名}
+- 接口路径:${adminPath}/{模块名}/{类名}/v1/{操作}
+- 包含接口:info(详情)、list(列表分页)、save(保存)、delete(删除)、export(导出)、import(导入)、import/template(下载模板)
+- 权限标识:{模块名}:{类名}:{操作}
+```
+
+**示例**:
+```
+根据 frontend-api-rules.mdc 规则,在 business-mall/doc/ 目录下生成订单管理的后台管理端接口文档:
+- 模块名:mall
+- 类名:order
+- 接口路径:${adminPath}/mall/order/v1/{操作}
+- 包含接口:info(详情)、list(列表分页)、save(保存)、delete(删除)、export(导出)、import(导入)、import/template(下载模板)
+- 权限标识:mall:order:{操作}
+```
+
+#### 4.2 生成移动端接口文档
+
+**提示词模板**:
+```
+根据 mobile-api-rules.mdc 规则,在 {模块目录}/doc/ 目录下生成 {功能名称} 的移动端接口文档:
+- 模块名:{模块名}
+- 类名:{类名}
+- 接口路径:/mobile/{模块名}/{类名}/{操作}
+- 包含接口:{接口列表}
+- 请求头:access_token、X-Device-Id、X-Platform、X-Version
+```
+
+**示例**:
+```
+根据 mobile-api-rules.mdc 规则,在 business-member/doc/ 目录下生成会员信息的移动端接口文档:
+- 模块名:member
+- 类名:member
+- 接口路径:/mobile/member/member/{操作}
+- 包含接口:info(获取会员信息)、update(更新会员信息)
+- 请求头:access_token、X-Device-Id、X-Platform、X-Version
+```
+
+---
+
+### 5. 模块文档生成
+
+**提示词模板**:
+```
+根据 documentation-rules.mdc 规则,在 business-{模块名}/ 目录下生成模块 README 文档:
+- 模块名:{模块名}
+- 功能说明:{功能描述}
+- 包含内容:
+  1. 模块概述
+  2. 模块结构
+  3. 功能特性
+  4. API 接口(接口路径和基本说明)
+  5. 权限配置(权限标识列表)
+  6. 基础字段规范
+  7. 数据库表设计
+  8. 版本记录
+```
+
+**示例**:
+```
+根据 documentation-rules.mdc 规则,在 business-mall/ 目录下生成模块 README 文档:
+- 模块名:mall
+- 功能说明:商城模块,包含订单管理、商品管理等功能
+- 包含内容:
+  1. 模块概述
+  2. 模块结构
+  3. 功能特性
+  4. API 接口(接口路径和基本说明)
+  5. 权限配置(权限标识列表)
+  6. 基础字段规范
+  7. 数据库表设计
+  8. 版本记录
+```
+
+---
+
+### 6. 快速修复和优化
+
+#### 6.1 修复依赖关系
+
+**提示词模板**:
+```
+检查并修复 business-{模块名} 模块的依赖关系:
+- 检查 Bean 模块是否包含 jeesharp-spring-boot-starter-mybatis 依赖
+- 检查 Service 模块是否依赖 Bean 模块
+- 检查 Application 模块是否依赖 Service 模块
+- 检查 Controller 模块是否依赖 Application 模块
+- 如缺失,自动添加依赖
+```
+
+#### 6.2 添加依赖
+
+**提示词模板**:
+```
+根据 pre-check-rules.mdc 第 0.6 节依赖版本管理规则,添加 {依赖名称} 依赖:
+- groupId: {groupId}
+- artifactId: {artifactId}
+- 模块:{模块名}
+- 检查 jeesharp-parent 中是否已定义
+- 如已定义(jeesharp 框架依赖或常见框架依赖),不指定版本号
+- 如未定义(第三方依赖),指定版本号 {version}
+- 添加到 {模块名} 模块的 pom.xml
+```
+
+**示例**:
+```
+根据 pre-check-rules.mdc 第 0.6 节依赖版本管理规则,添加 jeesharp-spring-boot-starter-mybatis 依赖:
+- groupId: org.jeesharp.boot
+- artifactId: jeesharp-spring-boot-starter-mybatis
+- 模块:business-member-bean
+- jeesharp 框架依赖,不指定版本号(从 jeesharp-parent 继承)
+- 添加到 business-member-bean 模块的 pom.xml
+```
+
+**第三方依赖示例**:
+```
+根据 pre-check-rules.mdc 第 0.6 节依赖版本管理规则,添加 JustAuth 依赖:
+- groupId: me.zhyd.oauth
+- artifactId: JustAuth
+- 模块:business-mobile-gateway
+- 第三方依赖,jeesharp-parent 中未定义,指定版本号 1.16.6
+- 添加到 business-mobile-gateway 模块的 pom.xml
+```
+
+#### 6.3 添加枚举定义
+
+**提示词模板**:
+```
+根据 pc-java-code-implementation-rules.mdc 第 6.5.0 节规则,为 {功能名称} 添加 {字段名} 枚举:
+- 模块名:{模块名}
+- 类名:{类名}
+- 枚举名:{Xxx}StatusEnum(或 {Xxx}TypeEnum)
+- 枚举值:{枚举值列表}
+- 位置:business-{模块名}-bean/src/main/java/com/jeesharp/business/modules/{模块名}/{类名}/enums/
+```
+
+**示例**:
+```
+根据 pc-java-code-implementation-rules.mdc 第 6.5.0 节规则,为订单管理添加订单状态枚举:
+- 模块名:mall
+- 类名:order
+- 枚举名:OrderStatusEnum
+- 枚举值:PENDING(0, "待支付")、PAID(1, "已支付")、SHIPPED(2, "已发货")、COMPLETED(3, "已完成")、CANCELLED(4, "已取消")
+- 位置:business-mall-bean/src/main/java/com/jeesharp/business/modules/mall/order/enums/
+```
+
+---
+
+## 🎯 提示词使用技巧
+
+### 1. 明确指定规则文件
+- ✅ **正确**:`根据 frontend-api-rules.mdc 规则,生成...`
+- ❌ **错误**:`生成接口文档`(未指定规则文件)
+
+### 2. 提供完整的上下文信息
+- ✅ **正确**:提供模块名、类名、功能说明等完整信息
+- ❌ **错误**:只提供功能名称,缺少模块信息
+
+### 3. 明确代码生成位置
+- ✅ **正确**:`代码位置:jeesharp-business/business-{模块名}-*`
+- ❌ **错误**:`生成代码`(未指定位置)
+
+### 4. 遵循命名规范
+- ✅ **正确**:模块名使用小写(如 `mall`、`member`),类名使用小写(如 `order`、`product`)
+- ❌ **错误**:使用大写或驼峰命名(如 `Mall`、`Order`)
+
+---
+
+## 📚 相关规则文件
+
+| 用途 | 规则文件 | 说明 |
+|------|---------|------|
+| **代码生成** | `.cursor/rules/code/pc-java-code-implementation-rules.mdc` | 后台管理端代码实现规范 |
+| **移动端代码** | `.cursor/rules/code/mobile-java-generation-code-rules.mdc` | 移动端代码生成规范 |
+| **接口规范** | `.cursor/rules/api/frontend-api-rules.mdc` | 后台管理端接口规范 |
+| **移动端接口** | `.cursor/rules/api/mobile-api-rules.mdc` | 移动端接口规范 |
+| **数据库规范** | `.cursor/rules/database/mysql-rules.mdc` | MySQL 建表规范 |
+| **文档规范** | `.cursor/rules/documentation/documentation-rules.mdc` | 文档编写规范 |
+| **命名规范** | `.cursor/rules/code/naming-rules.mdc` | 命名规范 |
+| **架构规范** | `.cursor/rules/architecture/architecture-rules.mdc` | 架构设计原则 |
+
+---
+
+## 💡 示例场景
+
+### 场景 1:开发订单管理模块
+
+```
+基于项目规则,在 business-mall 下开发一个订单管理模块,包含:
+1. 数据库表设计(遵循 mysql-rules.mdc 规范)
+   - 表名:biz_order
+   - 字段:order_no、user_id、product_id、quantity、total_amount、status
+2. Bean 层代码(Entity、DTO、VO、Convert、Enum)
+3. Service 层代码(Domain Service、Mapper)
+4. Application 层代码(Application Service)
+5. Controller 层代码(后台管理端 Controller)
+6. 后台管理端接口文档
+7. 模块 README 文档
+
+模块名:mall
+类名:order
+功能说明:订单管理,包括订单创建、查询、更新、删除等功能
+```
+
+### 场景 2:生成移动端会员接口
+
+```
+根据 mobile-api-rules.mdc 规则,生成会员信息的移动端代码和接口文档:
+- 模块名:member
+- 类名:member
+- 包含:Controller(继承 BaseMobileController)、DTO、VO
+- 接口:info(获取会员信息)、update(更新会员信息)
+- 代码位置:business-mobile-gateway
+- 文档位置:business-member/doc/
+```
+
+---
+
+**最后更新**:2026-07-13  
+**维护者**:项目团队
+
+
+根据小程序docs下的设计文档、小程序接口文档,在后台项目开发酒店模块,后台要在jeesharp-business下新建一个模块,同时编写后台的前端模块,提供小程序端接口,输出代码、建表语句、菜单和权限的插入sql,按开发规约开始写代码
+
+
+
+
+
+
+INSERT INTO `xinghc_travel`.`pay_wechat_config` (`id`, `channel_config_id`, `config_code`, `config_name`, `status`, `wx_enabled`, `mock_fallback`, `app_id`, `mch_id`, `wechat_pay_mode`, `merchant_serial_no`, `merchant_private_key_pem`, `public_key_id`, `public_key_pem`, `public_key_path`, `cert_path`, `cert_private_key_path`, `cert_private_cert_path`, `mch_key`, `notify_base_url`, `pay_center_notify_path`, `api_v3_key`, `skip_signature_verify`, `platform_serial_no`, `create_by`, `create_date`, `update_by`, `update_date`, `del_flag`, `remarks`, `tenant_code`, `tenant_id`, `organize_id`) VALUES ('2059120010287452162', '2059513091620925441', 'DEFAULT', '杏花村微信支付', 1, 1, 0, 'wx2aa33e1b0f13463e', '1746874930', 'DIRECT', '38E1EAB92C9B1DAEA2CACB02CAC44F08F04533B9', '', 'PUB_KEY_ID_0117468749302026061200191626000205', '', '/Users/qiyuan/workspace_fangshi/code/xinghuacun_travel/TravelWebSystem/doc/pay/fws/pub_key.pem', '/Users/qiyuan/workspace_fangshi/code/xinghuacun_travel/TravelWebSystem/doc/pay/fws/apiclient_cert.p12', '/Users/qiyuan/workspace_fangshi/code/xinghuacun_travel/TravelWebSystem/doc/pay/fws/apiclient_key.pem', '/Users/qiyuan/workspace_fangshi/code/xinghuacun_travel/TravelWebSystem/doc/pay/fws/apiclient_cert.pem', '9aB2cD4eF6gH8jK0mL2nP4qR6sT8vW0x', 'https://fs.towantto.com:10007', '/mobile/order/pay/notify/wechat', 'zY3xW5vU7tS9rQ7pO5nM3lK1jI9hG7fE', 0, '', '1', '2026-05-26 11:50:42', '1', '2026-06-12 17:39:50', '0', '', '1', '1', NULL);
+
+
+
+
+
+【积分配置】
+/business/member/pointsRule              # 积分规则列表(菜单)
+/business/member/pointsRule/form         # 积分规则表单(隐藏)
+/business/member/pointsLog               # 积分流水(菜单)
+/business/member/levelFeature            # 等级权益(菜单)
+/business/mall/rewardLevelConfig         # 商城等级奖励/消费送积分(asyncRouter 兜底)
+
+【分销返佣 · 费率与提现】
+/business/commission/withdraw            # 佣金提现
+/business/commission/rateRule            # 关系链费率(二/三级)
+/business/commission/rateRule/form       # 关系链费率表单
+/business/commission/globalConfig        # 分销全局开关/冷静期/提现门槛
+/business/commission/shareRateRule       # 分享直推费率
+/business/commission/shareRateRule/form  # 分享直推费率表单
+/business/commission/distributorLevel    # 分销员等级
+/business/commission/distributorLevel/form
+
+【分销返佣 · 活动运营】
+/business/marketing/activity             # 分享返佣活动
+/business/marketing/rule                 # 返佣奖励规则
+/business/marketing/posterTemplate       # 返佣海报
+/business/marketing/scope                # 投放范围

+ 57 - 0
docker/.env.example

@@ -0,0 +1,57 @@
+# 复制为 .env 后按现网 MySQL / Redis 填写:cp .env.example .env
+# 本 compose 仅启动后台 JAR,不启动 MySQL、Redis 容器
+
+COMPOSE_PROJECT_NAME=xhc-travel-backend
+
+# Harbor 镜像 tag(本地 build 打此标签;推送见 ./start.sh push;拉取用 ./start.sh up --pull)
+DOCKER_IMAGE=harbor.sxidc.com/ywzx/fenyangwenlv:v1.0.0
+
+# 现网 MySQL(示例与 application-dev 对齐,请改成你的地址)
+MYSQL_HOST=10.8.8.9
+MYSQL_PORT=3306
+MYSQL_DATABASE=xinghc_travel
+MYSQL_USER=xinghc_travel
+MYSQL_PASSWORD=请填写现网密码
+
+# 现网 Redis
+# - Redis 在宿主机本机:REDIS_HOST=host.docker.internal
+# - Redis 在远程机:REDIS_HOST=实际 IP
+REDIS_HOST=host.docker.internal
+REDIS_PORT=6379
+REDIS_PASSWORD=
+REDIS_DB=1
+
+# 后台 Spring Boot
+SERVER_PORT=8086
+SPRING_PROFILES_ACTIVE=docker
+# 文件/日志根目录(容器内默认 /serverdata/,与 pro 环境一致)
+# JEESHARP_FILE_BASE_PATH=/serverdata/
+JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseZGC
+
+# ---------- 外部配置文件(可选)----------
+# 宿主机配置目录,挂载到容器 /serverdata/config(默认 docker/config)
+# HOST_CONFIG_DIR=./config
+#
+# 方式一(推荐):追加外部目录,仍保留 JAR 内 application-*.yaml
+# SPRING_CONFIG_ADDITIONAL_LOCATION=optional:file:/serverdata/config/
+#
+# 方式二:指定单个或多个配置文件(逗号分隔;optional: 前缀避免缺失时报错)
+# SPRING_CONFIG_LOCATION=optional:file:/serverdata/config/application.yaml
+#
+# 方式三:命令行参数(写入 SPRING_ARGS,传给 java -jar)
+# SPRING_ARGS=--spring.config.location=optional:file:/serverdata/config/application.yaml
+#
+# 本地直启 JAR(非 Docker)示例:
+# java -jar web-starter/target/web-starter-1.0-SNAPSHOT.jar \
+#   --spring.profiles.active=docker \
+#   --spring.config.additional-location=optional:file:./docker/config/
+
+# Maven 工程根(含 pom.xml、web-starter/);部署目录非完整源码时必填
+# 例:PROJECT_ROOT=/opt/fenyanglvyou/TravelWebSystem
+# PROJECT_ROOT=
+
+# 已有 JAR 时跳过 mvn 编译(服务器无源码场景)
+# SKIP_MAVEN_BUILD=1
+
+# 可选:指定已构建 JAR 路径(相对 PROJECT_ROOT,或绝对路径)
+# APP_JAR=web-starter/target/web-starter-1.0-SNAPSHOT.jar

+ 24 - 0
docker/Dockerfile

@@ -0,0 +1,24 @@
+# 运行阶段:使用宿主机 mvn package 产物(见 docker/start.sh build-jar)
+FROM eclipse-temurin:21-jre-jammy
+
+WORKDIR /serverdata
+
+RUN groupadd -r app && useradd -r -g app app \
+    && mkdir -p /serverdata/uploads /serverdata/log /serverdata/ueditor /serverdata/ureport /serverdata/config \
+    && chown -R app:app /serverdata
+
+ARG APP_JAR=web-starter/target/web-starter-1.0-SNAPSHOT.jar
+COPY --chown=app:app ${APP_JAR} /serverdata/app.jar
+COPY --chown=app:app docker/entrypoint.sh /serverdata/entrypoint.sh
+RUN chmod +x /serverdata/entrypoint.sh
+
+USER app
+
+ENV SPRING_PROFILES_ACTIVE=docker \
+    JEESHARP_FILE_BASE_PATH=/serverdata/ \
+    TZ=Asia/Shanghai \
+    JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseZGC"
+
+EXPOSE 8086
+
+ENTRYPOINT ["/serverdata/entrypoint.sh"]

+ 34 - 0
docker/config/application.yaml.example

@@ -0,0 +1,34 @@
+# 外部配置示例:复制为 application.yaml 或 application-docker.yaml 后按需修改
+# 挂载方式见 docker-compose.yml:./config -> /serverdata/config
+#
+# 启用方式(.env 任选其一):
+#   SPRING_CONFIG_ADDITIONAL_LOCATION=optional:file:/serverdata/config/
+#   SPRING_CONFIG_LOCATION=optional:file:/serverdata/config/application.yaml
+#
+# 本地直启 JAR 示例:
+#   java -jar web-starter-1.0-SNAPSHOT.jar \
+#     --spring.config.additional-location=optional:file:./config/
+
+server:
+  port: 8086
+
+spring:
+  datasource:
+    dynamic:
+      primary: master
+      strict: false
+      datasource:
+        master:
+          url: jdbc:mysql://10.8.8.9:3306/xinghc_travel?useUnicode=true&characterEncoding=UTF-8&connectionCollation=utf8mb4_unicode_ci&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&allowPublicKeyRetrieval=true
+          driver-class-name: com.mysql.cj.jdbc.Driver
+          username: xinghc_travel
+          password: 请填写现网密码
+  data:
+    redis:
+      host: 127.0.0.1
+      port: 6379
+      password:
+      database: 1
+
+jeesharp:
+  file-base-path: /serverdata/

+ 45 - 0
docker/docker-compose.yml

@@ -0,0 +1,45 @@
+name: ${COMPOSE_PROJECT_NAME:-xhc-travel-backend}
+
+services:
+  backend:
+    image: ${DOCKER_IMAGE:-harbor.sxidc.com/ywzx/fenyangwenlv:v1.0.0}
+    pull_policy: never
+    build:
+      context: ${BUILD_CONTEXT:-${PROJECT_ROOT:-..}}
+      dockerfile: ${DOCKERFILE_PATH:-docker/Dockerfile}
+      args:
+        APP_JAR: ${APP_JAR:-web-starter/target/web-starter-1.0-SNAPSHOT.jar}
+    container_name: ${COMPOSE_PROJECT_NAME:-xhc-travel-backend}-app
+    restart: unless-stopped
+    extra_hosts:
+      - "host.docker.internal:host-gateway"
+    environment:
+      SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-docker}
+      # 追加外部配置目录(optional: 目录为空时不报错)
+      SPRING_CONFIG_ADDITIONAL_LOCATION: ${SPRING_CONFIG_ADDITIONAL_LOCATION:-optional:file:/serverdata/config/}
+      # 注意:勿在 compose 中写 SPRING_CONFIG_LOCATION= 空值,会覆盖默认 classpath 配置导致数据源丢失
+      # 若需完全指定配置,在 .env 中设置 SPRING_CONFIG_LOCATION 并在下方 environment 自行追加一行
+      JEESHARP_FILE_BASE_PATH: ${JEESHARP_FILE_BASE_PATH:-/serverdata/}
+      SERVER_PORT: ${SERVER_PORT:-8086}
+      MYSQL_HOST: ${MYSQL_HOST}
+      MYSQL_PORT: ${MYSQL_PORT:-3306}
+      MYSQL_DATABASE: ${MYSQL_DATABASE:-xinghc_travel}
+      MYSQL_USER: ${MYSQL_USER}
+      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
+      REDIS_HOST: ${REDIS_HOST}
+      REDIS_PORT: ${REDIS_PORT:-6379}
+      REDIS_PASSWORD: ${REDIS_PASSWORD:-}
+      REDIS_DB: ${REDIS_DB:-1}
+      JAVA_OPTS: ${JAVA_OPTS:--Xms512m -Xmx1024m -XX:+UseZGC}
+      TZ: Asia/Shanghai
+    ports:
+      - "${SERVER_PORT:-8086}:8086"
+    volumes:
+      - backend_uploads:/serverdata/uploads
+      - backend_log:/serverdata/log
+      # 宿主机配置目录 -> 容器 /serverdata/config(见 config/application.yaml.example)
+      - ${HOST_CONFIG_DIR:-./config}:/serverdata/config:ro
+
+volumes:
+  backend_uploads:
+  backend_log:

+ 10 - 0
docker/entrypoint.sh

@@ -0,0 +1,10 @@
+#!/usr/bin/env sh
+set -eu
+
+JAR="/serverdata/app.jar"
+JAVA_OPTS="${JAVA_OPTS:--Xms512m -Xmx1024m -XX:+UseZGC}"
+
+# SPRING_CONFIG_LOCATION / SPRING_CONFIG_ADDITIONAL_LOCATION 由 Spring Boot 自动读取环境变量
+# SPRING_ARGS 追加到 java -jar 之后,例如:
+#   --spring.config.location=optional:file:/serverdata/config/application.yaml
+exec sh -c "exec java ${JAVA_OPTS} -jar ${JAR} ${SPRING_ARGS:-}"

+ 411 - 0
docker/start.sh

@@ -0,0 +1,411 @@
+#!/usr/bin/env bash
+# 在线订餐管理系统后台 Docker Compose 启动脚本(仅后台容器,MySQL/Redis 使用现网)
+# 默认镜像: harbor.sxidc.com/ywzx/fenyangwenlv:v1.0.0
+#
+# 用法:
+#   ./start.sh up             # 本地编译 + 构建镜像 + 启动(输出精简)
+#   ./start.sh up --pull      # 从 Harbor 拉取镜像并启动
+#   ./start.sh up -v          # 显示完整编译/构建日志
+#   ./start.sh pull           # 仅拉取镜像
+#   ./start.sh push           # 本地打包、构建并推送到 Harbor(需 docker login harbor.sxidc.com)
+#   ./start.sh build          # 本地构建镜像(不打 tag 推送)
+#   ./start.sh build-jar      # 仅 Maven 打包
+#   ./start.sh logs           # 查看日志
+#   ./start.sh down           # 停止容器
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+MAVEN_ROOT=""
+BUILD_CONTEXT=""
+DOCKER_DIR=""
+ENV_FILE=""
+COMPOSE_FILE=""
+DOCKERFILE_PATH=""
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+log_info()  { echo -e "${GREEN}[INFO]${NC} $*"; }
+log_warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
+log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
+
+require_cmd() {
+  if ! command -v "$1" >/dev/null 2>&1; then
+    log_error "未找到命令: $1"
+    exit 1
+  fi
+}
+
+load_env() {
+  # shellcheck disable=SC1090
+  set -a
+  source "${ENV_FILE}" 2>/dev/null || true
+  set +a
+}
+
+# 判断目录是否为 Maven 多模块根(含 web-starter 子模块)
+is_maven_project_root() {
+  local dir="$1"
+  [[ -f "${dir}/pom.xml" && -f "${dir}/web-starter/pom.xml" ]]
+}
+
+# 解析 docker 目录、compose 文件与 Maven 工程根(兼容脚本在 docker/ 或部署根目录两种布局)
+init_paths() {
+  if [[ -f "${SCRIPT_DIR}/docker-compose.yml" ]]; then
+    DOCKER_DIR="${SCRIPT_DIR}"
+  elif [[ -f "${SCRIPT_DIR}/docker/docker-compose.yml" ]]; then
+    DOCKER_DIR="${SCRIPT_DIR}/docker"
+  else
+    log_error "未找到 docker-compose.yml(请在 docker/ 目录或部署根目录执行 start.sh)"
+    exit 1
+  fi
+
+  ENV_FILE="${DOCKER_DIR}/.env"
+  COMPOSE_FILE="${DOCKER_DIR}/docker-compose.yml"
+
+  if [[ -f "${DOCKER_DIR}/Dockerfile" ]]; then
+    DOCKERFILE_PATH="${DOCKER_DIR}/Dockerfile"
+  else
+    DOCKERFILE_PATH="${DOCKER_DIR}/docker/Dockerfile"
+  fi
+
+  # 先读 .env,允许 PROJECT_ROOT 显式指定 Maven 根
+  if [[ -f "${ENV_FILE}" ]]; then
+    load_env
+  fi
+
+  local from_env="${PROJECT_ROOT:-}"
+  MAVEN_ROOT=""
+  if [[ -n "${from_env}" ]] && is_maven_project_root "${from_env}"; then
+    MAVEN_ROOT="$(cd "${from_env}" && pwd)"
+  else
+    local candidate
+    for candidate in \
+      "${SCRIPT_DIR}" \
+      "${SCRIPT_DIR}/.." \
+      "${DOCKER_DIR}" \
+      "${DOCKER_DIR}/.." \
+      "${DOCKER_DIR}/../TravelWebSystem" \
+      "${SCRIPT_DIR}/TravelWebSystem"; do
+      if is_maven_project_root "${candidate}"; then
+        MAVEN_ROOT="$(cd "${candidate}" && pwd)"
+        break
+      fi
+    done
+  fi
+
+  BUILD_CONTEXT="${MAVEN_ROOT:-${DOCKER_DIR}}"
+  export MAVEN_ROOT
+  export BUILD_CONTEXT
+  export PROJECT_ROOT="${BUILD_CONTEXT}"
+  export DOCKERFILE_PATH
+}
+
+compose() {
+  if docker compose version >/dev/null 2>&1; then
+    docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" "$@"
+  elif command -v docker-compose >/dev/null 2>&1; then
+    docker-compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" "$@"
+  else
+    log_error "未找到 docker compose / docker-compose,请安装 Docker Compose v2"
+    exit 1
+  fi
+}
+
+default_jar_path() {
+  echo "${APP_JAR:-web-starter/target/web-starter-1.0-SNAPSHOT.jar}"
+}
+
+resolved_jar() {
+  local jar
+  jar="$(default_jar_path)"
+  if [[ "${jar}" = /* ]]; then
+    echo "${jar}"
+  else
+    echo "${BUILD_CONTEXT}/${jar}"
+  fi
+}
+
+ensure_env() {
+  if [[ ! -f "${ENV_FILE}" ]]; then
+    if [[ -f "${SCRIPT_DIR}/.env.example" ]]; then
+      cp "${SCRIPT_DIR}/.env.example" "${ENV_FILE}"
+      log_warn "已生成 ${ENV_FILE},请填写现网 MYSQL_* / REDIS_* 后再启动"
+    else
+      log_error "缺少 ${ENV_FILE} 与 .env.example"
+      exit 1
+    fi
+  fi
+  load_env
+  local missing=0
+  for key in MYSQL_HOST MYSQL_USER MYSQL_PASSWORD REDIS_HOST; do
+    if ! grep -q "^${key}=." "${ENV_FILE}" 2>/dev/null; then
+      log_error ".env 缺少或未填写: ${key}"
+      missing=1
+    fi
+    local val
+    val="$(grep "^${key}=" "${ENV_FILE}" | cut -d= -f2- | tr -d '\r')"
+    if [[ -z "${val}" || "${val}" == "请填写现网密码" ]]; then
+      log_error ".env 中 ${key} 仍为占位值,请改成现网配置"
+      missing=1
+    fi
+  done
+  if [[ "${missing}" -eq 1 ]]; then
+    exit 1
+  fi
+  mkdir -p "${DOCKER_DIR}/config"
+}
+
+ensure_docker() {
+  require_cmd docker
+  if ! docker info >/dev/null 2>&1; then
+    log_error "Docker 未运行,请先启动 Docker Desktop / dockerd"
+    exit 1
+  fi
+}
+
+image_name() {
+  load_env
+  echo "${DOCKER_IMAGE:-harbor.sxidc.com/ywzx/fenyangwenlv:v1.0.0}"
+}
+
+BUILD_LOG=""
+COMPOSE_LOG=""
+
+is_verbose() {
+  [[ "${VERBOSE:-0}" -eq 1 ]]
+}
+
+ensure_jar() {
+  local jar
+  jar="$(resolved_jar)"
+
+  if [[ -f "${jar}" && "${SKIP_MAVEN_BUILD:-0}" == "1" ]]; then
+    log_info "已存在 JAR,跳过 Maven 编译: ${jar}"
+    return 0
+  fi
+
+  if [[ -z "${MAVEN_ROOT}" ]]; then
+    if [[ -f "${jar}" ]]; then
+      log_warn "未找到 Maven 工程根目录,使用已有 JAR: ${jar}"
+      return 0
+    fi
+    log_error "未找到 Maven 工程(缺少 pom.xml 与 web-starter 模块)"
+    log_error "请任选其一:"
+    log_error "  1) 在 .env 设置 PROJECT_ROOT=完整源码目录(含 pom.xml、web-starter/)"
+    log_error "  2) 放置 JAR 到 ${jar} 并设置 SKIP_MAVEN_BUILD=1"
+    log_error "  3) 使用 Harbor 镜像: ./start.sh up --pull"
+    exit 1
+  fi
+
+  require_cmd mvn
+  log_info "Maven 工程根: ${MAVEN_ROOT}"
+
+  if is_verbose; then
+    log_info "Maven 打包 web-starter(跳过测试)..."
+    (cd "${MAVEN_ROOT}" && mvn -B -f pom.xml -pl web-starter -am package -DskipTests)
+  else
+    log_info "正在编译..."
+    : > "${BUILD_LOG}"
+    if ! (cd "${MAVEN_ROOT}" && mvn -B -q -f pom.xml -pl web-starter -am package -DskipTests >>"${BUILD_LOG}" 2>&1); then
+      log_error "编译失败,日志: ${BUILD_LOG}"
+      tail -n 50 "${BUILD_LOG}" >&2
+      exit 1
+    fi
+  fi
+
+  if [[ ! -f "${jar}" ]]; then
+    log_error "未找到构建产物: ${jar}"
+    exit 1
+  fi
+}
+
+compose_up_build() {
+  if is_verbose; then
+    log_info "构建镜像并启动容器..."
+    compose up -d --build
+    return
+  fi
+
+  log_info "正在构建镜像并启动..."
+  : > "${COMPOSE_LOG}"
+  if ! compose up -d --build --quiet-pull >>"${COMPOSE_LOG}" 2>&1; then
+    log_error "构建或启动失败,日志: ${COMPOSE_LOG}"
+    tail -n 50 "${COMPOSE_LOG}" >&2
+    exit 1
+  fi
+}
+
+compose_build() {
+  if is_verbose; then
+    compose build backend
+    return
+  fi
+  : > "${COMPOSE_LOG}"
+  if ! compose build backend >>"${COMPOSE_LOG}" 2>&1; then
+    log_error "镜像构建失败,日志: ${COMPOSE_LOG}"
+    tail -n 50 "${COMPOSE_LOG}" >&2
+    exit 1
+  fi
+}
+
+print_endpoints() {
+  log_info "镜像: $(image_name)"
+  log_info "后台 API: http://localhost:${SERVER_PORT:-8086}"
+  log_info "Swagger:  http://localhost:${SERVER_PORT:-8086}/swagger-ui.html"
+  log_info "MySQL: ${MYSQL_HOST}:${MYSQL_PORT:-3306}/${MYSQL_DATABASE:-xinghc_travel}"
+  log_info "Redis: ${REDIS_HOST}:${REDIS_PORT:-6379} db=${REDIS_DB:-1}"
+  log_info "查看日志: ${SCRIPT_DIR}/start.sh logs"
+}
+
+cmd_pull() {
+  ensure_docker
+  ensure_env
+  log_info "拉取镜像 $(image_name) ..."
+  compose pull backend
+}
+
+cmd_up() {
+  local do_pull=0
+  local do_no_build=0
+  VERBOSE=0
+  for arg in "$@"; do
+    case "${arg}" in
+      --pull) do_pull=1 ;;
+      --no-build) do_no_build=1 ;;
+      -v|--verbose) VERBOSE=1 ;;
+    esac
+  done
+
+  ensure_docker
+  ensure_env
+
+  if [[ "${do_pull}" -eq 1 ]]; then
+    log_info "拉取镜像并启动: $(image_name)"
+    compose pull backend
+    compose up -d
+  elif [[ "${do_no_build}" -eq 1 ]]; then
+    log_info "启动已有镜像: $(image_name)"
+    compose up -d
+  else
+    log_info "构建并启动后台: $(image_name)"
+    ensure_jar
+    compose_up_build
+    log_info "启动完成"
+  fi
+
+  compose ps
+  print_endpoints
+}
+
+cmd_build() {
+  ensure_docker
+  ensure_env
+  VERBOSE="${VERBOSE:-0}"
+  if [[ ! -f "$(resolved_jar)" ]]; then
+    ensure_jar
+  fi
+  log_info "构建镜像: $(image_name)"
+  compose_build
+}
+
+cmd_push() {
+  ensure_docker
+  ensure_env
+  VERBOSE="${VERBOSE:-0}"
+  ensure_jar
+  local img
+  img="$(image_name)"
+  log_info "构建并推送: ${img}"
+  compose_build
+  docker push "${img}"
+  log_info "推送完成: ${img}"
+}
+
+cmd_logs() {
+  ensure_docker
+  ensure_env
+  compose logs -f --tail=200 backend
+}
+
+cmd_down() {
+  ensure_docker
+  ensure_env
+  if [[ "${1:-}" == "-v" ]]; then
+    log_warn "将删除 backend 数据卷(上传文件、日志等)"
+    compose down -v
+  else
+    compose down
+  fi
+}
+
+cmd_ps() {
+  ensure_docker
+  ensure_env
+  compose ps -a
+}
+
+cmd_restart() {
+  ensure_docker
+  ensure_env
+  compose restart backend
+}
+
+usage() {
+  cat <<EOF
+用法: $(basename "$0") <command>
+
+命令:
+  up [--pull] [--no-build] [-v]  构建并启动(--pull 用 Harbor;--no-build 不编译/不构建镜像)
+  pull          仅拉取 Harbor 镜像
+  push          本地打包、构建并 push 到 Harbor(需 docker login)
+  build         仅本地构建镜像
+  build-jar     仅 Maven 打包
+  logs          跟踪 backend 日志
+  ps            查看容器状态
+  restart       重启 backend
+  down          停止容器(保留数据卷)
+  down -v       停止并删除数据卷
+
+环境变量(.env):
+  DOCKER_IMAGE        默认 harbor.sxidc.com/ywzx/fenyangwenlv:v1.0.0
+  PROJECT_ROOT        Maven 工程根(含 pom.xml、web-starter/);未设置则自动探测
+  SKIP_MAVEN_BUILD=1  跳过 mvn,使用已有 JAR
+  SPRING_ARGS             传给 java -jar 的 Spring 参数(如 --spring.config.location=...)
+  HOST_CONFIG_DIR         宿主机配置目录,挂载为 /serverdata/config(默认 ./config)
+
+外部配置: 复制 config/application.yaml.example 为 config/application.yaml 后修改
+EOF
+}
+
+main() {
+  init_paths
+  BUILD_LOG="${DOCKER_DIR}/.build.log"
+  COMPOSE_LOG="${DOCKER_DIR}/.compose.log"
+
+  local cmd="${1:-up}"
+  shift || true
+
+  case "${cmd}" in
+    up)        cmd_up "$@" ;;
+    pull)      cmd_pull ;;
+    push)      cmd_push ;;
+    build)     cmd_build ;;
+    build-jar) VERBOSE=1; ensure_jar ;;
+    logs)      cmd_logs ;;
+    down)      cmd_down "$@" ;;
+    ps)        cmd_ps ;;
+    restart)   cmd_restart ;;
+    -h|--help|help) usage ;;
+    *)
+      log_error "未知命令: ${cmd}"
+      usage
+      exit 1
+      ;;
+  esac
+}
+
+main "$@"

+ 237 - 0
jeesharp-business/DELIVERY_SUMMARY.md

@@ -0,0 +1,237 @@
+# 小程序核心业务交付说明
+
+## 一、本轮交付范围
+
+本轮已完成以下小程序核心业务模块的正式化落地:
+
+- `route` 路线规划
+- `leisure` 休闲玩乐
+- `culture` 文旅活动
+- `explain` 游览讲解
+- `heritage` 非遗文化
+- `study` 研学团建
+- `member-invoice` 发票申请
+
+交付目标:
+
+- 主数据不再依赖纯前端 mock
+- 小程序列表 / 详情走正式后端
+- 提交预约 / 报名走正式业务单
+- 成功页支持按 `orderId` 回查
+- 全局搜索 `search/global` 可命中核心业务
+- 后台至少具备主数据维护和业务单查询入口
+- 会员可对订单发起发票申请,后台可审核并回填附件
+
+---
+
+## 二、后端交付结果
+
+### 1. 已新增正式业务模块
+
+- `business-route`
+- `business-culture`
+
+### 2. 已扩展现有正式模块
+
+- `business-guide`
+  - 补齐 `explain` / `heritage` 主数据 payload
+  - 补齐 `app_explain_booking_order`
+  - 补齐 `app_heritage_booking_order`
+- `business-study`
+  - 补齐 `StudyProgramPayloadVO`
+  - 补齐 `app_study_booking_order`
+- `business-member`
+  - 补齐 `app_member_invoice_apply`
+  - 补齐发票申请后台审核与移动端申请
+  - 补齐发票申请模拟数据脚本
+
+### 3. 已打通移动端网关接口
+
+以下接口现已走正式业务模块:
+
+- `GET /route/plans`
+- `GET /route/plan/{id}`
+- `POST /route/trip/create`
+- `GET /route/trip/result/{orderId}`
+
+- `GET /culture/activities`
+- `GET /culture/activity/{id}`
+- `POST /culture/activity/enroll`
+- `GET /culture/activity/enroll/result/{id}`
+
+- `GET /explain/spots`
+- `GET /explain/spot/{id}`
+- `POST /explain/booking/create`
+- `GET /explain/booking/result/{orderId}`
+
+- `GET /heritage/culture-products`
+- `GET /heritage/culture-product/{id}`
+- `GET /heritage/projects`
+- `GET /heritage/project/{id}`
+- `POST /heritage/booking/create`
+- `GET /heritage/booking/result/{orderId}`
+
+- `GET /study/programs`
+- `GET /study/program/{id}`
+- `POST /study/booking/create`
+- `GET /study/booking/result/{orderId}`
+
+- `POST /member/invoiceApply/create`
+- `GET /member/invoiceApply/result/{id}`
+- `GET /member/invoiceApply/order/{orderId}`
+
+### 4. 搜索聚合
+
+`GET /search/global`
+
+当前已切正式数据的业务域:
+
+- `route`
+- `leisure`
+- `study`
+- `explain`
+- `heritage`
+- `culture`
+
+---
+
+## 三、前端交付结果
+
+### 1. 小程序端
+
+以下页面已接入正式提交流程与结果回查:
+
+- `pages/route/detail`
+- `pages/route/success`
+- `pages/activity/detail`
+- `pages/activity/success`
+- `pages/explain/book`
+- `pages/explain/success`
+- `pages/heritage/book`
+- `pages/heritage/success`
+- `pages/study/book`
+- `pages/study/success`
+- `pages/invoice/apply`
+- `pages/invoice/result`
+
+### 2. 管理端
+
+主数据维护页已存在或已补齐:
+
+- 路线:线路 / 站点 / 行程单
+- 活动:活动管理
+- 讲解:讲解点 / 音轨 / 讲解套餐
+- 非遗:文创 / 项目 / 体验套餐
+- 研学:方案 / 套餐
+
+业务单最小查询页已补齐:
+
+- 活动报名单
+- 讲解预约单
+- 非遗预约单
+- 研学预约单
+- 发票申请单
+
+说明:
+
+- 当前业务单后台页是“最小查询版”,可用于联调验证与排障。
+- 还不是完整分页运营台。
+
+---
+
+## 四、数据库与脚本
+
+### 1. 已新增或补充的核心脚本
+
+- `business-route/route_schema.sql`
+- `business-route/route_seed.sql`
+- `business-route/route_menu_permission.sql`
+
+- `business-culture/culture_schema.sql`
+- `business-culture/culture_menu_permission.sql`
+
+- `business-guide/guide_schema.sql`
+- `business-guide/guide_menu_permission.sql`
+
+- `business-study/src/main/resources/sql/study.sql`
+- `business-study/study_menu_permission.sql`
+
+### 2. 已补全 mapper.xml
+
+`jeesharp-business` 下原先空壳的 `mapper.xml` 已全部补齐,当前复扫为 `0` 个空壳文件。
+
+---
+
+## 五、已完成的技术收口
+
+- `MiniAppExperienceSupportService` 已明显瘦身
+- 已删除一批被正式模块替代的 mock 提交 / 结果逻辑
+- 已清理移动端 controller 中无用的 `MiniAppExperienceSupportService` 依赖
+- 已补齐各模块最小标准 CRUD `mapper.xml`
+
+---
+
+## 六、当前已知状态
+
+当前可以认为本轮核心链路已全部正式闭环:
+
+- `route`
+- `leisure`
+- `culture`
+- `explain`
+- `heritage`
+- `study`
+
+---
+
+## 七、剩余可选优化项
+
+以下内容不是当前交付阻塞项,但值得作为下一阶段优化:
+
+### 1. 后台业务单分页管理台
+
+目前 `culture/explain/heritage/study` 业务单后台页仍以最小查询版为主。
+
+建议补齐:
+
+- `list`
+- `info`
+- 标准 `pro-table`
+- 权限按钮
+
+### 2. 统一交易域深度联动
+
+建议继续推进:
+
+- `trade_order_id` 真实挂单
+- 支付状态联动
+- 取消 / 退款 / 过期
+- 状态流转日志
+
+### 3. Support Service 继续瘦身
+
+`MiniAppExperienceSupportService` 仍保留少量搜索托底与演示初始化逻辑。
+
+建议最终收口为:
+
+- 只保留确实尚未正式化的辅助能力
+- 或拆分为单独的 `demo support` 组件
+
+### 4. 编译与质量门禁
+
+建议单独推进:
+
+- Maven 全量编译验证
+- 管理端 `vue-tsc` / 构建验证
+- 联调冒烟脚本
+
+---
+
+## 八、推荐下一步
+
+如果进入下一阶段,推荐顺序:
+
+1. 业务单后台分页管理台完善
+2. 统一交易域联动
+3. `MiniAppExperienceSupportService` 终极瘦身
+4. 编译与联调门禁建设

+ 388 - 0
jeesharp-business/HOTEL_SCENIC_DEPLOYMENT.md

@@ -0,0 +1,388 @@
+# 酒店与景区模块联调说明
+
+## 范围
+
+本文档覆盖当前仓库内已经落地的两个业务模块:
+
+- `business-hotel`
+- `business-scenic`
+
+适用场景:
+
+- 数据库初始化
+- 菜单权限导入
+- 后台管理端联调
+- 小程序接口联调
+- 微信支付联调前置检查
+
+当前结论:
+
+- 两个模块都已经接入 `jeesharp-business` 聚合工程。
+- 后台 controller、application、service、mapper、SQL 脚本都已落地。
+- 小程序端接口已通过 `business-mobile-gateway` 挂出。
+- 酒店与景区支付回调入口、统一回调适配层、业务确认支付适配器都已落地。
+- “我的订单”聚合接口已纳入酒店与景区订单。
+
+## 一、酒店模块
+
+目录:
+
+- `jeesharp-business/business-hotel`
+
+推荐 SQL 顺序:
+
+1. `01_schema.sql`
+2. `02_seed.sql`
+3. `03_menu_permission.sql`
+4. `04_cleanup.sql` 可选回滚
+
+### 1. 后台能力
+
+当前后台已具备以下对象管理能力:
+
+- 酒店管理
+- 房型管理
+- 预订单管理
+
+菜单路径:
+
+- `/business/hotel/stay`
+- `/business/hotel/room-type`
+- `/business/hotel/booking`
+
+对应后台接口子路径:
+
+- `GET /manager/business/hotel/stay/v1/info`
+- `POST /manager/business/hotel/stay/v1/list`
+- `POST /manager/business/hotel/stay/v1/save`
+- `GET /manager/business/hotel/stay/v1/delete`
+- `GET /manager/business/hotel/roomType/v1/info`
+- `POST /manager/business/hotel/roomType/v1/list`
+- `POST /manager/business/hotel/roomType/v1/save`
+- `GET /manager/business/hotel/roomType/v1/delete`
+- `GET /manager/business/hotel/booking/v1/info`
+- `POST /manager/business/hotel/booking/v1/list`
+- `POST /manager/business/hotel/booking/v1/save`
+- `GET /manager/business/hotel/booking/v1/delete`
+
+说明:
+
+- 菜单路由仍然是 `/business/hotel/room-type`,但后台 controller 路径是 `roomType/v1`,这是当前代码中的实际实现。
+
+### 2. 小程序能力
+
+当前移动端接口挂载在:
+
+- `${jeesharp.web.mobilePath}/hotel`
+
+主要接口:
+
+- `GET /mobile/hotel/stays`
+- `GET /mobile/hotel/stay/{id}`
+- `POST /mobile/hotel/booking/create`
+- `POST /mobile/hotel/booking/pay/prepare`
+- `GET /mobile/hotel/booking/pay/status/{orderId}`
+- `POST /mobile/hotel/booking/pay/mock-success`
+- `POST /mobile/hotel/booking/cancel`
+- `GET /mobile/hotel/booking/result/{orderId}`
+
+当前已经实现的业务行为:
+
+- 酒店列表与详情查询
+- 房型随酒店详情一起输出
+- 创建酒店业务预约单
+- 根据入住/离店日期、房型、间数计算支付金额
+- 生成支付业务字段:`tradeOrderId / payOrderId / attach / amountFen`
+- 查询支付状态
+- 模拟支付成功
+- 微信支付回调后确认支付
+- 取消未完成预订
+- 成功页按 `orderId` 回查结果
+
+### 3. 当前支付接入状态
+
+当前不是纯占位状态,已经具备以下能力:
+
+- `HotelBookingOrderServiceImpl#preparePay(...)` 会生成完整业务支付参数
+- `HotelMobileController#payPrepare(...)` 通过 `WechatJsapiPayOrderPrepayService` 创建财务支付单(`biz_finance_pay_order`)、调用 `WechatMiniPayService.createPrepay(...)` 并回填调起参数
+- 如果会员已登录且存在 `memberId + appId + openid` 绑定,会把 `payer.openid` 一起带上
+- 支付回调:`TravelWechatPayNotifyMobileController`(`/mobile/pay/notify/wechat`,attach 路由)
+- 回调适配层已经接到 `WechatPayNotifyAdapterService`
+- 酒店业务确认支付适配器已经落地在 `HotelWechatPayCallbackAdapter`
+
+## 二、景区门票模块
+
+目录:
+
+- `jeesharp-business/business-scenic`
+
+推荐 SQL 顺序:
+
+1. `01_schema.sql`
+2. `02_seed.sql`
+3. `03_menu_permission.sql`
+4. `04_cleanup.sql` 可选回滚
+
+### 1. 后台能力
+
+当前后台已具备以下对象管理能力:
+
+- 景区场馆管理
+- 景区预约单管理
+
+菜单路径:
+
+- `/business/scenic/venue`
+- `/business/scenic/booking`
+
+对应后台接口子路径:
+
+- `GET /manager/business/scenic/venue/v1/info`
+- `POST /manager/business/scenic/venue/v1/list`
+- `POST /manager/business/scenic/venue/v1/save`
+- `GET /manager/business/scenic/venue/v1/delete`
+- `GET /manager/business/scenic/booking/v1/info`
+- `POST /manager/business/scenic/booking/v1/list`
+- `POST /manager/business/scenic/booking/v1/save`
+- `GET /manager/business/scenic/booking/v1/delete`
+
+### 2. 小程序能力
+
+当前移动端接口挂载在:
+
+- `${jeesharp.web.mobilePath}/ticket/booking`
+
+主要接口:
+
+- `GET /mobile/ticket/booking/venues`
+- `GET /mobile/ticket/booking/venue/{id}`
+- `POST /mobile/ticket/booking/create`
+- `POST /mobile/ticket/booking/pay/prepare`
+- `GET /mobile/ticket/booking/pay/status/{orderId}`
+- `POST /mobile/ticket/booking/pay/mock-success`
+- `POST /mobile/ticket/booking/cancel`
+- `GET /mobile/ticket/booking/result/{orderId}`
+
+当前已经实现的业务行为:
+
+- 场馆列表与详情查询
+- 免费预约:创建后进入 `pending_use`
+- 付费预约:创建后进入 `pending_pay`
+- 保存票档人数、出行人、金额明细
+- 生成支付业务字段:`tradeOrderId / payOrderId / attach / amountFen`
+- 查询支付状态
+- 模拟支付成功
+- 微信支付回调后确认支付
+- 取消未完成预约
+- 成功页按 `orderId` 回查结果
+
+### 3. 当前支付接入状态
+
+当前景区支付链路也已经超出“仅占位”阶段:
+
+- `ScenicBookingOrderServiceImpl#preparePay(...)` 已生成支付业务参数
+- `ScenicMobileController#preparePay(...)` 通过 `WechatJsapiPayOrderPrepayService` 创建财务支付单、调用 `WechatMiniPayService.createPrepay(...)` 并回填调起参数
+- 支付回调:`TravelWechatPayNotifyMobileController`(`/mobile/pay/notify/wechat`,attach 路由)
+- 统一回调适配层已经接入 `WechatPayNotifyAdapterService`
+- 景区业务确认支付适配器已经落地在 `ScenicWechatPayCallbackAdapter`
+
+## 三、后端挂载点
+
+两个模块当前已经挂入以下工程:
+
+- `jeesharp-business/pom.xml`
+- `jeesharp-business/business-mobile-gateway/pom.xml`
+- `web-starter/pom.xml`
+
+因此正常启动 `web-starter` 后,会同时暴露:
+
+- 后台管理接口
+- 小程序业务接口
+- 微信支付回调接口
+
+支付相关公共组件当前在 `business-mobile-gateway` 内统一维护:
+
+- `WechatJsapiPayOrderPrepayService`(支付单 + 统一下单 + 回填 VO)
+- `WechatPayNotifyAdapterService`
+- `HotelWechatPayCallbackAdapter`
+- `ScenicWechatPayCallbackAdapter`
+- `MallWechatPayCallbackAdapter`
+- `WECHAT_PAY_NOTIFY_SAMPLE.yml`
+
+财务侧支付单与支付流水在 `business-finance`:`PayOrderApplicationService`、`PayRecordApplicationService`(回调先更新支付单再落支付记录)。
+
+## 四、订单聚合现状
+
+当前“我的订单”聚合已经不是待办,已接入移动端订单接口:
+
+- `GET /mobile/order/list`
+- `GET /mobile/order/{id}`
+
+当前会聚合三类订单:
+
+- 商城订单
+- 酒店预订单
+- 景区预约单
+
+说明:
+
+- 这里是接口聚合,未接统一订单中心。
+- 酒店、景区、**商城订单**预支付均走 `WechatJsapiPayOrderPrepayService`(财务支付单 + 微信统一下单);酒店与景区仍各自维护业务单状态;商城订单支付状态见 `mall_order.status`。
+
+## 五、联调前检查
+
+### 1. 菜单父级
+
+两个模块的菜单 SQL 默认依赖:
+
+- `sys_menu.parent_id = biz_root`
+
+如果现网父菜单不是 `biz_root`,执行前需要先修改:
+
+- `03_menu_permission.sql`
+- 对应总菜单脚本
+
+### 2. 角色授权
+
+当前菜单 SQL 只负责:
+
+- 菜单
+- 按钮权限
+
+不会自动给真实角色写入授权。
+
+仍需:
+
+- 手动执行 `sys_role_menu` 授权
+- 或在后台角色管理中勾选
+
+### 3. 静态资源
+
+演示数据默认引用部分静态图片,如:
+
+- `/static/images/home-hero-default.png`(原 `home-hero-banner.png` / `home-hero-ref.png` 已合并)
+
+如果现网不提供这些资源,需要改成真实 URL。
+
+### 4. 微信小程序支付前置
+
+要跑通真实 `createPrepay(...)`,至少要满足:
+
+- `wx.pay.jsapi.enabled=true`
+- `notify-base-url` 配置正确
+- 商户私钥、平台公钥、`api-v3-key` 配置完整
+- 当前会员存在微信小程序登录态
+- `app_member_oauth` 中可查到 `memberId + appId + openid`
+
+配置样例:
+
+- `jeesharp-business/business-mobile-gateway/WECHAT_PAY_NOTIFY_SAMPLE.yml`
+
+### 5. 本地联调说明
+
+本地联调时,如果没有完整微信支付配置,可使用:
+
+- `pay/mock-success`
+- `wx.pay.jsapi.mock-fallback=true`
+- `wx.pay.notify.skip-signature-verify=true`
+
+但测试/生产环境不能依赖这些开发态设置。
+
+## 六、联调顺序建议
+
+### 酒店
+
+1. 执行酒店 SQL
+2. 在后台维护酒店与房型
+3. 调 `GET /mobile/hotel/stays` 与 `GET /mobile/hotel/stay/{id}` 校验数据
+4. 调 `POST /mobile/hotel/booking/create` 创建业务单
+5. 调 `POST /mobile/hotel/booking/pay/prepare` 验证预支付参数
+6. 本地先用 `POST /mobile/hotel/booking/pay/mock-success` 验证支付成功流转
+7. 核对 `biz_finance_pay_order.notify_url` 为 `.../mobile/pay/notify/wechat` 并完成真实支付回调联调
+8. 最后用 `GET /mobile/hotel/booking/result/{orderId}` 与 `GET /mobile/order/{id}` 校验结果
+
+### 景区门票
+
+1. 执行景区 SQL
+2. 在后台维护场馆与票档 JSON
+3. 调 `GET /mobile/ticket/booking/venues` 与 `GET /mobile/ticket/booking/venue/{id}` 校验数据
+4. 免费票验证创建后直接进入 `pending_use`
+5. 付费票验证创建后进入 `pending_pay`
+6. 调 `POST /mobile/ticket/booking/pay/prepare` 验证预支付参数
+7. 本地先用 `POST /mobile/ticket/booking/pay/mock-success` 验证支付成功流转
+8. 核对 `biz_finance_pay_order.notify_url` 为 `.../mobile/pay/notify/wechat` 并完成真实支付回调联调
+9. 最后用 `GET /mobile/ticket/booking/result/{orderId}` 与 `GET /mobile/order/{id}` 校验结果
+
+## 七、当前限制
+
+### 酒店
+
+已完成:
+
+- 业务单创建
+- 金额计算
+- 预支付参数生成
+- JSAPI 下单调用接入点
+- 支付状态查询
+- 模拟支付成功
+- 微信支付回调入口
+- 回调统一适配
+- 成功页回查
+
+未完成或仍需加强:
+
+- 真实环境下的完整支付联调验证
+- 支付回调幂等保护的严格校验
+- 超时关单任务
+- 退款/售后流程
+- 与统一订单中心的正式打通
+
+### 景区门票
+
+已完成:
+
+- 免费预约闭环
+- 付费预约业务单创建
+- 预支付参数生成
+- JSAPI 下单调用接入点
+- 支付状态查询
+- 模拟支付成功
+- 微信支付回调入口
+- 回调统一适配
+- 成功页回查
+
+未完成或仍需加强:
+
+- 真实环境下的完整支付联调验证
+- 支付回调幂等保护的严格校验
+- 超时关单任务
+- 退款/撤销能力
+- 与统一订单中心的正式打通
+
+## 八、前端联调说明
+
+当前仓库内已经明确了后端接口契约,但没有在本仓库直接检索到 `pages/hotel/*` 或 `pages/ticket/*` 页面源码。
+
+因此这里把以下路径视为小程序端联调约定,而不是当前仓库内现成资源:
+
+- `pages/hotel/index`
+- `pages/hotel/detail`
+- `pages/hotel/book`
+- `pages/hotel/success`
+- `pages/ticket/index`
+- `pages/ticket/book`
+- `pages/ticket/success`
+
+如果联调使用的是外部小程序仓库,请以该仓库中的页面与 API 封装为准。
+
+## 九、推荐下一步
+
+按当前进度,优先建议:
+
+1. 用测试商户参数把酒店、景区两条真实微信支付链路各跑通一次
+2. 给酒店、景区回调补更严格的幂等与异常补偿
+3. 增加未支付超时关闭任务
+4. 明确是否要接统一订单中心,而不是继续双轨维护业务单
+5. 若进入运营阶段,再补退款、撤销、售后相关流程
+
+如果只是做业务演示或后台联调,当前版本已经可以支撑酒店与景区的主流程联调。

+ 293 - 0
jeesharp-business/MINIPROGRAM_BUSINESS_CHECKLIST.md

@@ -0,0 +1,293 @@
+# 小程序核心业务联调 Checklist
+
+适用范围:
+
+- `route`
+- `leisure`
+- `culture`
+- `explain`
+- `heritage`
+- `study`
+
+目标:
+
+- 后台主数据可维护
+- 小程序列表/详情可读取
+- 预约/报名可提交
+- 成功页可按 `orderId` 回查
+- 全局搜索 `search/global` 可命中对应业务
+
+---
+
+## 一、数据库准备
+
+按模块执行 SQL。
+
+### 1. 必执行脚本
+
+- `business-route/route_schema.sql`
+- `business-route/route_seed.sql`
+- `business-culture/culture_schema.sql`
+- `business-guide/guide_schema.sql`
+- `business-study/src/main/resources/sql/study.sql`
+- `business-leisure` 对应建表脚本
+- `business-village` 对应建表脚本
+- `business-yearcard` 对应建表脚本
+
+### 2. 重点核查表
+
+- `biz_route_plan`
+- `biz_route_stop`
+- `app_route_trip_order`
+- `biz_culture_activity`
+- `app_culture_enroll_order`
+- `biz_explain_spot`
+- `biz_explain_audio_track`
+- `biz_explain_guide_package`
+- `app_explain_booking_order`
+- `biz_heritage_project`
+- `biz_heritage_experience_package`
+- `biz_heritage_culture_product`
+- `app_heritage_booking_order`
+- `biz_study_program`
+- `biz_study_package`
+- `app_study_booking_order`
+- `biz_leisure_venue`
+- `biz_leisure_package`
+- `app_leisure_booking_order`
+
+### 3. 数据检查
+
+- 所有主表 `del_flag='0'`
+- 所有主数据 `status=1`
+- 所有关联附表已挂到正确主表 ID
+- `sort_no` / `sort_order` 有默认值
+
+---
+
+## 二、菜单权限准备
+
+执行或导入以下菜单脚本:
+
+- `business-route/route_menu_permission.sql`
+- `business-culture/culture_menu_permission.sql`
+- `business-guide/guide_menu_permission.sql`
+- `business-leisure/leisure_menu_permission.sql`
+- `business-study/study_menu_permission.sql`
+- `business-village/village_menu_permission.sql`
+- `business-yearcard/yearcard_menu_permission.sql`
+
+核查:
+
+- 菜单父节点 `biz_root` 已存在
+- 角色已绑定对应菜单
+- 管理端路由能正常打开
+
+重点页面:
+
+- `/business/route/plan`
+- `/business/route/stop`
+- `/business/route/tripOrder`
+- `/business/culture/activity`
+- `/business/culture/enroll`
+- `/business/explain/spot`
+- `/business/explain/audioTrack`
+- `/business/explain/guidePackage`
+- `/business/explain/booking`
+- `/business/heritage/cultureProduct`
+- `/business/heritage/project`
+- `/business/heritage/experiencePackage`
+- `/business/heritage/booking`
+- `/business/study/program`
+- `/business/study/package`
+- `/business/study/booking`
+
+---
+
+## 三、启动工程检查
+
+确认以下模块已被聚合:
+
+- `jeesharp-business/pom.xml`
+- `business-mobile-gateway/pom.xml`
+- `web-starter/pom.xml`
+
+确认以下 controller 已能被启动工程扫描到:
+
+- `RoutePlanController`
+- `RouteStopController`
+- `RouteTripOrderController`
+- `CultureActivityController`
+- `CultureEnrollOrderController`
+- `ExplainSpotController`
+- `ExplainBookingOrderController`
+- `HeritageProjectController`
+- `HeritageCultureProductController`
+- `HeritageBookingOrderController`
+- `StudyProgramController`
+- `StudyPackageController`
+- `StudyBookingOrderController`
+
+---
+
+## 四、后台建数顺序
+
+### 1. 路线规划
+
+1. 建线路
+2. 建站点
+3. 校验小程序 `/route/plans`、`/route/plan/{id}`
+
+### 2. 文旅活动
+
+1. 建活动主数据
+2. 状态设为 `published`
+3. 剩余名额 `quota_left > 0`
+4. 校验小程序 `/culture/activities`、`/culture/activity/{id}`
+
+### 3. 游览讲解
+
+1. 建讲解点
+2. 建音轨
+3. 建讲解套餐
+4. 校验小程序 `/explain/spots`、`/explain/spot/{id}`
+
+### 4. 非遗文化
+
+1. 建文创
+2. 建非遗项目
+3. 建体验套餐
+4. 如有需要,给项目挂 `relatedCultureProductId`
+5. 校验小程序 `/heritage/projects`、`/heritage/project/{id}`
+
+### 5. 研学团建
+
+1. 建方案
+2. 建套餐
+3. 校验小程序 `/study/programs`、`/study/program/{id}`
+
+---
+
+## 五、小程序链路联调
+
+### 1. route
+
+- 打开线路列表
+- 打开详情
+- 点击“加入我的行程”
+- 校验:
+  - `POST /route/trip/create`
+  - `GET /route/trip/result/{orderId}`
+  - 成功页展示正常
+
+### 2. culture
+
+- 打开活动列表
+- 打开活动详情
+- 填写姓名、手机号、人数
+- 提交报名
+- 校验:
+  - `POST /culture/activity/enroll`
+  - `GET /culture/activity/enroll/result/{id}`
+  - `app_culture_enroll_order` 已落单
+
+### 3. explain
+
+- 打开讲解点列表
+- 打开讲解详情
+- 选择讲解套餐
+- 填写手机号
+- 提交预约
+- 校验:
+  - `POST /explain/booking/create`
+  - `GET /explain/booking/result/{orderId}`
+  - `app_explain_booking_order` 已落单
+
+### 4. heritage
+
+- 打开非遗项目详情
+- 选择体验套餐
+- 填写手机号
+- 提交预约
+- 校验:
+  - `POST /heritage/booking/create`
+  - `GET /heritage/booking/result/{orderId}`
+  - `app_heritage_booking_order` 已落单
+
+### 5. study
+
+- 打开研学方案详情
+- 选择套餐
+- 填写手机号
+- 提交预约
+- 校验:
+  - `POST /study/booking/create`
+  - `GET /study/booking/result/{orderId}`
+  - `app_study_booking_order` 已落单
+
+---
+
+## 六、后台结果回查
+
+提交成功后,用成功页带回的 `orderId` / `id` 在后台验证:
+
+- 活动报名单:`/business/culture/enroll`
+- 讲解预约单:`/business/explain/booking`
+- 非遗预约单:`/business/heritage/booking`
+- 研学预约单:`/business/study/booking`
+- 路线行程单:`/business/route/tripOrder`
+
+核查字段:
+
+- `orderId`
+- `verifyCode`
+- `statusLabel` / 状态字段
+- `resultPayloadJson`
+- `createDate`
+
+---
+
+## 七、搜索联调
+
+接口:
+
+- `GET /search/global?keyword=...`
+
+当前应能命中:
+
+- `route`
+- `leisure`
+- `study`
+- `explain`
+- `heritage`
+- `culture`
+
+核查点:
+
+- `bizType`
+- `title`
+- `subtitle`
+- `cover`
+- `route`
+- `extra`
+
+---
+
+## 八、验收口径
+
+满足以下条件即可认为本轮联调完成:
+
+- 主数据后台可维护
+- 小程序列表/详情均可读取正式数据
+- 预约/报名提交均可落正式业务单
+- 成功页支持 `orderId` 回查
+- 后台能按单据 ID 查看结果
+- `search/global` 可命中六类业务
+
+---
+
+## 九、已知后续项
+
+- 预约/报名后台页目前是“最小查询版”,还不是完整分页运营台
+- `MiniAppExperienceSupportService` 仍保留少量搜索/演示托底逻辑,可继续瘦身
+- Maven / TS 的全仓编译环境问题仍需单独处理

+ 31 - 0
jeesharp-business/business-ai/business-ai-application/pom.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.xinghuacuntravel</groupId>
+        <artifactId>business-ai</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>business-ai-application</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.xinghuacuntravel</groupId>
+            <artifactId>business-ai-bean</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.xinghuacuntravel</groupId>
+            <artifactId>business-ai-service</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.xinghuacuntravel</groupId>
+            <artifactId>business-member-service</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 160 - 0
jeesharp-business/business-ai/business-ai-application/src/main/java/com/jeesharp/business/modules/ai/application/AiChatFeedbackApplicationService.java

@@ -0,0 +1,160 @@
+package com.jeesharp.business.modules.ai.application;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.jeesharp.business.modules.ai.convert.AiChatFeedbackConvert;
+import com.jeesharp.business.modules.ai.dto.AiChatFeedbackSaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiChatFeedback;
+import com.jeesharp.business.modules.ai.service.AiChatFeedbackService;
+import com.jeesharp.business.modules.ai.service.AiChatMessageService;
+import com.jeesharp.business.modules.ai.service.AiChatSessionService;
+import com.jeesharp.business.modules.ai.vo.AiChatFeedbackInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiChatFeedbackListVO;
+import com.jeesharp.business.modules.ai.vo.AiChatFeedbackQueryVO;
+import com.jeesharp.business.modules.member.service.MemberService;
+import com.jeesharp.framework.pojo.Page;
+import com.jeesharp.framework.pojo.query.PageRespVO;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * AI 客服反馈应用服务:后台反馈查询、维护及会话/消息/会员展示信息编排。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Service
+@AllArgsConstructor
+@Transactional(readOnly = true)
+public class AiChatFeedbackApplicationService {
+
+    private final AiChatFeedbackService aiChatFeedbackService;
+    private final AiChatSessionService aiChatSessionService;
+    private final AiChatMessageService aiChatMessageService;
+    private final MemberService memberService;
+
+    public AiChatFeedbackInfoVO get(String id) {
+        AiChatFeedback entity = aiChatFeedbackService.get(id);
+        if (entity == null) {
+            return null;
+        }
+        AiChatFeedbackInfoVO vo = AiChatFeedbackConvert.INSTANCE.toInfoVO(entity);
+        enrichInfoDisplay(vo);
+        return vo;
+    }
+
+    public PageRespVO<AiChatFeedbackListVO> findPage(Page<AiChatFeedback> page, AiChatFeedbackQueryVO query) {
+        AiChatFeedbackQueryVO prepared = prepareQuery(query);
+        if (prepared == null) {
+            return emptyPage(page);
+        }
+        Page<AiChatFeedback> result = aiChatFeedbackService.findPage(page, prepared);
+        PageRespVO<AiChatFeedbackListVO> pageVo = AiChatFeedbackConvert.INSTANCE.toPageRespVO(result);
+        enrichListDisplay(pageVo.getRecords());
+        return pageVo;
+    }
+
+    @Transactional(readOnly = false)
+    public void save(AiChatFeedbackSaveDTO dto) {
+        AiChatFeedback entity = AiChatFeedbackConvert.INSTANCE.toEntity(dto);
+        if (StrUtil.isBlank(dto.getId())) {
+            aiChatFeedbackService.createFeedback(entity);
+            return;
+        }
+        aiChatFeedbackService.updateFeedback(entity);
+    }
+
+    @Transactional(readOnly = false)
+    public void delete(String ids) {
+        aiChatFeedbackService.batchDelete(StrUtil.splitTrim(ids, ','));
+    }
+
+    /**
+     * 管理端名称类筛选:会话标题/编码、消息内容、会员关键词 → 主键 IN。
+     */
+    private AiChatFeedbackQueryVO prepareQuery(AiChatFeedbackQueryVO query) {
+        AiChatFeedbackQueryVO q = query != null ? query : new AiChatFeedbackQueryVO();
+        if (StrUtil.isNotBlank(q.getMemberKeyword())) {
+            List<String> memberIds = memberService.findIdsByKeyword(q.getMemberKeyword().trim());
+            q.setMemberKeyword(null);
+            if (CollUtil.isEmpty(memberIds)) {
+                return null;
+            }
+            q.setMemberIdIn(memberIds);
+        }
+        if (StrUtil.isNotBlank(q.getSessionTitle())) {
+            List<String> sessionIds = aiChatSessionService.findIdsByTitleOrCodeLike(q.getSessionTitle().trim());
+            q.setSessionTitle(null);
+            if (CollUtil.isEmpty(sessionIds)) {
+                return null;
+            }
+            q.setSessionIdIn(sessionIds);
+        }
+        if (StrUtil.isNotBlank(q.getMessageContent())) {
+            List<String> messageIds = aiChatMessageService.findIdsByContentLike(q.getMessageContent().trim());
+            q.setMessageContent(null);
+            if (CollUtil.isEmpty(messageIds)) {
+                return null;
+            }
+            q.setMessageIdIn(messageIds);
+        }
+        return q;
+    }
+
+    private void enrichListDisplay(List<AiChatFeedbackListVO> rows) {
+        if (CollUtil.isEmpty(rows)) {
+            return;
+        }
+        memberService.enrichMemberDisplay(rows);
+        List<String> sessionIds = rows.stream()
+            .map(AiChatFeedbackListVO::getSessionId)
+            .filter(StrUtil::isNotBlank)
+            .distinct()
+            .toList();
+        List<String> messageIds = rows.stream()
+            .map(AiChatFeedbackListVO::getMessageId)
+            .filter(StrUtil::isNotBlank)
+            .distinct()
+            .toList();
+        Map<String, String> sessionLabelMap = aiChatSessionService.mapSessionLabelByIds(sessionIds);
+        Map<String, String> messagePreviewMap = aiChatMessageService.mapMessageContentPreviewByIds(messageIds);
+        for (AiChatFeedbackListVO row : rows) {
+            if (StrUtil.isNotBlank(row.getSessionId())) {
+                String sessionTitle = sessionLabelMap.get(row.getSessionId());
+                row.setSessionTitle(StrUtil.blankToDefault(sessionTitle, row.getSessionId()));
+            }
+            if (StrUtil.isNotBlank(row.getMessageId())) {
+                row.setMessageContent(messagePreviewMap.get(row.getMessageId()));
+            }
+        }
+    }
+
+    private void enrichInfoDisplay(AiChatFeedbackInfoVO vo) {
+        if (vo == null) {
+            return;
+        }
+        memberService.enrichMemberDisplay(vo);
+        if (StrUtil.isNotBlank(vo.getSessionId())) {
+            Map<String, String> sessionMap = aiChatSessionService.mapSessionLabelByIds(List.of(vo.getSessionId()));
+            vo.setSessionTitle(sessionMap.get(vo.getSessionId()));
+        }
+        if (StrUtil.isNotBlank(vo.getMessageId())) {
+            Map<String, String> messageMap = aiChatMessageService.mapMessageContentPreviewByIds(List.of(vo.getMessageId()));
+            vo.setMessageContent(messageMap.get(vo.getMessageId()));
+        }
+    }
+
+    private static PageRespVO<AiChatFeedbackListVO> emptyPage(Page<AiChatFeedback> page) {
+        PageRespVO<AiChatFeedbackListVO> empty = new PageRespVO<>();
+        empty.setRecords(Collections.emptyList());
+        empty.setTotal(0L);
+        empty.setPageNo(page != null ? page.getPageNo() : 1L);
+        empty.setPageSize(page != null ? page.getPageSize() : 10L);
+        return empty;
+    }
+}

+ 136 - 0
jeesharp-business/business-ai/business-ai-application/src/main/java/com/jeesharp/business/modules/ai/application/AiChatMessageApplicationService.java

@@ -0,0 +1,136 @@
+package com.jeesharp.business.modules.ai.application;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.jeesharp.business.modules.ai.convert.AiChatMessageConvert;
+import com.jeesharp.business.modules.ai.dto.AiChatMessageSaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiChatMessage;
+import com.jeesharp.business.modules.ai.service.AiChatMessageService;
+import com.jeesharp.business.modules.ai.service.AiChatSessionService;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageListVO;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageQueryVO;
+import com.jeesharp.business.modules.member.service.MemberService;
+import com.jeesharp.framework.pojo.Page;
+import com.jeesharp.framework.pojo.query.PageRespVO;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * AI 客服消息应用服务:后台消息查询、维护及会话/会员展示信息编排。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Service
+@AllArgsConstructor
+@Transactional(readOnly = true)
+public class AiChatMessageApplicationService {
+
+    private final AiChatMessageService aiChatMessageService;
+    private final AiChatSessionService aiChatSessionService;
+    private final MemberService memberService;
+
+    public AiChatMessageInfoVO get(String id) {
+        AiChatMessage entity = aiChatMessageService.get(id);
+        if (entity == null) {
+            return null;
+        }
+        AiChatMessageInfoVO vo = AiChatMessageConvert.INSTANCE.toInfoVO(entity);
+        enrichInfoDisplay(vo);
+        return vo;
+    }
+
+    public PageRespVO<AiChatMessageListVO> findPage(Page<AiChatMessage> page, AiChatMessageQueryVO query) {
+        AiChatMessageQueryVO prepared = prepareQuery(query);
+        if (prepared == null) {
+            return emptyPage(page);
+        }
+        Page<AiChatMessage> result = aiChatMessageService.findPage(page, prepared);
+        PageRespVO<AiChatMessageListVO> pageVo = AiChatMessageConvert.INSTANCE.toPageRespVO(result);
+        enrichListDisplay(pageVo.getRecords());
+        return pageVo;
+    }
+
+    @Transactional(readOnly = false)
+    public void save(AiChatMessageSaveDTO dto) {
+        AiChatMessage entity = AiChatMessageConvert.INSTANCE.toEntity(dto);
+        if (StrUtil.isBlank(dto.getId())) {
+            aiChatMessageService.createMessage(entity);
+            return;
+        }
+        aiChatMessageService.updateMessage(entity);
+    }
+
+    @Transactional(readOnly = false)
+    public void delete(String ids) {
+        aiChatMessageService.batchDelete(StrUtil.splitTrim(ids, ','));
+    }
+
+    /**
+     * 管理端名称类筛选:会话标题/编码、会员关键词 → 主键 IN。
+     */
+    private AiChatMessageQueryVO prepareQuery(AiChatMessageQueryVO query) {
+        AiChatMessageQueryVO q = query != null ? query : new AiChatMessageQueryVO();
+        if (StrUtil.isNotBlank(q.getMemberKeyword())) {
+            List<String> memberIds = memberService.findIdsByKeyword(q.getMemberKeyword().trim());
+            q.setMemberKeyword(null);
+            if (CollUtil.isEmpty(memberIds)) {
+                return null;
+            }
+            q.setMemberIdIn(memberIds);
+        }
+        if (StrUtil.isNotBlank(q.getSessionTitle())) {
+            List<String> sessionIds = aiChatSessionService.findIdsByTitleOrCodeLike(q.getSessionTitle().trim());
+            q.setSessionTitle(null);
+            if (CollUtil.isEmpty(sessionIds)) {
+                return null;
+            }
+            q.setSessionIdIn(sessionIds);
+        }
+        return q;
+    }
+
+    private void enrichListDisplay(List<AiChatMessageListVO> rows) {
+        if (CollUtil.isEmpty(rows)) {
+            return;
+        }
+        memberService.enrichMemberDisplay(rows);
+        List<String> sessionIds = rows.stream()
+            .map(AiChatMessageListVO::getSessionId)
+            .filter(StrUtil::isNotBlank)
+            .distinct()
+            .toList();
+        Map<String, String> sessionLabelMap = aiChatSessionService.mapSessionLabelByIds(sessionIds);
+        for (AiChatMessageListVO row : rows) {
+            if (StrUtil.isNotBlank(row.getSessionId())) {
+                row.setSessionTitle(sessionLabelMap.get(row.getSessionId()));
+            }
+        }
+    }
+
+    private void enrichInfoDisplay(AiChatMessageInfoVO vo) {
+        if (vo == null) {
+            return;
+        }
+        memberService.enrichMemberDisplay(vo);
+        if (StrUtil.isNotBlank(vo.getSessionId())) {
+            Map<String, String> labelMap = aiChatSessionService.mapSessionLabelByIds(List.of(vo.getSessionId()));
+            vo.setSessionTitle(labelMap.get(vo.getSessionId()));
+        }
+    }
+
+    private static PageRespVO<AiChatMessageListVO> emptyPage(Page<AiChatMessage> page) {
+        PageRespVO<AiChatMessageListVO> empty = new PageRespVO<>();
+        empty.setRecords(Collections.emptyList());
+        empty.setTotal(0L);
+        empty.setPageNo(page != null ? page.getPageNo() : 1L);
+        empty.setPageSize(page != null ? page.getPageSize() : 10L);
+        return empty;
+    }
+}

+ 221 - 0
jeesharp-business/business-ai/business-ai-application/src/main/java/com/jeesharp/business/modules/ai/application/AiChatMobileApplicationService.java

@@ -0,0 +1,221 @@
+package com.jeesharp.business.modules.ai.application;
+
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
+import com.jeesharp.business.modules.ai.dto.AiChatFeedbackDTO;
+import com.jeesharp.business.modules.ai.dto.AiChatMessageDTO;
+import com.jeesharp.business.modules.ai.dto.AiChatSessionCreateDTO;
+import com.jeesharp.business.modules.ai.entity.AiChatFeedback;
+import com.jeesharp.business.modules.ai.entity.AiChatMessage;
+import com.jeesharp.business.modules.ai.entity.AiChatSession;
+import com.jeesharp.business.modules.ai.entity.AiFaq;
+import com.jeesharp.business.modules.ai.service.AiChatFeedbackService;
+import com.jeesharp.business.modules.ai.service.AiChatMessageService;
+import com.jeesharp.business.modules.ai.service.AiChatSessionService;
+import com.jeesharp.business.modules.ai.service.AiFaqService;
+import com.jeesharp.business.modules.common.constants.PlatformTenantConstants;
+import com.jeesharp.business.modules.ai.vo.AiChatAnswerVO;
+import com.jeesharp.business.modules.ai.vo.AiChatHistoryVO;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageListVO;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageQueryVO;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionCreateVO;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+@Service
+@Transactional(readOnly = true)
+public class AiChatMobileApplicationService {
+
+    private static final String DEFAULT_MEMBER_ID = "-";
+    private static final String DEFAULT_SESSION_STATUS = "active";
+    private static final String ROLE_USER = "user";
+    private static final String ROLE_ASSISTANT = "assistant";
+    private static final String MESSAGE_TYPE_TEXT = "text";
+    private static final String MESSAGE_TYPE_FAQ = "faq";
+    private static final String MESSAGE_STATUS_SUCCESS = "success";
+    private static final String DEFAULT_WELCOME = "你好,我是牧童小悠,24小时在线陪你游杏花村。你可以直接问我门票、路线、停车、文创特产等问题。";
+    private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    private final AiChatSessionService aiChatSessionService;
+    private final AiChatMessageService aiChatMessageService;
+    private final AiChatFeedbackService aiChatFeedbackService;
+    private final AiFaqService aiFaqService;
+
+    public AiChatMobileApplicationService(AiChatSessionService aiChatSessionService,
+                                          AiChatMessageService aiChatMessageService,
+                                          AiChatFeedbackService aiChatFeedbackService,
+                                          AiFaqService aiFaqService) {
+        this.aiChatSessionService = aiChatSessionService;
+        this.aiChatMessageService = aiChatMessageService;
+        this.aiChatFeedbackService = aiChatFeedbackService;
+        this.aiFaqService = aiFaqService;
+    }
+
+    @Transactional(readOnly = false)
+    public AiChatSessionCreateVO createSession(AiChatSessionCreateDTO dto) {
+        LocalDateTime now = LocalDateTime.now();
+        AiChatSession session = new AiChatSession();
+        session.setId(IdUtil.getSnowflakeNextIdStr());
+        session.setMemberId(StrUtil.blankToDefault(dto.getMemberId(), DEFAULT_MEMBER_ID));
+        session.setSessionCode("AI" + session.getId());
+        session.setSessionTitle(StrUtil.blankToDefault(dto.getFirstQuestion(), "新会话"));
+        session.setSessionStatus(DEFAULT_SESSION_STATUS);
+        session.setLastMessageTime(now);
+        session.setLastMessagePreview(StrUtil.blankToDefault(dto.getFirstQuestion(), "欢迎使用 AI 客服"));
+        session.setExtJson(buildSessionExt(dto));
+        session.setTenantCode(PlatformTenantConstants.PLATFORM_TENANT_CODE);
+        session.setTenantId(PlatformTenantConstants.PLATFORM_TENANT_ID);
+        aiChatSessionService.createSession(session);
+
+        AiChatMessage welcome = new AiChatMessage();
+        welcome.setId(IdUtil.getSnowflakeNextIdStr());
+        welcome.setSessionId(session.getId());
+        welcome.setMemberId(StrUtil.blankToDefault(dto.getMemberId(), DEFAULT_MEMBER_ID));
+        welcome.setMessageRole(ROLE_ASSISTANT);
+        welcome.setMessageType(MESSAGE_TYPE_TEXT);
+        welcome.setMessageContent(DEFAULT_WELCOME);
+        welcome.setMessageStatus(MESSAGE_STATUS_SUCCESS);
+        welcome.setTenantCode(PlatformTenantConstants.PLATFORM_TENANT_CODE);
+        welcome.setTenantId(PlatformTenantConstants.PLATFORM_TENANT_ID);
+        aiChatMessageService.createMessage(welcome);
+
+        if (StrUtil.isNotBlank(dto.getFirstQuestion())) {
+            AiChatMessage first = new AiChatMessage();
+            first.setId(IdUtil.getSnowflakeNextIdStr());
+            first.setSessionId(session.getId());
+            first.setMemberId(StrUtil.blankToDefault(dto.getMemberId(), DEFAULT_MEMBER_ID));
+            first.setMessageRole(ROLE_USER);
+            first.setMessageType(MESSAGE_TYPE_TEXT);
+            first.setMessageContent(dto.getFirstQuestion());
+            first.setMessageStatus(MESSAGE_STATUS_SUCCESS);
+            aiChatMessageService.createMessage(first);
+        }
+
+        AiChatSessionCreateVO vo = new AiChatSessionCreateVO();
+        vo.setSessionId(session.getId());
+        vo.setWelcomeMessage(DEFAULT_WELCOME);
+        vo.setCreatedAt(now.format(DT_FMT));
+        return vo;
+    }
+
+    @Transactional(readOnly = false)
+    public AiChatAnswerVO sendMessage(AiChatMessageDTO dto) {
+        LocalDateTime now = LocalDateTime.now();
+
+        AiChatMessage userMsg = new AiChatMessage();
+        userMsg.setId(IdUtil.getSnowflakeNextIdStr());
+        userMsg.setSessionId(dto.getSessionId());
+        userMsg.setMemberId(StrUtil.blankToDefault(dto.getMemberId(), DEFAULT_MEMBER_ID));
+        userMsg.setMessageRole(ROLE_USER);
+        userMsg.setMessageType(MESSAGE_TYPE_TEXT);
+        userMsg.setMessageContent(dto.getMessage());
+        userMsg.setMessageStatus(MESSAGE_STATUS_SUCCESS);
+        userMsg.setExtJson(buildMessageExt(dto));
+        userMsg.closeTenantCodeAutoSet();
+        userMsg.setTenantCode(PlatformTenantConstants.PLATFORM_TENANT_CODE);
+        userMsg.setTenantId(PlatformTenantConstants.PLATFORM_TENANT_ID);
+        aiChatMessageService.createMessage(userMsg);
+
+        AiFaq matchedFaq = aiFaqService.matchFaq(dto.getMessage());
+        String answer = matchedFaq == null
+                ? "已收到您的问题,我们会根据 FAQ 与当前场景给出建议。"
+                : matchedFaq.getAnswer();
+
+        AiChatMessage assistantMsg = new AiChatMessage();
+        assistantMsg.setId(IdUtil.getSnowflakeNextIdStr());
+        assistantMsg.setSessionId(dto.getSessionId());
+        assistantMsg.setMemberId(StrUtil.blankToDefault(dto.getMemberId(), DEFAULT_MEMBER_ID));
+        assistantMsg.setMessageRole(ROLE_ASSISTANT);
+        assistantMsg.setMessageType(MESSAGE_TYPE_TEXT);
+        assistantMsg.setMessageContent(answer);
+        assistantMsg.setMessageStatus(MESSAGE_STATUS_SUCCESS);
+        assistantMsg.setExtJson(buildAnswerExt(dto, matchedFaq));
+        aiChatMessageService.createMessage(assistantMsg);
+
+        AiChatSession session = aiChatSessionService.get(dto.getSessionId());
+        if (session != null) {
+            session.setLastMessageTime(now);
+            session.setLastMessagePreview(dto.getMessage());
+            if (StrUtil.isBlank(session.getSessionTitle())) {
+                session.setSessionTitle(StrUtil.sub(dto.getMessage(), 0, 20));
+            }
+            aiChatSessionService.updateSession(session);
+        }
+
+        AiChatAnswerVO vo = new AiChatAnswerVO();
+        vo.setMessageId(assistantMsg.getId());
+        vo.setAnswer(answer);
+        vo.setAnswerType(matchedFaq == null ? MESSAGE_TYPE_TEXT : MESSAGE_TYPE_FAQ);
+        if (matchedFaq != null) {
+            vo.getReferences().add("faq:" + matchedFaq.getId());
+            vo.getReferences().add("faq_category:" + StrUtil.nullToEmpty(matchedFaq.getCategoryId()));
+        }
+        return vo;
+    }
+
+    public AiChatHistoryVO getHistory(String sessionId) {
+        AiChatSession session = aiChatSessionService.get(sessionId);
+        AiChatMessageQueryVO query = new AiChatMessageQueryVO();
+        query.setSessionId(sessionId);
+        List<AiChatMessageListVO> messages = com.jeesharp.business.modules.ai.convert.AiChatMessageConvert.INSTANCE
+                .toListVO(aiChatMessageService.findList(query));
+
+        AiChatHistoryVO vo = new AiChatHistoryVO();
+        vo.setSessionId(sessionId);
+        if (session != null) {
+            vo.setSessionTitle(session.getSessionTitle());
+            vo.setSessionStatus(session.getSessionStatus());
+            vo.setUpdatedAt(session.getUpdateDate() == null ? null : session.getUpdateDate().format(DT_FMT));
+        }
+        messages.sort((a, b) -> {
+            if (a.getCreateDate() == null || b.getCreateDate() == null) {
+                return 0;
+            }
+            return a.getCreateDate().compareTo(b.getCreateDate());
+        });
+        vo.setMessages(messages);
+        return vo;
+    }
+
+    @Transactional(readOnly = false)
+    public void feedback(AiChatFeedbackDTO dto) {
+        AiChatFeedback feedback = new AiChatFeedback();
+        feedback.setId(IdUtil.getSnowflakeNextIdStr());
+        feedback.setSessionId(dto.getSessionId());
+        feedback.setMessageId(dto.getMessageId());
+        feedback.setMemberId(StrUtil.blankToDefault(dto.getMemberId(), DEFAULT_MEMBER_ID));
+        feedback.setFeedbackType(Boolean.TRUE.equals(dto.getHelpful()) ? "helpful" : "unhelpful");
+        feedback.setFeedbackNote(StrUtil.trimToNull(dto.getRemark()));
+        aiChatFeedbackService.createFeedback(feedback);
+    }
+
+    private String buildSessionExt(AiChatSessionCreateDTO dto) {
+        JSONObject ext = new JSONObject();
+        ext.set("scene", dto.getScene());
+        ext.set("bizId", dto.getBizId());
+        return ext.toString();
+    }
+
+    private String buildMessageExt(AiChatMessageDTO dto) {
+        JSONObject ext = new JSONObject();
+        ext.set("contextBizType", dto.getContextBizType());
+        ext.set("contextBizId", dto.getContextBizId());
+        return ext.toString();
+    }
+
+    private String buildAnswerExt(AiChatMessageDTO dto, AiFaq matchedFaq) {
+        JSONObject ext = new JSONObject();
+        ext.set("contextBizType", dto.getContextBizType());
+        ext.set("contextBizId", dto.getContextBizId());
+        if (matchedFaq != null) {
+            ext.set("faqId", matchedFaq.getId());
+            ext.set("faqCategoryId", matchedFaq.getCategoryId());
+        }
+        return ext.toString();
+    }
+}

+ 110 - 0
jeesharp-business/business-ai/business-ai-application/src/main/java/com/jeesharp/business/modules/ai/application/AiChatSessionApplicationService.java

@@ -0,0 +1,110 @@
+package com.jeesharp.business.modules.ai.application;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.jeesharp.business.modules.ai.convert.AiChatSessionConvert;
+import com.jeesharp.business.modules.ai.dto.AiChatSessionSaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiChatSession;
+import com.jeesharp.business.modules.ai.service.AiChatSessionService;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionListVO;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionQueryVO;
+import com.jeesharp.business.modules.member.service.MemberService;
+import com.jeesharp.framework.pojo.Page;
+import com.jeesharp.framework.pojo.query.PageRespVO;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * AI 客服会话应用服务:后台会话查询、维护及会员展示信息编排。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Service
+@AllArgsConstructor
+@Transactional(readOnly = true)
+public class AiChatSessionApplicationService {
+
+    private final AiChatSessionService aiChatSessionService;
+    private final MemberService memberService;
+
+    public AiChatSessionInfoVO get(String id) {
+        AiChatSession entity = aiChatSessionService.get(id);
+        if (entity == null) {
+            return null;
+        }
+        AiChatSessionInfoVO vo = AiChatSessionConvert.INSTANCE.toInfoVO(entity);
+        enrichInfoDisplay(vo);
+        return vo;
+    }
+
+    public PageRespVO<AiChatSessionListVO> findPage(Page<AiChatSession> page, AiChatSessionQueryVO query) {
+        AiChatSessionQueryVO prepared = prepareQuery(query);
+        if (prepared == null) {
+            return emptyPage(page);
+        }
+        Page<AiChatSession> result = aiChatSessionService.findPage(page, prepared);
+        PageRespVO<AiChatSessionListVO> pageVo = AiChatSessionConvert.INSTANCE.toPageRespVO(result);
+        enrichListDisplay(pageVo.getRecords());
+        return pageVo;
+    }
+
+    @Transactional(readOnly = false)
+    public void save(AiChatSessionSaveDTO dto) {
+        AiChatSession entity = AiChatSessionConvert.INSTANCE.toEntity(dto);
+        if (StrUtil.isBlank(dto.getId())) {
+            aiChatSessionService.createSession(entity);
+            return;
+        }
+        aiChatSessionService.updateSession(entity);
+    }
+
+    @Transactional(readOnly = false)
+    public void delete(String ids) {
+        aiChatSessionService.batchDelete(StrUtil.splitTrim(ids, ','));
+    }
+
+    /**
+     * 管理端会员关键词筛选 → memberIdIn。
+     */
+    private AiChatSessionQueryVO prepareQuery(AiChatSessionQueryVO query) {
+        AiChatSessionQueryVO q = query != null ? query : new AiChatSessionQueryVO();
+        if (StrUtil.isNotBlank(q.getMemberKeyword())) {
+            List<String> memberIds = memberService.findIdsByKeyword(q.getMemberKeyword().trim());
+            q.setMemberKeyword(null);
+            if (CollUtil.isEmpty(memberIds)) {
+                return null;
+            }
+            q.setMemberIdIn(memberIds);
+        }
+        return q;
+    }
+
+    private void enrichListDisplay(List<AiChatSessionListVO> rows) {
+        if (CollUtil.isEmpty(rows)) {
+            return;
+        }
+        memberService.enrichMemberDisplay(rows);
+    }
+
+    private void enrichInfoDisplay(AiChatSessionInfoVO vo) {
+        if (vo == null) {
+            return;
+        }
+        memberService.enrichMemberDisplay(vo);
+    }
+
+    private static PageRespVO<AiChatSessionListVO> emptyPage(Page<AiChatSession> page) {
+        PageRespVO<AiChatSessionListVO> empty = new PageRespVO<>();
+        empty.setRecords(Collections.emptyList());
+        empty.setTotal(0L);
+        empty.setPageNo(page != null ? page.getPageNo() : 1L);
+        empty.setPageSize(page != null ? page.getPageSize() : 10L);
+        return empty;
+    }
+}

+ 132 - 0
jeesharp-business/business-ai/business-ai-application/src/main/java/com/jeesharp/business/modules/ai/application/AiFaqApplicationService.java

@@ -0,0 +1,132 @@
+package com.jeesharp.business.modules.ai.application;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.jeesharp.business.modules.ai.convert.AiFaqConvert;
+import com.jeesharp.business.modules.ai.dto.AiFaqSaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiFaq;
+import com.jeesharp.business.modules.ai.entity.AiFaqCategory;
+import com.jeesharp.business.modules.ai.service.AiFaqCategoryService;
+import com.jeesharp.business.modules.ai.service.AiFaqService;
+import com.jeesharp.business.modules.ai.vo.AiFaqInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiFaqListVO;
+import com.jeesharp.business.modules.ai.vo.AiFaqQueryVO;
+import com.jeesharp.framework.pojo.Page;
+import com.jeesharp.framework.pojo.query.PageRespVO;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Service
+@AllArgsConstructor
+@Transactional(readOnly = true)
+public class AiFaqApplicationService {
+
+    private final AiFaqService aiFaqService;
+    private final AiFaqCategoryService aiFaqCategoryService;
+
+    public AiFaqInfoVO get(String id) {
+        return AiFaqConvert.INSTANCE.toInfoVO(aiFaqService.get(id));
+    }
+
+    public PageRespVO<AiFaqListVO> findPage(Page<AiFaq> page, AiFaqQueryVO query) {
+        AiFaqQueryVO prepared = prepareQuery(query);
+        if (prepared == null) {
+            return emptyPage(page);
+        }
+        Page<AiFaq> result = aiFaqService.findPage(page, prepared);
+        PageRespVO<AiFaqListVO> pageVo = AiFaqConvert.INSTANCE.toPageRespVO(result);
+        enrichCategoryDisplay(pageVo.getRecords());
+        return pageVo;
+    }
+
+    @Transactional(readOnly = false)
+    public void save(AiFaqSaveDTO dto) {
+        AiFaq entity = AiFaqConvert.INSTANCE.toEntity(dto);
+        if (StrUtil.isBlank(dto.getId())) {
+            aiFaqService.createFaq(entity);
+            return;
+        }
+        aiFaqService.updateFaq(entity);
+    }
+
+    @Transactional(readOnly = false)
+    public void delete(String ids) {
+        aiFaqService.batchDelete(StrUtil.splitTrim(ids, ','));
+    }
+
+    public List<AiFaqListVO> listForMobile(String categoryId, String keyword) {
+        AiFaqQueryVO query = new AiFaqQueryVO();
+        query.setStatus(1);
+        List<AiFaqListVO> list = AiFaqConvert.INSTANCE.toListVO(aiFaqService.findList(query));
+        enrichCategoryDisplay(list);
+        list.forEach(item -> item.setHot(item.getSortNo() != null && item.getSortNo() <= 10));
+        if (StrUtil.isNotBlank(categoryId)) {
+            list = list.stream()
+                    .filter(item -> StrUtil.equals(categoryId, item.getCategoryId())
+                            || StrUtil.equals(categoryId, item.getCategory()))
+                    .toList();
+        }
+        if (StrUtil.isBlank(keyword)) {
+            return list;
+        }
+        return list.stream()
+                .filter(item -> StrUtil.containsIgnoreCase(item.getQuestion(), keyword)
+                        || StrUtil.containsIgnoreCase(StrUtil.nullToEmpty(item.getKeywords()), keyword))
+                .toList();
+    }
+
+    /**
+     * 管理端分类名称筛选:categoryKeyword → categoryIdIn。
+     */
+    private AiFaqQueryVO prepareQuery(AiFaqQueryVO query) {
+        AiFaqQueryVO q = query != null ? query : new AiFaqQueryVO();
+        if (StrUtil.isNotBlank(q.getCategoryKeyword())) {
+            List<String> categoryIds = aiFaqCategoryService.findIdsByCategoryName(q.getCategoryKeyword().trim());
+            q.setCategoryKeyword(null);
+            if (CollUtil.isEmpty(categoryIds)) {
+                return null;
+            }
+            q.setCategoryId(null);
+            q.setCategoryIdIn(categoryIds);
+        }
+        return q;
+    }
+
+    private static PageRespVO<AiFaqListVO> emptyPage(Page<AiFaq> page) {
+        PageRespVO<AiFaqListVO> empty = new PageRespVO<>();
+        empty.setRecords(Collections.emptyList());
+        empty.setTotal(0L);
+        empty.setPageNo(page != null ? page.getPageNo() : 1L);
+        empty.setPageSize(page != null ? page.getPageSize() : 10L);
+        return empty;
+    }
+
+    private void enrichCategoryDisplay(List<AiFaqListVO> rows) {
+        if (rows == null || rows.isEmpty()) {
+            return;
+        }
+        List<String> categoryIds = rows.stream()
+                .map(AiFaqListVO::getCategoryId)
+                .filter(StrUtil::isNotBlank)
+                .map(String::trim)
+                .distinct()
+                .toList();
+        if (categoryIds.isEmpty()) {
+            return;
+        }
+        Map<String, String> categoryNameMap = aiFaqCategoryService.listByIds(categoryIds).stream()
+                .filter(category -> category != null && StrUtil.isNotBlank(category.getId()))
+                .collect(Collectors.toMap(AiFaqCategory::getId, AiFaqCategory::getCategoryName, (a, b) -> a));
+        rows.forEach(item -> {
+            String categoryId = StrUtil.trim(item.getCategoryId());
+            String categoryName = categoryNameMap.get(categoryId);
+            item.setCategory(StrUtil.isNotBlank(categoryName) ? categoryName : categoryId);
+        });
+    }
+}

+ 47 - 0
jeesharp-business/business-ai/business-ai-application/src/main/java/com/jeesharp/business/modules/ai/application/AiFaqCategoryApplicationService.java

@@ -0,0 +1,47 @@
+package com.jeesharp.business.modules.ai.application;
+
+import cn.hutool.core.util.StrUtil;
+import com.jeesharp.business.modules.ai.convert.AiFaqCategoryConvert;
+import com.jeesharp.business.modules.ai.dto.AiFaqCategorySaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiFaqCategory;
+import com.jeesharp.business.modules.ai.service.AiFaqCategoryService;
+import com.jeesharp.business.modules.ai.vo.AiFaqCategoryInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiFaqCategoryListVO;
+import com.jeesharp.business.modules.ai.vo.AiFaqCategoryQueryVO;
+import com.jeesharp.framework.pojo.Page;
+import com.jeesharp.framework.pojo.query.PageRespVO;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@AllArgsConstructor
+@Transactional(readOnly = true)
+public class AiFaqCategoryApplicationService {
+
+    private final AiFaqCategoryService aiFaqCategoryService;
+
+    public AiFaqCategoryInfoVO get(String id) {
+        return AiFaqCategoryConvert.INSTANCE.toInfoVO(aiFaqCategoryService.get(id));
+    }
+
+    public PageRespVO<AiFaqCategoryListVO> findPage(Page<AiFaqCategory> page, AiFaqCategoryQueryVO query) {
+        Page<AiFaqCategory> result = aiFaqCategoryService.findPage(page, query);
+        return AiFaqCategoryConvert.INSTANCE.toPageRespVO(result);
+    }
+
+    @Transactional(readOnly = false)
+    public void save(AiFaqCategorySaveDTO dto) {
+        AiFaqCategory entity = AiFaqCategoryConvert.INSTANCE.toEntity(dto);
+        if (StrUtil.isBlank(dto.getId())) {
+            aiFaqCategoryService.createCategory(entity);
+            return;
+        }
+        aiFaqCategoryService.updateCategory(entity);
+    }
+
+    @Transactional(readOnly = false)
+    public void delete(String ids) {
+        aiFaqCategoryService.batchDelete(StrUtil.splitTrim(ids, ','));
+    }
+}

+ 30 - 0
jeesharp-business/business-ai/business-ai-bean/pom.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.xinghuacuntravel</groupId>
+        <artifactId>business-ai</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>business-ai-bean</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jeesharp.boot</groupId>
+            <artifactId>jeesharp-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.xinghuacuntravel</groupId>
+            <artifactId>business-common-bean</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.xinghuacuntravel</groupId>
+            <artifactId>business-member-bean</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 16 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/convert/AiChatFeedbackConvert.java

@@ -0,0 +1,16 @@
+package com.jeesharp.business.modules.ai.convert;
+
+import com.jeesharp.business.modules.ai.dto.AiChatFeedbackSaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiChatFeedback;
+import com.jeesharp.business.modules.ai.vo.AiChatFeedbackInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiChatFeedbackListVO;
+import com.jeesharp.business.modules.ai.vo.AiChatFeedbackQueryVO;
+import com.jeesharp.framework.common.pojo.convert.BasePojoConvert;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface AiChatFeedbackConvert extends BasePojoConvert<AiChatFeedback, AiChatFeedbackSaveDTO, AiChatFeedbackInfoVO, AiChatFeedbackListVO, AiChatFeedbackQueryVO> {
+
+    AiChatFeedbackConvert INSTANCE = Mappers.getMapper(AiChatFeedbackConvert.class);
+}

+ 16 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/convert/AiChatMessageConvert.java

@@ -0,0 +1,16 @@
+package com.jeesharp.business.modules.ai.convert;
+
+import com.jeesharp.business.modules.ai.dto.AiChatMessageSaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiChatMessage;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageListVO;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageQueryVO;
+import com.jeesharp.framework.common.pojo.convert.BasePojoConvert;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface AiChatMessageConvert extends BasePojoConvert<AiChatMessage, AiChatMessageSaveDTO, AiChatMessageInfoVO, AiChatMessageListVO, AiChatMessageQueryVO> {
+
+    AiChatMessageConvert INSTANCE = Mappers.getMapper(AiChatMessageConvert.class);
+}

+ 22 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/convert/AiChatSessionConvert.java

@@ -0,0 +1,22 @@
+package com.jeesharp.business.modules.ai.convert;
+
+import com.jeesharp.business.modules.ai.dto.AiChatSessionSaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiChatSession;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionListVO;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionQueryVO;
+import com.jeesharp.framework.common.pojo.convert.BasePojoConvert;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * AI 会话 MapStruct 转换器;由应用层 {@code AiMapStructConfiguration} 注册为 Spring Bean,供构造器注入。
+ *
+ * @author jeesharp
+ * @since 2026-05-12
+ */
+@Mapper
+public interface AiChatSessionConvert extends BasePojoConvert<AiChatSession, AiChatSessionSaveDTO, AiChatSessionInfoVO, AiChatSessionListVO, AiChatSessionQueryVO> {
+    AiChatSessionConvert INSTANCE = Mappers.getMapper(AiChatSessionConvert.class);
+
+}

+ 16 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/convert/AiFaqCategoryConvert.java

@@ -0,0 +1,16 @@
+package com.jeesharp.business.modules.ai.convert;
+
+import com.jeesharp.business.modules.ai.dto.AiFaqCategorySaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiFaqCategory;
+import com.jeesharp.business.modules.ai.vo.AiFaqCategoryInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiFaqCategoryListVO;
+import com.jeesharp.business.modules.ai.vo.AiFaqCategoryQueryVO;
+import com.jeesharp.framework.common.pojo.convert.BasePojoConvert;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface AiFaqCategoryConvert extends BasePojoConvert<AiFaqCategory, AiFaqCategorySaveDTO, AiFaqCategoryInfoVO, AiFaqCategoryListVO, AiFaqCategoryQueryVO> {
+
+    AiFaqCategoryConvert INSTANCE = Mappers.getMapper(AiFaqCategoryConvert.class);
+}

+ 16 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/convert/AiFaqConvert.java

@@ -0,0 +1,16 @@
+package com.jeesharp.business.modules.ai.convert;
+
+import com.jeesharp.business.modules.ai.dto.AiFaqSaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiFaq;
+import com.jeesharp.business.modules.ai.vo.AiFaqInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiFaqListVO;
+import com.jeesharp.business.modules.ai.vo.AiFaqQueryVO;
+import com.jeesharp.framework.common.pojo.convert.BasePojoConvert;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface AiFaqConvert extends BasePojoConvert<AiFaq, AiFaqSaveDTO, AiFaqInfoVO, AiFaqListVO, AiFaqQueryVO> {
+
+    AiFaqConvert INSTANCE = Mappers.getMapper(AiFaqConvert.class);
+}

+ 23 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiChatFeedbackDTO.java

@@ -0,0 +1,23 @@
+package com.jeesharp.business.modules.ai.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Data
+public class AiChatFeedbackDTO {
+
+    @NotBlank(message = "sessionId不能为空")
+    private String sessionId;
+
+    @NotBlank(message = "messageId不能为空")
+    private String messageId;
+
+    @NotNull(message = "helpful不能为空")
+    private Boolean helpful;
+
+    @Size(max = 500, message = "反馈备注不能超过500字")
+    private String remark;
+    private String memberId;
+}

+ 25 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiChatFeedbackSaveDTO.java

@@ -0,0 +1,25 @@
+package com.jeesharp.business.modules.ai.dto;
+
+import com.jeesharp.framework.pojo.dto.BaseSaveDTO;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiChatFeedbackSaveDTO extends BaseSaveDTO {
+
+    @NotBlank(message = "会话ID不能为空")
+    private String sessionId;
+
+    @NotBlank(message = "消息ID不能为空")
+    private String messageId;
+
+    @NotBlank(message = "会员ID不能为空")
+    private String memberId;
+
+    @NotBlank(message = "反馈类型不能为空")
+    private String feedbackType;
+
+    private String feedbackNote;
+}

+ 18 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiChatMessageDTO.java

@@ -0,0 +1,18 @@
+package com.jeesharp.business.modules.ai.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class AiChatMessageDTO {
+
+    @NotBlank(message = "sessionId不能为空")
+    private String sessionId;
+
+    @NotBlank(message = "message不能为空")
+    private String message;
+
+    private String contextBizType;
+    private String contextBizId;
+    private String memberId;
+}

+ 31 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiChatMessageSaveDTO.java

@@ -0,0 +1,31 @@
+package com.jeesharp.business.modules.ai.dto;
+
+import com.jeesharp.framework.pojo.dto.BaseSaveDTO;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiChatMessageSaveDTO extends BaseSaveDTO {
+
+    @NotBlank(message = "会话ID不能为空")
+    private String sessionId;
+
+    @NotBlank(message = "会员ID不能为空")
+    private String memberId;
+
+    @NotBlank(message = "消息角色不能为空")
+    private String messageRole;
+
+    @NotBlank(message = "消息类型不能为空")
+    private String messageType;
+
+    @NotBlank(message = "消息内容不能为空")
+    private String messageContent;
+
+    private String messageStatus;
+    private String traceId;
+    private String quoteMessageId;
+    private String extJson;
+}

+ 12 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiChatSessionCreateDTO.java

@@ -0,0 +1,12 @@
+package com.jeesharp.business.modules.ai.dto;
+
+import lombok.Data;
+
+@Data
+public class AiChatSessionCreateDTO {
+
+    private String scene;
+    private String bizId;
+    private String firstQuestion;
+    private String memberId;
+}

+ 28 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiChatSessionSaveDTO.java

@@ -0,0 +1,28 @@
+package com.jeesharp.business.modules.ai.dto;
+
+import com.jeesharp.framework.pojo.dto.BaseSaveDTO;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiChatSessionSaveDTO extends BaseSaveDTO {
+
+    @NotBlank(message = "会员ID不能为空")
+    private String memberId;
+
+    @NotBlank(message = "会话编码不能为空")
+    private String sessionCode;
+
+    private String sessionTitle;
+    private String sessionStatus;
+    private String handoverStatus;
+    private LocalDateTime handoverTime;
+    private String handlerId;
+    private LocalDateTime lastMessageTime;
+    private String lastMessagePreview;
+    private String extJson;
+}

+ 20 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiFaqCategorySaveDTO.java

@@ -0,0 +1,20 @@
+package com.jeesharp.business.modules.ai.dto;
+
+import com.jeesharp.framework.pojo.dto.BaseSaveDTO;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiFaqCategorySaveDTO extends BaseSaveDTO {
+
+    @NotBlank(message = "分类名称不能为空")
+    private String categoryName;
+
+    @NotNull(message = "状态不能为空")
+    private Integer status;
+
+    private Integer sortNo;
+}

+ 28 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/dto/AiFaqSaveDTO.java

@@ -0,0 +1,28 @@
+package com.jeesharp.business.modules.ai.dto;
+
+import com.jeesharp.framework.pojo.dto.BaseSaveDTO;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiFaqSaveDTO extends BaseSaveDTO {
+
+    @NotBlank(message = "分类ID不能为空")
+    private String categoryId;
+
+    @NotBlank(message = "问题不能为空")
+    private String question;
+
+    @NotBlank(message = "答案不能为空")
+    private String answer;
+
+    private String keywords;
+
+    @NotNull(message = "状态不能为空")
+    private Integer status;
+
+    private Integer sortNo;
+}

+ 21 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/entity/AiChatFeedback.java

@@ -0,0 +1,21 @@
+package com.jeesharp.business.modules.ai.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeesharp.framework.mybatis.core.dataobject.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * AI 回答反馈。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("app_ai_chat_feedback")
+public class AiChatFeedback extends BaseEntity<AiChatFeedback> {
+
+    private String sessionId;
+    private String messageId;
+    private String memberId;
+    private String feedbackType;
+    private String feedbackNote;
+}

+ 25 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/entity/AiChatMessage.java

@@ -0,0 +1,25 @@
+package com.jeesharp.business.modules.ai.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeesharp.framework.mybatis.core.dataobject.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * AI 会话消息。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("app_ai_chat_message")
+public class AiChatMessage extends BaseEntity<AiChatMessage> {
+
+    private String sessionId;
+    private String memberId;
+    private String messageRole;
+    private String messageType;
+    private String messageContent;
+    private String messageStatus;
+    private String traceId;
+    private String quoteMessageId;
+    private String extJson;
+}

+ 28 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/entity/AiChatSession.java

@@ -0,0 +1,28 @@
+package com.jeesharp.business.modules.ai.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeesharp.framework.mybatis.core.dataobject.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * AI 会话主表。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("app_ai_chat_session")
+public class AiChatSession extends BaseEntity<AiChatSession> {
+
+    private String memberId;
+    private String sessionCode;
+    private String sessionTitle;
+    private String sessionStatus;
+    private String handoverStatus;
+    private LocalDateTime handoverTime;
+    private String handlerId;
+    private LocalDateTime lastMessageTime;
+    private String lastMessagePreview;
+    private String extJson;
+}

+ 22 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/entity/AiFaq.java

@@ -0,0 +1,22 @@
+package com.jeesharp.business.modules.ai.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeesharp.framework.mybatis.core.dataobject.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * AI FAQ 内容。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("biz_ai_faq")
+public class AiFaq extends BaseEntity<AiFaq> {
+
+    private String categoryId;
+    private String question;
+    private String answer;
+    private String keywords;
+    private Integer sortNo;
+    private Integer status;
+}

+ 19 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/entity/AiFaqCategory.java

@@ -0,0 +1,19 @@
+package com.jeesharp.business.modules.ai.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeesharp.framework.mybatis.core.dataobject.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * AI FAQ 分类。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("biz_ai_faq_category")
+public class AiFaqCategory extends BaseEntity<AiFaqCategory> {
+
+    private String categoryName;
+    private Integer sortNo;
+    private Integer status;
+}

+ 25 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/support/AiPlatformTenantConstants.java

@@ -0,0 +1,25 @@
+package com.jeesharp.business.modules.ai.support;
+
+import com.jeesharp.business.modules.common.constants.PlatformTenantConstants;
+
+/**
+ * AI 客服模块平台租户常量,与 {@code jeesharp.config.system.platform-tenant-code}、{@code administer-id} 对齐。
+ *
+ * @author jeesharp
+ * @since 2026-06-30
+ * @deprecated 使用 {@link PlatformTenantConstants}
+ */
+@Deprecated
+public final class AiPlatformTenantConstants {
+
+    private AiPlatformTenantConstants() {
+    }
+
+    /** @deprecated 使用 {@link PlatformTenantConstants#PLATFORM_TENANT_CODE} */
+    @Deprecated
+    public static final String PLATFORM_TENANT_CODE = PlatformTenantConstants.PLATFORM_TENANT_CODE;
+
+    /** @deprecated 使用 {@link PlatformTenantConstants#PLATFORM_TENANT_ID} */
+    @Deprecated
+    public static final String PLATFORM_TENANT_ID = PlatformTenantConstants.PLATFORM_TENANT_ID;
+}

+ 15 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatAnswerVO.java

@@ -0,0 +1,15 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class AiChatAnswerVO {
+
+    private String messageId;
+    private String answer;
+    private String answerType;
+    private List<String> references = new ArrayList<>();
+}

+ 39 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatFeedbackInfoVO.java

@@ -0,0 +1,39 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.business.modules.member.vo.MemberOrderDisplay;
+import com.jeesharp.framework.pojo.vo.BaseInfoVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * AI 客服反馈详情 VO。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiChatFeedbackInfoVO extends BaseInfoVO implements MemberOrderDisplay {
+
+    private String sessionId;
+
+    /** 会话展示名(优先标题,无标题则为编码) */
+    private String sessionTitle;
+
+    private String messageId;
+
+    /** 关联消息内容摘要 */
+    private String messageContent;
+
+    private String memberId;
+    private String memberNo;
+    private String memberNickname;
+    private String memberMobile;
+
+    private String feedbackType;
+    private String feedbackNote;
+    private LocalDateTime createDate;
+    private LocalDateTime updateDate;
+}

+ 39 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatFeedbackListVO.java

@@ -0,0 +1,39 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.business.modules.member.vo.MemberOrderDisplay;
+import com.jeesharp.framework.pojo.vo.BaseListVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * AI 客服反馈列表行 VO。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiChatFeedbackListVO extends BaseListVO implements MemberOrderDisplay {
+
+    private String sessionId;
+
+    /** 会话展示名(优先标题,无标题则为编码) */
+    private String sessionTitle;
+
+    private String messageId;
+
+    /** 关联消息内容摘要 */
+    private String messageContent;
+
+    private String memberId;
+    private String memberNo;
+    private String memberNickname;
+    private String memberMobile;
+
+    private String feedbackType;
+    private String feedbackNote;
+    private LocalDateTime createDate;
+    private LocalDateTime updateDate;
+}

+ 47 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatFeedbackQueryVO.java

@@ -0,0 +1,47 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.framework.pojo.query.PaginationVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * AI 客服反馈分页查询条件。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiChatFeedbackQueryVO extends PaginationVO {
+
+    /** 会话主键(精确,兼容) */
+    private String sessionId;
+
+    /** 会话标题/编码(管理端筛选;Application 解析为 sessionIdIn) */
+    private String sessionTitle;
+
+    /** 会话主键 IN(内部) */
+    private List<String> sessionIdIn;
+
+    /** 消息主键(精确,兼容) */
+    private String messageId;
+
+    /** 消息内容(管理端筛选;Application 解析为 messageIdIn) */
+    private String messageContent;
+
+    /** 消息主键 IN(内部) */
+    private List<String> messageIdIn;
+
+    /** 会员主键(精确,兼容) */
+    private String memberId;
+
+    /** 会员编号/昵称/姓名/手机(管理端筛选;Application 解析为 memberIdIn) */
+    private String memberKeyword;
+
+    /** 会员主键 IN(内部) */
+    private List<String> memberIdIn;
+
+    private String feedbackType;
+}

+ 16 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatHistoryVO.java

@@ -0,0 +1,16 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class AiChatHistoryVO {
+
+    private String sessionId;
+    private String sessionTitle;
+    private String sessionStatus;
+    private String updatedAt;
+    private List<AiChatMessageListVO> messages = new ArrayList<>();
+}

+ 39 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatMessageInfoVO.java

@@ -0,0 +1,39 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.business.modules.member.vo.MemberOrderDisplay;
+import com.jeesharp.framework.pojo.vo.BaseInfoVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * AI 客服消息详情 VO。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiChatMessageInfoVO extends BaseInfoVO implements MemberOrderDisplay {
+
+    private String sessionId;
+
+    /** 会话展示名(优先标题,无标题则为编码) */
+    private String sessionTitle;
+
+    private String memberId;
+    private String memberNo;
+    private String memberNickname;
+    private String memberMobile;
+
+    private String messageRole;
+    private String messageType;
+    private String messageContent;
+    private String messageStatus;
+    private String traceId;
+    private String quoteMessageId;
+    private String extJson;
+    private LocalDateTime createDate;
+    private LocalDateTime updateDate;
+}

+ 39 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatMessageListVO.java

@@ -0,0 +1,39 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.business.modules.member.vo.MemberOrderDisplay;
+import com.jeesharp.framework.pojo.vo.BaseListVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * AI 客服消息列表行 VO。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiChatMessageListVO extends BaseListVO implements MemberOrderDisplay {
+
+    private String sessionId;
+
+    /** 会话展示名(优先标题,无标题则为编码) */
+    private String sessionTitle;
+
+    private String memberId;
+    private String memberNo;
+    private String memberNickname;
+    private String memberMobile;
+
+    private String messageRole;
+    private String messageType;
+    private String messageContent;
+    private String messageStatus;
+    private String traceId;
+    private String quoteMessageId;
+    private String extJson;
+    private LocalDateTime createDate;
+    private LocalDateTime updateDate;
+}

+ 43 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatMessageQueryVO.java

@@ -0,0 +1,43 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.framework.pojo.query.PaginationVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * AI 客服消息分页查询条件。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiChatMessageQueryVO extends PaginationVO {
+
+    /** 会话主键(精确,兼容) */
+    private String sessionId;
+
+    /** 会话标题/编码(管理端筛选;Application 解析为 sessionIdIn) */
+    private String sessionTitle;
+
+    /** 会话主键 IN(内部) */
+    private List<String> sessionIdIn;
+
+    /** 会员主键(精确,兼容) */
+    private String memberId;
+
+    /** 会员编号/昵称/姓名/手机(管理端筛选;Application 解析为 memberIdIn) */
+    private String memberKeyword;
+
+    /** 会员主键 IN(内部) */
+    private List<String> memberIdIn;
+
+    private String messageRole;
+    private String messageType;
+    private String messageStatus;
+
+    /** 主键 IN(内部批量补全展示) */
+    private List<String> idIn;
+}

+ 11 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatSessionCreateVO.java

@@ -0,0 +1,11 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import lombok.Data;
+
+@Data
+public class AiChatSessionCreateVO {
+
+    private String sessionId;
+    private String welcomeMessage;
+    private String createdAt;
+}

+ 36 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatSessionInfoVO.java

@@ -0,0 +1,36 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.business.modules.member.vo.MemberOrderDisplay;
+import com.jeesharp.framework.pojo.vo.BaseInfoVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * AI 客服会话详情 VO。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiChatSessionInfoVO extends BaseInfoVO implements MemberOrderDisplay {
+
+    private String memberId;
+    private String memberNo;
+    private String memberNickname;
+    private String memberMobile;
+
+    private String sessionCode;
+    private String sessionTitle;
+    private String sessionStatus;
+    private String handoverStatus;
+    private LocalDateTime handoverTime;
+    private String handlerId;
+    private LocalDateTime lastMessageTime;
+    private String lastMessagePreview;
+    private String extJson;
+    private LocalDateTime createDate;
+    private LocalDateTime updateDate;
+}

+ 36 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatSessionListVO.java

@@ -0,0 +1,36 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.business.modules.member.vo.MemberOrderDisplay;
+import com.jeesharp.framework.pojo.vo.BaseListVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * AI 客服会话列表行 VO。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiChatSessionListVO extends BaseListVO implements MemberOrderDisplay {
+
+    private String memberId;
+    private String memberNo;
+    private String memberNickname;
+    private String memberMobile;
+
+    private String sessionCode;
+    private String sessionTitle;
+    private String sessionStatus;
+    private String handoverStatus;
+    private LocalDateTime handoverTime;
+    private String handlerId;
+    private LocalDateTime lastMessageTime;
+    private String lastMessagePreview;
+    private String extJson;
+    private LocalDateTime createDate;
+    private LocalDateTime updateDate;
+}

+ 34 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiChatSessionQueryVO.java

@@ -0,0 +1,34 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.framework.pojo.query.PaginationVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * AI 客服会话分页查询条件。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiChatSessionQueryVO extends PaginationVO {
+
+    /** 会员主键(精确,兼容) */
+    private String memberId;
+
+    /** 会员编号/昵称/姓名/手机(管理端筛选;Application 解析为 memberIdIn) */
+    private String memberKeyword;
+
+    /** 会员主键 IN(内部) */
+    private List<String> memberIdIn;
+
+    private String sessionCode;
+    private String sessionTitle;
+    private String sessionStatus;
+
+    /** 主键 IN(内部批量补全展示) */
+    private List<String> idIn;
+}

+ 18 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiFaqCategoryInfoVO.java

@@ -0,0 +1,18 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.framework.pojo.vo.BaseInfoVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiFaqCategoryInfoVO extends BaseInfoVO {
+
+    private String categoryName;
+    private Integer sortNo;
+    private Integer status;
+    private LocalDateTime createDate;
+    private LocalDateTime updateDate;
+}

+ 18 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiFaqCategoryListVO.java

@@ -0,0 +1,18 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.framework.pojo.vo.BaseListVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiFaqCategoryListVO extends BaseListVO {
+
+    private String categoryName;
+    private Integer sortNo;
+    private Integer status;
+    private LocalDateTime createDate;
+    private LocalDateTime updateDate;
+}

+ 13 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiFaqCategoryQueryVO.java

@@ -0,0 +1,13 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.framework.pojo.query.PaginationVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiFaqCategoryQueryVO extends PaginationVO {
+
+    private String categoryName;
+    private Integer status;
+}

+ 21 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiFaqInfoVO.java

@@ -0,0 +1,21 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.framework.pojo.vo.BaseInfoVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiFaqInfoVO extends BaseInfoVO {
+
+    private String categoryId;
+    private String question;
+    private String answer;
+    private String keywords;
+    private Integer sortNo;
+    private Integer status;
+    private LocalDateTime createDate;
+    private LocalDateTime updateDate;
+}

+ 23 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiFaqListVO.java

@@ -0,0 +1,23 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.framework.pojo.vo.BaseListVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiFaqListVO extends BaseListVO {
+
+    private String categoryId;
+    private String category;
+    private String question;
+    private String answer;
+    private String keywords;
+    private Boolean hot;
+    private Integer sortNo;
+    private Integer status;
+    private LocalDateTime createDate;
+    private LocalDateTime updateDate;
+}

+ 30 - 0
jeesharp-business/business-ai/business-ai-bean/src/main/java/com/jeesharp/business/modules/ai/vo/AiFaqQueryVO.java

@@ -0,0 +1,30 @@
+package com.jeesharp.business.modules.ai.vo;
+
+import com.jeesharp.framework.pojo.query.PaginationVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * AI FAQ 分页查询条件。
+ *
+ * @author jeesharp
+ * @since 2026-06-30
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiFaqQueryVO extends PaginationVO {
+
+    /** 分类主键(精确,兼容) */
+    private String categoryId;
+
+    /** 分类名称(管理端筛选;Application 解析为 categoryIdIn) */
+    private String categoryKeyword;
+
+    /** 分类主键 IN(内部) */
+    private List<String> categoryIdIn;
+
+    private String question;
+    private Integer status;
+}

+ 30 - 0
jeesharp-business/business-ai/business-ai-controller/pom.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.xinghuacuntravel</groupId>
+        <artifactId>business-ai</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>business-ai-controller</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.xinghuacuntravel</groupId>
+            <artifactId>business-ai-bean</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.xinghuacuntravel</groupId>
+            <artifactId>business-ai-application</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jeesharp.boot</groupId>
+            <artifactId>jeesharp-spring-boot-starter-web</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 57 - 0
jeesharp-business/business-ai/business-ai-controller/src/main/java/com/jeesharp/business/modules/ai/controller/AiChatFeedbackController.java

@@ -0,0 +1,57 @@
+package com.jeesharp.business.modules.ai.controller;
+
+import com.jeesharp.business.modules.ai.application.AiChatFeedbackApplicationService;
+import com.jeesharp.business.modules.ai.dto.AiChatFeedbackSaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiChatFeedback;
+import com.jeesharp.business.modules.ai.vo.AiChatFeedbackInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiChatFeedbackListVO;
+import com.jeesharp.business.modules.ai.vo.AiChatFeedbackQueryVO;
+import com.jeesharp.framework.pojo.AjaxResult;
+import com.jeesharp.framework.pojo.Page;
+import com.jeesharp.framework.pojo.query.PageRespVO;
+import com.jeesharp.framework.web.controller.BaseController;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.AllArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping(value = "${jeesharp.web.adminPath}/manager/business/ai/chatFeedback/v1")
+@Tag(name = "AI客服-反馈")
+@AllArgsConstructor
+public class AiChatFeedbackController extends BaseController {
+
+    private final AiChatFeedbackApplicationService aiChatFeedbackApplicationService;
+
+    @Operation(summary = "获取详情")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:chatFeedback:info')")
+    @GetMapping("info")
+    public AjaxResult<AiChatFeedbackInfoVO> info(@RequestParam("id") String id) {
+        return AjaxResult.okData(aiChatFeedbackApplicationService.get(id));
+    }
+
+    @Operation(summary = "分页列表")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:chatFeedback:list')")
+    @PostMapping("list")
+    public AjaxResult<PageRespVO<AiChatFeedbackListVO>> list(@RequestBody AiChatFeedbackQueryVO query) {
+        return AjaxResult.okData(aiChatFeedbackApplicationService.findPage(new Page<>(query), query));
+    }
+
+    @Operation(summary = "保存")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:chatFeedback:save')")
+    @PostMapping("save")
+    public AjaxResult<Void> save(@RequestBody @Valid AiChatFeedbackSaveDTO dto) {
+        aiChatFeedbackApplicationService.save(dto);
+        return AjaxResult.ok("保存成功");
+    }
+
+    @Operation(summary = "删除")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:chatFeedback:delete')")
+    @GetMapping("delete")
+    public AjaxResult<Void> delete(@RequestParam("ids") String ids) {
+        aiChatFeedbackApplicationService.delete(ids);
+        return AjaxResult.ok("删除成功");
+    }
+}

+ 57 - 0
jeesharp-business/business-ai/business-ai-controller/src/main/java/com/jeesharp/business/modules/ai/controller/AiChatMessageController.java

@@ -0,0 +1,57 @@
+package com.jeesharp.business.modules.ai.controller;
+
+import com.jeesharp.business.modules.ai.application.AiChatMessageApplicationService;
+import com.jeesharp.business.modules.ai.dto.AiChatMessageSaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiChatMessage;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageListVO;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageQueryVO;
+import com.jeesharp.framework.pojo.AjaxResult;
+import com.jeesharp.framework.pojo.Page;
+import com.jeesharp.framework.pojo.query.PageRespVO;
+import com.jeesharp.framework.web.controller.BaseController;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.AllArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping(value = "${jeesharp.web.adminPath}/manager/business/ai/chatMessage/v1")
+@Tag(name = "AI客服-消息")
+@AllArgsConstructor
+public class AiChatMessageController extends BaseController {
+
+    private final AiChatMessageApplicationService aiChatMessageApplicationService;
+
+    @Operation(summary = "获取详情")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:chatMessage:info')")
+    @GetMapping("info")
+    public AjaxResult<AiChatMessageInfoVO> info(@RequestParam("id") String id) {
+        return AjaxResult.okData(aiChatMessageApplicationService.get(id));
+    }
+
+    @Operation(summary = "分页列表")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:chatMessage:list')")
+    @PostMapping("list")
+    public AjaxResult<PageRespVO<AiChatMessageListVO>> list(@RequestBody AiChatMessageQueryVO query) {
+        return AjaxResult.okData(aiChatMessageApplicationService.findPage(new Page<>(query), query));
+    }
+
+    @Operation(summary = "保存")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:chatMessage:save')")
+    @PostMapping("save")
+    public AjaxResult<Void> save(@RequestBody @Valid AiChatMessageSaveDTO dto) {
+        aiChatMessageApplicationService.save(dto);
+        return AjaxResult.ok("保存成功");
+    }
+
+    @Operation(summary = "删除")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:chatMessage:delete')")
+    @GetMapping("delete")
+    public AjaxResult<Void> delete(@RequestParam("ids") String ids) {
+        aiChatMessageApplicationService.delete(ids);
+        return AjaxResult.ok("删除成功");
+    }
+}

+ 57 - 0
jeesharp-business/business-ai/business-ai-controller/src/main/java/com/jeesharp/business/modules/ai/controller/AiChatSessionController.java

@@ -0,0 +1,57 @@
+package com.jeesharp.business.modules.ai.controller;
+
+import com.jeesharp.business.modules.ai.application.AiChatSessionApplicationService;
+import com.jeesharp.business.modules.ai.dto.AiChatSessionSaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiChatSession;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionListVO;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionQueryVO;
+import com.jeesharp.framework.pojo.AjaxResult;
+import com.jeesharp.framework.pojo.Page;
+import com.jeesharp.framework.pojo.query.PageRespVO;
+import com.jeesharp.framework.web.controller.BaseController;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.AllArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping(value = "${jeesharp.web.adminPath}/manager/business/ai/chatSession/v1")
+@Tag(name = "AI客服-会话")
+@AllArgsConstructor
+public class AiChatSessionController extends BaseController {
+
+    private final AiChatSessionApplicationService aiChatSessionApplicationService;
+
+    @Operation(summary = "获取详情")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:chatSession:info')")
+    @GetMapping("info")
+    public AjaxResult<AiChatSessionInfoVO> info(@RequestParam("id") String id) {
+        return AjaxResult.okData(aiChatSessionApplicationService.get(id));
+    }
+
+    @Operation(summary = "分页列表")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:chatSession:list')")
+    @PostMapping("list")
+    public AjaxResult<PageRespVO<AiChatSessionListVO>> list(@RequestBody AiChatSessionQueryVO query) {
+        return AjaxResult.okData(aiChatSessionApplicationService.findPage(new Page<>(query), query));
+    }
+
+    @Operation(summary = "保存")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:chatSession:save')")
+    @PostMapping("save")
+    public AjaxResult<Void> save(@RequestBody @Valid AiChatSessionSaveDTO dto) {
+        aiChatSessionApplicationService.save(dto);
+        return AjaxResult.ok("保存成功");
+    }
+
+    @Operation(summary = "删除")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:chatSession:delete')")
+    @GetMapping("delete")
+    public AjaxResult<Void> delete(@RequestParam("ids") String ids) {
+        aiChatSessionApplicationService.delete(ids);
+        return AjaxResult.ok("删除成功");
+    }
+}

+ 57 - 0
jeesharp-business/business-ai/business-ai-controller/src/main/java/com/jeesharp/business/modules/ai/controller/AiFaqCategoryController.java

@@ -0,0 +1,57 @@
+package com.jeesharp.business.modules.ai.controller;
+
+import com.jeesharp.business.modules.ai.application.AiFaqCategoryApplicationService;
+import com.jeesharp.business.modules.ai.dto.AiFaqCategorySaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiFaqCategory;
+import com.jeesharp.business.modules.ai.vo.AiFaqCategoryInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiFaqCategoryListVO;
+import com.jeesharp.business.modules.ai.vo.AiFaqCategoryQueryVO;
+import com.jeesharp.framework.pojo.AjaxResult;
+import com.jeesharp.framework.pojo.Page;
+import com.jeesharp.framework.pojo.query.PageRespVO;
+import com.jeesharp.framework.web.controller.BaseController;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.AllArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping(value = "${jeesharp.web.adminPath}/manager/business/ai/faqCategory/v1")
+@Tag(name = "AI客服-FAQ分类")
+@AllArgsConstructor
+public class AiFaqCategoryController extends BaseController {
+
+    private final AiFaqCategoryApplicationService aiFaqCategoryApplicationService;
+
+    @Operation(summary = "获取详情")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:faqCategory:info')")
+    @GetMapping("info")
+    public AjaxResult<AiFaqCategoryInfoVO> info(@RequestParam("id") String id) {
+        return AjaxResult.okData(aiFaqCategoryApplicationService.get(id));
+    }
+
+    @Operation(summary = "分页列表")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:faqCategory:list')")
+    @PostMapping("list")
+    public AjaxResult<PageRespVO<AiFaqCategoryListVO>> list(@RequestBody AiFaqCategoryQueryVO query) {
+        return AjaxResult.okData(aiFaqCategoryApplicationService.findPage(new Page<>(query), query));
+    }
+
+    @Operation(summary = "保存")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:faqCategory:save')")
+    @PostMapping("save")
+    public AjaxResult<Void> save(@RequestBody @Valid AiFaqCategorySaveDTO dto) {
+        aiFaqCategoryApplicationService.save(dto);
+        return AjaxResult.ok("保存成功");
+    }
+
+    @Operation(summary = "删除")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:faqCategory:delete')")
+    @GetMapping("delete")
+    public AjaxResult<Void> delete(@RequestParam("ids") String ids) {
+        aiFaqCategoryApplicationService.delete(ids);
+        return AjaxResult.ok("删除成功");
+    }
+}

+ 57 - 0
jeesharp-business/business-ai/business-ai-controller/src/main/java/com/jeesharp/business/modules/ai/controller/AiFaqController.java

@@ -0,0 +1,57 @@
+package com.jeesharp.business.modules.ai.controller;
+
+import com.jeesharp.business.modules.ai.application.AiFaqApplicationService;
+import com.jeesharp.business.modules.ai.dto.AiFaqSaveDTO;
+import com.jeesharp.business.modules.ai.entity.AiFaq;
+import com.jeesharp.business.modules.ai.vo.AiFaqInfoVO;
+import com.jeesharp.business.modules.ai.vo.AiFaqListVO;
+import com.jeesharp.business.modules.ai.vo.AiFaqQueryVO;
+import com.jeesharp.framework.pojo.AjaxResult;
+import com.jeesharp.framework.pojo.Page;
+import com.jeesharp.framework.pojo.query.PageRespVO;
+import com.jeesharp.framework.web.controller.BaseController;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.AllArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping(value = "${jeesharp.web.adminPath}/manager/business/ai/faq/v1")
+@Tag(name = "AI客服-FAQ内容")
+@AllArgsConstructor
+public class AiFaqController extends BaseController {
+
+    private final AiFaqApplicationService aiFaqApplicationService;
+
+    @Operation(summary = "获取详情")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:faq:info')")
+    @GetMapping("info")
+    public AjaxResult<AiFaqInfoVO> info(@RequestParam("id") String id) {
+        return AjaxResult.okData(aiFaqApplicationService.get(id));
+    }
+
+    @Operation(summary = "分页列表")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:faq:list')")
+    @PostMapping("list")
+    public AjaxResult<PageRespVO<AiFaqListVO>> list(@RequestBody AiFaqQueryVO query) {
+        return AjaxResult.okData(aiFaqApplicationService.findPage(new Page<>(query), query));
+    }
+
+    @Operation(summary = "保存")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:faq:save')")
+    @PostMapping("save")
+    public AjaxResult<Void> save(@RequestBody @Valid AiFaqSaveDTO dto) {
+        aiFaqApplicationService.save(dto);
+        return AjaxResult.ok("保存成功");
+    }
+
+    @Operation(summary = "删除")
+    @PreAuthorize("@ss.hasPermission('manager:business:ai:faq:delete')")
+    @GetMapping("delete")
+    public AjaxResult<Void> delete(@RequestParam("ids") String ids) {
+        aiFaqApplicationService.delete(ids);
+        return AjaxResult.ok("删除成功");
+    }
+}

+ 26 - 0
jeesharp-business/business-ai/business-ai-service/pom.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.xinghuacuntravel</groupId>
+        <artifactId>business-ai</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>business-ai-service</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jeesharp.boot</groupId>
+            <artifactId>jeesharp-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+<dependency>
+            <groupId>com.xinghuacuntravel</groupId>
+            <artifactId>business-ai-bean</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 10 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/AiChatFeedbackMapper.java

@@ -0,0 +1,10 @@
+package com.jeesharp.business.modules.ai.mapper;
+
+import com.jeesharp.business.modules.ai.entity.AiChatFeedback;
+import com.jeesharp.business.modules.ai.vo.AiChatFeedbackQueryVO;
+import com.jeesharp.framework.mybatis.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AiChatFeedbackMapper extends BaseMapper<AiChatFeedback, AiChatFeedbackQueryVO> {
+}

+ 10 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/AiChatMessageMapper.java

@@ -0,0 +1,10 @@
+package com.jeesharp.business.modules.ai.mapper;
+
+import com.jeesharp.business.modules.ai.entity.AiChatMessage;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageQueryVO;
+import com.jeesharp.framework.mybatis.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AiChatMessageMapper extends BaseMapper<AiChatMessage, AiChatMessageQueryVO> {
+}

+ 10 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/AiChatSessionMapper.java

@@ -0,0 +1,10 @@
+package com.jeesharp.business.modules.ai.mapper;
+
+import com.jeesharp.business.modules.ai.entity.AiChatSession;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionQueryVO;
+import com.jeesharp.framework.mybatis.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AiChatSessionMapper extends BaseMapper<AiChatSession, AiChatSessionQueryVO> {
+}

+ 10 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/AiFaqCategoryMapper.java

@@ -0,0 +1,10 @@
+package com.jeesharp.business.modules.ai.mapper;
+
+import com.jeesharp.business.modules.ai.entity.AiFaqCategory;
+import com.jeesharp.business.modules.ai.vo.AiFaqCategoryQueryVO;
+import com.jeesharp.framework.mybatis.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AiFaqCategoryMapper extends BaseMapper<AiFaqCategory, AiFaqCategoryQueryVO> {
+}

+ 10 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/AiFaqMapper.java

@@ -0,0 +1,10 @@
+package com.jeesharp.business.modules.ai.mapper;
+
+import com.jeesharp.business.modules.ai.entity.AiFaq;
+import com.jeesharp.business.modules.ai.vo.AiFaqQueryVO;
+import com.jeesharp.framework.mybatis.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AiFaqMapper extends BaseMapper<AiFaq, AiFaqQueryVO> {
+}

+ 80 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/xml/AiChatFeedbackMapper.xml

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesharp.business.modules.ai.mapper.AiChatFeedbackMapper">
+    <sql id="aiChatFeedbackColumns">
+        id, session_id, message_id, member_id, feedback_type, feedback_note,
+        create_by, create_date, update_by, update_date, del_flag, remarks, tenant_code, tenant_id
+    </sql>
+
+    <select id="get" parameterType="string" resultType="com.jeesharp.business.modules.ai.entity.AiChatFeedback">
+        SELECT <include refid="aiChatFeedbackColumns"/>
+        FROM app_ai_chat_feedback
+        WHERE id = #{id} AND del_flag = '0'
+        LIMIT 1
+    </select>
+
+    <select id="findList" resultType="com.jeesharp.business.modules.ai.entity.AiChatFeedback">
+        SELECT <include refid="aiChatFeedbackColumns"/>
+        FROM app_ai_chat_feedback a
+        <where>
+            
+            ${dataScope}
+            AND a.del_flag = '0'
+            <if test="sessionId != null and sessionId != ''">AND session_id = #{sessionId}</if>
+            <if test="sessionIdIn != null and sessionIdIn.size() > 0">
+                AND session_id IN
+                <foreach collection="sessionIdIn" item="sid" open="(" separator="," close=")">#{sid}</foreach>
+            </if>
+            <if test="messageId != null and messageId != ''">AND message_id = #{messageId}</if>
+            <if test="messageIdIn != null and messageIdIn.size() > 0">
+                AND message_id IN
+                <foreach collection="messageIdIn" item="msgId" open="(" separator="," close=")">#{msgId}</foreach>
+            </if>
+            <if test="memberId != null and memberId != ''">AND member_id = #{memberId}</if>
+            <if test="memberIdIn != null and memberIdIn.size() > 0">
+                AND member_id IN
+                <foreach collection="memberIdIn" item="mid" open="(" separator="," close=")">#{mid}</foreach>
+            </if>
+            <if test="feedbackType != null and feedbackType != ''">AND feedback_type = #{feedbackType}</if>
+        </where>
+        ORDER BY create_date DESC
+    </select>
+
+    <insert id="insert" parameterType="com.jeesharp.business.modules.ai.entity.AiChatFeedback">
+        INSERT INTO app_ai_chat_feedback (
+            id, session_id, message_id, member_id, feedback_type, feedback_note,
+            create_by, create_date, update_by, update_date, del_flag, remarks, tenant_code, tenant_id
+        ) VALUES (
+            #{id}, #{sessionId}, #{messageId}, #{memberId}, #{feedbackType}, #{feedbackNote},
+            #{createById}, #{createDate}, #{updateById}, #{updateDate}, #{delFlag}, #{remarks}, #{tenantCode}, #{tenantId}
+        )
+    </insert>
+
+    <update id="update" parameterType="com.jeesharp.business.modules.ai.entity.AiChatFeedback">
+        UPDATE app_ai_chat_feedback
+        <set>
+            <if test="sessionId != null">session_id = #{sessionId},</if>
+            <if test="messageId != null">message_id = #{messageId},</if>
+            <if test="memberId != null">member_id = #{memberId},</if>
+            <if test="feedbackType != null">feedback_type = #{feedbackType},</if>
+            <if test="feedbackNote != null">feedback_note = #{feedbackNote},</if>
+            <if test="updateById != null">update_by = #{updateById},</if>
+            <if test="updateDate != null">update_date = #{updateDate},</if>
+            <if test="remarks != null">remarks = #{remarks},</if>
+            <if test="tenantCode != null">tenant_code = #{tenantCode},</if>
+            <if test="tenantId != null">tenant_id = #{tenantId},</if>
+        </set>
+        WHERE id = #{id} AND tenant_code = #{tenantCode} AND del_flag = '0'
+    </update>
+
+    <update id="delete" parameterType="string">
+        UPDATE app_ai_chat_feedback SET del_flag = '1', update_date = NOW() WHERE id = #{id} AND tenant_code = #{tenantCode} AND del_flag = '0'
+    </update>
+
+    <update id="batchDelete">
+        UPDATE app_ai_chat_feedback SET del_flag = '1', update_date = NOW()
+        WHERE del_flag = '0'
+        AND tenant_code = #{tenantCode} AND id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
+    </update>
+</mapper>

+ 85 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/xml/AiChatMessageMapper.xml

@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesharp.business.modules.ai.mapper.AiChatMessageMapper">
+    <sql id="aiChatMessageColumns">
+        id, session_id, member_id, message_role, message_type, message_content,
+        message_status, trace_id, quote_message_id, ext_json, create_by, create_date,
+        update_by, update_date, del_flag, remarks, tenant_code, tenant_id
+    </sql>
+
+    <select id="get" parameterType="string" resultType="com.jeesharp.business.modules.ai.entity.AiChatMessage">
+        SELECT <include refid="aiChatMessageColumns"/> FROM app_ai_chat_message
+        WHERE id = #{id} AND del_flag = '0' LIMIT 1
+    </select>
+
+    <select id="findList" resultType="com.jeesharp.business.modules.ai.entity.AiChatMessage">
+        SELECT <include refid="aiChatMessageColumns"/> FROM app_ai_chat_message a
+        <where>
+            
+            ${dataScope}
+            AND a.del_flag = '0'
+            <if test="sessionId != null and sessionId != ''">AND session_id = #{sessionId}</if>
+            <if test="sessionIdIn != null and sessionIdIn.size() > 0">
+                AND session_id IN
+                <foreach collection="sessionIdIn" item="sid" open="(" separator="," close=")">#{sid}</foreach>
+            </if>
+            <if test="memberId != null and memberId != ''">AND member_id = #{memberId}</if>
+            <if test="memberIdIn != null and memberIdIn.size() > 0">
+                AND member_id IN
+                <foreach collection="memberIdIn" item="mid" open="(" separator="," close=")">#{mid}</foreach>
+            </if>
+            <if test="messageRole != null and messageRole != ''">AND message_role = #{messageRole}</if>
+            <if test="messageType != null and messageType != ''">AND message_type = #{messageType}</if>
+            <if test="messageStatus != null and messageStatus != ''">AND message_status = #{messageStatus}</if>
+            <if test="idIn != null and idIn.size() > 0">
+                AND id IN
+                <foreach collection="idIn" item="msgId" open="(" separator="," close=")">#{msgId}</foreach>
+            </if>
+        </where>
+        ORDER BY create_date DESC
+    </select>
+
+    <insert id="insert" parameterType="com.jeesharp.business.modules.ai.entity.AiChatMessage">
+        INSERT INTO app_ai_chat_message (
+            id, session_id, member_id, message_role, message_type, message_content,
+            message_status, trace_id, quote_message_id, ext_json, create_by, create_date,
+            update_by, update_date, del_flag, remarks, tenant_code, tenant_id
+        ) VALUES (
+            #{id}, #{sessionId}, #{memberId}, #{messageRole}, #{messageType}, #{messageContent},
+            #{messageStatus}, #{traceId}, #{quoteMessageId}, #{extJson}, #{createById}, #{createDate},
+            #{updateById}, #{updateDate}, #{delFlag}, #{remarks}, #{tenantCode}, #{tenantId}
+        )
+    </insert>
+
+    <update id="update" parameterType="com.jeesharp.business.modules.ai.entity.AiChatMessage">
+        UPDATE app_ai_chat_message
+        <set>
+            <if test="sessionId != null">session_id = #{sessionId},</if>
+            <if test="memberId != null">member_id = #{memberId},</if>
+            <if test="messageRole != null">message_role = #{messageRole},</if>
+            <if test="messageType != null">message_type = #{messageType},</if>
+            <if test="messageContent != null">message_content = #{messageContent},</if>
+            <if test="messageStatus != null">message_status = #{messageStatus},</if>
+            <if test="traceId != null">trace_id = #{traceId},</if>
+            <if test="quoteMessageId != null">quote_message_id = #{quoteMessageId},</if>
+            <if test="extJson != null">ext_json = #{extJson},</if>
+            <if test="updateById != null">update_by = #{updateById},</if>
+            <if test="updateDate != null">update_date = #{updateDate},</if>
+            <if test="remarks != null">remarks = #{remarks},</if>
+            <if test="tenantCode != null">tenant_code = #{tenantCode},</if>
+            <if test="tenantId != null">tenant_id = #{tenantId},</if>
+        </set>
+        WHERE id = #{id} AND tenant_code = #{tenantCode} AND del_flag = '0'
+    </update>
+
+    <update id="delete" parameterType="string">
+        UPDATE app_ai_chat_message SET del_flag = '1', update_date = NOW() WHERE id = #{id} AND tenant_code = #{tenantCode} AND del_flag = '0'
+    </update>
+
+    <update id="batchDelete">
+        UPDATE app_ai_chat_message SET del_flag = '1', update_date = NOW()
+        WHERE del_flag = '0'
+        AND tenant_code = #{tenantCode} AND id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
+    </update>
+</mapper>

+ 81 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/xml/AiChatSessionMapper.xml

@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesharp.business.modules.ai.mapper.AiChatSessionMapper">
+    <sql id="aiChatSessionColumns">
+        id, member_id, session_code, session_title, session_status, handover_status,
+        handover_time, handler_id, last_message_time, last_message_preview, ext_json,
+        create_by, create_date, update_by, update_date, del_flag, remarks, tenant_code, tenant_id
+    </sql>
+
+    <select id="get" parameterType="string" resultType="com.jeesharp.business.modules.ai.entity.AiChatSession">
+        SELECT <include refid="aiChatSessionColumns"/> FROM app_ai_chat_session
+        WHERE id = #{id} AND del_flag = '0' LIMIT 1
+    </select>
+
+    <select id="findList" resultType="com.jeesharp.business.modules.ai.entity.AiChatSession">
+        SELECT <include refid="aiChatSessionColumns"/> FROM app_ai_chat_session a
+        <where>
+            
+            ${dataScope}
+            AND a.del_flag = '0'
+            <if test="memberId != null and memberId != ''">AND member_id = #{memberId}</if>
+            <if test="memberIdIn != null and memberIdIn.size() > 0">
+                AND member_id IN
+                <foreach collection="memberIdIn" item="mid" open="(" separator="," close=")">#{mid}</foreach>
+            </if>
+            <if test="sessionCode != null and sessionCode != ''">AND session_code = #{sessionCode}</if>
+            <if test="sessionTitle != null and sessionTitle != ''">AND session_title LIKE CONCAT('%', #{sessionTitle}, '%')</if>
+            <if test="sessionStatus != null and sessionStatus != ''">AND session_status = #{sessionStatus}</if>
+            <if test="idIn != null and idIn.size() > 0">
+                AND id IN
+                <foreach collection="idIn" item="sid" open="(" separator="," close=")">#{sid}</foreach>
+            </if>
+        </where>
+        ORDER BY create_date DESC
+    </select>
+
+    <insert id="insert" parameterType="com.jeesharp.business.modules.ai.entity.AiChatSession">
+        INSERT INTO app_ai_chat_session (
+            id, member_id, session_code, session_title, session_status, handover_status,
+            handover_time, handler_id, last_message_time, last_message_preview, ext_json,
+            create_by, create_date, update_by, update_date, del_flag, remarks, tenant_code, tenant_id
+        ) VALUES (
+            #{id}, #{memberId}, #{sessionCode}, #{sessionTitle}, #{sessionStatus}, #{handoverStatus},
+            #{handoverTime}, #{handlerId}, #{lastMessageTime}, #{lastMessagePreview}, #{extJson},
+            #{createById}, #{createDate}, #{updateById}, #{updateDate}, #{delFlag}, #{remarks}, #{tenantCode}, #{tenantId}
+        )
+    </insert>
+
+    <update id="update" parameterType="com.jeesharp.business.modules.ai.entity.AiChatSession">
+        UPDATE app_ai_chat_session
+        <set>
+            <if test="memberId != null">member_id = #{memberId},</if>
+            <if test="sessionCode != null">session_code = #{sessionCode},</if>
+            <if test="sessionTitle != null">session_title = #{sessionTitle},</if>
+            <if test="sessionStatus != null">session_status = #{sessionStatus},</if>
+            <if test="handoverStatus != null">handover_status = #{handoverStatus},</if>
+            <if test="handoverTime != null">handover_time = #{handoverTime},</if>
+            <if test="handlerId != null">handler_id = #{handlerId},</if>
+            <if test="lastMessageTime != null">last_message_time = #{lastMessageTime},</if>
+            <if test="lastMessagePreview != null">last_message_preview = #{lastMessagePreview},</if>
+            <if test="extJson != null">ext_json = #{extJson},</if>
+            <if test="updateById != null">update_by = #{updateById},</if>
+            <if test="updateDate != null">update_date = #{updateDate},</if>
+            <if test="remarks != null">remarks = #{remarks},</if>
+            <if test="tenantCode != null">tenant_code = #{tenantCode},</if>
+            <if test="tenantId != null">tenant_id = #{tenantId},</if>
+        </set>
+        WHERE id = #{id} AND tenant_code = #{tenantCode} AND del_flag = '0'
+    </update>
+
+    <update id="delete" parameterType="string">
+        UPDATE app_ai_chat_session SET del_flag = '1', update_date = NOW() WHERE id = #{id} AND tenant_code = #{tenantCode} AND del_flag = '0'
+    </update>
+
+    <update id="batchDelete">
+        UPDATE app_ai_chat_session SET del_flag = '1', update_date = NOW()
+        WHERE del_flag = '0'
+        AND tenant_code = #{tenantCode} AND id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
+    </update>
+</mapper>

+ 58 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/xml/AiFaqCategoryMapper.xml

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesharp.business.modules.ai.mapper.AiFaqCategoryMapper">
+    <sql id="aiFaqCategoryColumns">
+        id, category_name, sort_no, status, create_by, create_date, update_by, update_date, del_flag, remarks, tenant_code, tenant_id
+    </sql>
+
+    <select id="get" parameterType="string" resultType="com.jeesharp.business.modules.ai.entity.AiFaqCategory">
+        SELECT <include refid="aiFaqCategoryColumns"/> FROM biz_ai_faq_category
+        WHERE id = #{id} AND del_flag = '0' LIMIT 1
+    </select>
+
+    <select id="findList" resultType="com.jeesharp.business.modules.ai.entity.AiFaqCategory">
+        SELECT <include refid="aiFaqCategoryColumns"/> FROM biz_ai_faq_category a
+        <where>
+            
+            ${dataScope}
+            AND a.del_flag = '0'
+            <if test="categoryName != null and categoryName != ''">AND category_name LIKE CONCAT('%', #{categoryName}, '%')</if>
+            <if test="status != null">AND status = #{status}</if>
+        </where>
+        ORDER BY sort_no ASC, create_date DESC
+    </select>
+
+    <insert id="insert" parameterType="com.jeesharp.business.modules.ai.entity.AiFaqCategory">
+        INSERT INTO biz_ai_faq_category (
+            id, category_name, sort_no, status, create_by, create_date, update_by, update_date, del_flag, remarks, tenant_code, tenant_id
+        ) VALUES (
+            #{id}, #{categoryName}, #{sortNo}, #{status}, #{createById}, #{createDate}, #{updateById}, #{updateDate}, #{delFlag}, #{remarks}, #{tenantCode}, #{tenantId}
+        )
+    </insert>
+
+    <update id="update" parameterType="com.jeesharp.business.modules.ai.entity.AiFaqCategory">
+        UPDATE biz_ai_faq_category
+        <set>
+            <if test="categoryName != null">category_name = #{categoryName},</if>
+            <if test="sortNo != null">sort_no = #{sortNo},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="updateById != null">update_by = #{updateById},</if>
+            <if test="updateDate != null">update_date = #{updateDate},</if>
+            <if test="remarks != null">remarks = #{remarks},</if>
+            <if test="tenantCode != null">tenant_code = #{tenantCode},</if>
+            <if test="tenantId != null">tenant_id = #{tenantId},</if>
+        </set>
+        WHERE id = #{id} AND tenant_code = #{tenantCode} AND del_flag = '0'
+    </update>
+
+    <update id="delete" parameterType="string">
+        UPDATE biz_ai_faq_category SET del_flag = '1', update_date = NOW() WHERE id = #{id} AND tenant_code = #{tenantCode} AND del_flag = '0'
+    </update>
+
+    <update id="batchDelete">
+        UPDATE biz_ai_faq_category SET del_flag = '1', update_date = NOW()
+        WHERE del_flag = '0'
+        AND tenant_code = #{tenantCode} AND id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
+    </update>
+</mapper>

+ 69 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/mapper/xml/AiFaqMapper.xml

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesharp.business.modules.ai.mapper.AiFaqMapper">
+    <sql id="aiFaqColumns">
+        id, category_id, question, answer, keywords, sort_no, status,
+        create_by, create_date, update_by, update_date, del_flag, remarks, tenant_code, tenant_id
+    </sql>
+
+    <select id="get" parameterType="string" resultType="com.jeesharp.business.modules.ai.entity.AiFaq">
+        SELECT <include refid="aiFaqColumns"/> FROM biz_ai_faq
+        WHERE id = #{id} AND del_flag = '0' LIMIT 1
+    </select>
+
+    <select id="findList" resultType="com.jeesharp.business.modules.ai.entity.AiFaq">
+        SELECT <include refid="aiFaqColumns"/> FROM biz_ai_faq a
+        <where>
+            
+            ${dataScope}
+            AND a.del_flag = '0'
+            <if test="categoryId != null and categoryId != ''">AND category_id = #{categoryId}</if>
+            <if test="categoryIdIn != null and categoryIdIn.size() > 0">
+                AND category_id IN
+                <foreach collection="categoryIdIn" item="cid" open="(" separator="," close=")">#{cid}</foreach>
+            </if>
+            <if test="question != null and question != ''">AND question LIKE CONCAT('%', #{question}, '%')</if>
+            <if test="status != null">AND status = #{status}</if>
+        </where>
+        ORDER BY sort_no ASC, create_date DESC
+    </select>
+
+    <insert id="insert" parameterType="com.jeesharp.business.modules.ai.entity.AiFaq">
+        INSERT INTO biz_ai_faq (
+            id, category_id, question, answer, keywords, sort_no, status,
+            create_by, create_date, update_by, update_date, del_flag, remarks, tenant_code, tenant_id
+        ) VALUES (
+            #{id}, #{categoryId}, #{question}, #{answer}, #{keywords}, #{sortNo}, #{status},
+            #{createById}, #{createDate}, #{updateById}, #{updateDate}, #{delFlag}, #{remarks}, #{tenantCode}, #{tenantId}
+        )
+    </insert>
+
+    <update id="update" parameterType="com.jeesharp.business.modules.ai.entity.AiFaq">
+        UPDATE biz_ai_faq
+        <set>
+            <if test="categoryId != null">category_id = #{categoryId},</if>
+            <if test="question != null">question = #{question},</if>
+            <if test="answer != null">answer = #{answer},</if>
+            <if test="keywords != null">keywords = #{keywords},</if>
+            <if test="sortNo != null">sort_no = #{sortNo},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="updateById != null">update_by = #{updateById},</if>
+            <if test="updateDate != null">update_date = #{updateDate},</if>
+            <if test="remarks != null">remarks = #{remarks},</if>
+            <if test="tenantCode != null">tenant_code = #{tenantCode},</if>
+            <if test="tenantId != null">tenant_id = #{tenantId},</if>
+        </set>
+        WHERE id = #{id} AND tenant_code = #{tenantCode} AND del_flag = '0'
+    </update>
+
+    <update id="delete" parameterType="string">
+        UPDATE biz_ai_faq SET del_flag = '1', update_date = NOW() WHERE id = #{id} AND tenant_code = #{tenantCode} AND del_flag = '0'
+    </update>
+
+    <update id="batchDelete">
+        UPDATE biz_ai_faq SET del_flag = '1', update_date = NOW()
+        WHERE del_flag = '0'
+        AND tenant_code = #{tenantCode} AND id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
+    </update>
+</mapper>

+ 12 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/AiChatFeedbackService.java

@@ -0,0 +1,12 @@
+package com.jeesharp.business.modules.ai.service;
+
+import com.jeesharp.business.modules.ai.entity.AiChatFeedback;
+import com.jeesharp.business.modules.ai.vo.AiChatFeedbackQueryVO;
+import com.jeesharp.framework.mybatis.core.service.BaseInterface;
+
+public interface AiChatFeedbackService extends BaseInterface<AiChatFeedback, AiChatFeedbackQueryVO> {
+
+    void createFeedback(AiChatFeedback entity);
+
+    void updateFeedback(AiChatFeedback entity);
+}

+ 30 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/AiChatMessageService.java

@@ -0,0 +1,30 @@
+package com.jeesharp.business.modules.ai.service;
+
+import com.jeesharp.business.modules.ai.entity.AiChatMessage;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageQueryVO;
+import com.jeesharp.framework.mybatis.core.service.BaseInterface;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * AI 会话消息领域服务接口。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+public interface AiChatMessageService extends BaseInterface<AiChatMessage, AiChatMessageQueryVO> {
+
+    void createMessage(AiChatMessage entity);
+
+    void updateMessage(AiChatMessage entity);
+/**
+     * 按消息内容模糊匹配,返回消息主键列表。
+     */
+    List<String> findIdsByContentLike(String keyword);
+
+    /**
+     * 批量获取消息内容摘要(列表展示用,过长截断)。
+     */
+    Map<String, String> mapMessageContentPreviewByIds(List<String> ids);
+}

+ 30 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/AiChatSessionService.java

@@ -0,0 +1,30 @@
+package com.jeesharp.business.modules.ai.service;
+
+import com.jeesharp.business.modules.ai.entity.AiChatSession;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionQueryVO;
+import com.jeesharp.framework.mybatis.core.service.BaseInterface;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * AI 会话领域服务接口。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+public interface AiChatSessionService extends BaseInterface<AiChatSession, AiChatSessionQueryVO> {
+
+    void createSession(AiChatSession entity);
+
+    void updateSession(AiChatSession entity);
+/**
+     * 按会话标题或编码模糊匹配,返回会话主键列表。
+     */
+    List<String> findIdsByTitleOrCodeLike(String keyword);
+
+    /**
+     * 批量获取会话展示名(优先标题,无标题则用编码)。
+     */
+    Map<String, String> mapSessionLabelByIds(List<String> ids);
+}

+ 24 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/AiFaqCategoryService.java

@@ -0,0 +1,24 @@
+package com.jeesharp.business.modules.ai.service;
+
+import com.jeesharp.business.modules.ai.entity.AiFaqCategory;
+import com.jeesharp.business.modules.ai.vo.AiFaqCategoryQueryVO;
+import com.jeesharp.framework.mybatis.core.service.BaseInterface;
+
+import java.util.List;
+
+public interface AiFaqCategoryService extends BaseInterface<AiFaqCategory, AiFaqCategoryQueryVO> {
+
+    void createCategory(AiFaqCategory entity);
+
+    void updateCategory(AiFaqCategory entity);
+
+    /**
+     * 按主键批量查询 FAQ 分类。
+     */
+    List<AiFaqCategory> listByIds(List<String> ids);
+
+    /**
+     * 按分类名称模糊匹配,返回分类主键列表(供 FAQ 列表筛选)。
+     */
+    List<String> findIdsByCategoryName(String categoryName);
+}

+ 15 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/AiFaqService.java

@@ -0,0 +1,15 @@
+package com.jeesharp.business.modules.ai.service;
+
+import com.jeesharp.business.modules.ai.entity.AiFaq;
+import com.jeesharp.business.modules.ai.vo.AiFaqQueryVO;
+import com.jeesharp.framework.mybatis.core.service.BaseInterface;
+
+public interface AiFaqService extends BaseInterface<AiFaq, AiFaqQueryVO> {
+
+    void createFaq(AiFaq entity);
+
+    void updateFaq(AiFaq entity);
+AiFaq matchFaq(String keyword);
+
+    AiFaq matchFaq(String categoryId, String keyword);
+}

+ 32 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/impl/AiChatFeedbackServiceImpl.java

@@ -0,0 +1,32 @@
+package com.jeesharp.business.modules.ai.service.impl;
+
+import com.jeesharp.business.modules.ai.entity.AiChatFeedback;
+import com.jeesharp.business.modules.ai.mapper.AiChatFeedbackMapper;
+import com.jeesharp.business.modules.ai.service.AiChatFeedbackService;
+import com.jeesharp.business.modules.common.constants.PlatformTenantConstants;
+import com.jeesharp.business.modules.ai.vo.AiChatFeedbackQueryVO;
+import com.jeesharp.framework.mybatis.core.service.CrudService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional(readOnly = true)
+public class AiChatFeedbackServiceImpl extends CrudService<AiChatFeedbackMapper, AiChatFeedback, AiChatFeedbackQueryVO>
+        implements AiChatFeedbackService {
+
+    @Override
+    @Transactional(readOnly = false)
+    public void createFeedback(AiChatFeedback entity) {
+        entity.closeTenantCodeAutoSet();
+        entity.setTenantCode(PlatformTenantConstants.PLATFORM_TENANT_CODE);
+        entity.setTenantId(PlatformTenantConstants.PLATFORM_TENANT_ID);
+        save(entity);
+    }
+
+    @Override
+    @Transactional(readOnly = false)
+    public void updateFeedback(AiChatFeedback entity) {
+        update(entity);
+    }
+
+}

+ 92 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/impl/AiChatMessageServiceImpl.java

@@ -0,0 +1,92 @@
+package com.jeesharp.business.modules.ai.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.jeesharp.business.modules.ai.entity.AiChatMessage;
+import com.jeesharp.business.modules.ai.mapper.AiChatMessageMapper;
+import com.jeesharp.business.modules.ai.service.AiChatMessageService;
+import com.jeesharp.business.modules.ai.vo.AiChatMessageQueryVO;
+import com.jeesharp.framework.mybatis.core.service.CrudService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * AI 会话消息领域服务实现。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Service
+@Transactional(readOnly = true)
+public class AiChatMessageServiceImpl extends CrudService<AiChatMessageMapper, AiChatMessage, AiChatMessageQueryVO>
+        implements AiChatMessageService {
+
+    private static final int MESSAGE_PREVIEW_MAX_LEN = 80;
+
+    @Override
+    @Transactional(readOnly = false)
+    public void createMessage(AiChatMessage entity) {
+        save(entity);
+    }
+
+    @Override
+    @Transactional(readOnly = false)
+    public void updateMessage(AiChatMessage entity) {
+        update(entity);
+    }
+
+    @Override
+    public List<String> findIdsByContentLike(String keyword) {
+        if (StrUtil.isBlank(keyword)) {
+            return List.of();
+        }
+        LambdaQueryWrapper<AiChatMessage> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AiChatMessage::getDelFlag, "0");
+        wrapper.like(AiChatMessage::getMessageContent, keyword.trim());
+        return getBaseMapper().selectList(wrapper).stream()
+            .map(AiChatMessage::getId)
+            .filter(StrUtil::isNotBlank)
+            .distinct()
+            .collect(Collectors.toList());
+    }
+
+    @Override
+    public Map<String, String> mapMessageContentPreviewByIds(List<String> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return Collections.emptyMap();
+        }
+        List<String> distinctIds = ids.stream().filter(StrUtil::isNotBlank).distinct().toList();
+        if (distinctIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        AiChatMessageQueryVO query = new AiChatMessageQueryVO();
+        query.setIdIn(distinctIds);
+        Map<String, String> map = new HashMap<>();
+        for (AiChatMessage message : findList(query)) {
+            if (message == null || StrUtil.isBlank(message.getId())) {
+                continue;
+            }
+            map.put(message.getId(), truncatePreview(message.getMessageContent()));
+        }
+        return map;
+    }
+
+    private static String truncatePreview(String content) {
+        if (StrUtil.isBlank(content)) {
+            return "";
+        }
+        String trimmed = content.trim();
+        if (trimmed.length() <= MESSAGE_PREVIEW_MAX_LEN) {
+            return trimmed;
+        }
+        return trimmed.substring(0, MESSAGE_PREVIEW_MAX_LEN) + "…";
+    }
+}

+ 88 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/impl/AiChatSessionServiceImpl.java

@@ -0,0 +1,88 @@
+package com.jeesharp.business.modules.ai.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.jeesharp.business.modules.ai.entity.AiChatSession;
+import com.jeesharp.business.modules.ai.mapper.AiChatSessionMapper;
+import com.jeesharp.business.modules.ai.service.AiChatSessionService;
+import com.jeesharp.business.modules.ai.vo.AiChatSessionQueryVO;
+import com.jeesharp.framework.mybatis.core.service.CrudService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * AI 会话领域服务实现。
+ *
+ * @author jeesharp
+ * @since 2026-06-04
+ */
+@Service
+@Transactional(readOnly = true)
+public class AiChatSessionServiceImpl extends CrudService<AiChatSessionMapper, AiChatSession, AiChatSessionQueryVO>
+        implements AiChatSessionService {
+
+    @Override
+    @Transactional(readOnly = false)
+    public void createSession(AiChatSession entity) {
+        save(entity);
+    }
+
+    @Override
+    @Transactional(readOnly = false)
+    public void updateSession(AiChatSession entity) {
+        update(entity);
+    }
+
+    @Override
+    public List<String> findIdsByTitleOrCodeLike(String keyword) {
+        if (StrUtil.isBlank(keyword)) {
+            return List.of();
+        }
+        String trimmed = keyword.trim();
+        LambdaQueryWrapper<AiChatSession> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AiChatSession::getDelFlag, "0");
+        wrapper.and(w -> w.like(AiChatSession::getSessionTitle, trimmed)
+            .or().like(AiChatSession::getSessionCode, trimmed));
+        return getBaseMapper().selectList(wrapper).stream()
+            .map(AiChatSession::getId)
+            .filter(StrUtil::isNotBlank)
+            .distinct()
+            .collect(Collectors.toList());
+    }
+
+    @Override
+    public Map<String, String> mapSessionLabelByIds(List<String> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return Collections.emptyMap();
+        }
+        List<String> distinctIds = ids.stream().filter(StrUtil::isNotBlank).distinct().toList();
+        if (distinctIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        AiChatSessionQueryVO query = new AiChatSessionQueryVO();
+        query.setIdIn(distinctIds);
+        Map<String, String> map = new HashMap<>();
+        for (AiChatSession session : findList(query)) {
+            if (session == null || StrUtil.isBlank(session.getId())) {
+                continue;
+            }
+            map.put(session.getId(), resolveSessionLabel(session));
+        }
+        return map;
+    }
+
+    private static String resolveSessionLabel(AiChatSession session) {
+        if (StrUtil.isNotBlank(session.getSessionTitle())) {
+            return session.getSessionTitle();
+        }
+        return StrUtil.blankToDefault(session.getSessionCode(), session.getId());
+    }
+}

+ 62 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/impl/AiFaqCategoryServiceImpl.java

@@ -0,0 +1,62 @@
+package com.jeesharp.business.modules.ai.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.jeesharp.business.modules.ai.entity.AiFaqCategory;
+import com.jeesharp.business.modules.ai.mapper.AiFaqCategoryMapper;
+import com.jeesharp.business.modules.ai.service.AiFaqCategoryService;
+import com.jeesharp.business.modules.ai.vo.AiFaqCategoryQueryVO;
+import com.jeesharp.framework.mybatis.core.service.CrudService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@Transactional(readOnly = true)
+public class AiFaqCategoryServiceImpl extends CrudService<AiFaqCategoryMapper, AiFaqCategory, AiFaqCategoryQueryVO>
+        implements AiFaqCategoryService {
+
+    @Override
+    @Transactional(readOnly = false)
+    public void createCategory(AiFaqCategory entity) {
+        save(entity);
+    }
+
+    @Override
+    @Transactional(readOnly = false)
+    public void updateCategory(AiFaqCategory entity) {
+        update(entity);
+    }
+
+    @Override
+    public List<AiFaqCategory> listByIds(List<String> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return List.of();
+        }
+        List<String> idList = ids.stream()
+                .filter(StrUtil::isNotBlank)
+                .map(String::trim)
+                .distinct()
+                .toList();
+        if (idList.isEmpty()) {
+            return List.of();
+        }
+        return getBaseMapper().selectList(new LambdaQueryWrapper<AiFaqCategory>().in(AiFaqCategory::getId, idList));
+    }
+
+    @Override
+    public List<String> findIdsByCategoryName(String categoryName) {
+        if (StrUtil.isBlank(categoryName)) {
+            return List.of();
+        }
+        AiFaqCategoryQueryVO query = new AiFaqCategoryQueryVO();
+        query.setCategoryName(categoryName.trim());
+        return findList(query).stream()
+                .map(AiFaqCategory::getId)
+                .filter(StrUtil::isNotBlank)
+                .distinct()
+                .toList();
+    }
+
+}

+ 72 - 0
jeesharp-business/business-ai/business-ai-service/src/main/java/com/jeesharp/business/modules/ai/service/impl/AiFaqServiceImpl.java

@@ -0,0 +1,72 @@
+package com.jeesharp.business.modules.ai.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.jeesharp.business.modules.ai.entity.AiFaq;
+import com.jeesharp.business.modules.ai.mapper.AiFaqMapper;
+import com.jeesharp.business.modules.ai.service.AiFaqService;
+import com.jeesharp.business.modules.ai.vo.AiFaqQueryVO;
+import com.jeesharp.framework.mybatis.core.service.CrudService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional(readOnly = true)
+public class AiFaqServiceImpl extends CrudService<AiFaqMapper, AiFaq, AiFaqQueryVO>
+        implements AiFaqService {
+
+    @Override
+    @Transactional(readOnly = false)
+    public void createFaq(AiFaq entity) {
+        save(entity);
+    }
+
+    @Override
+    @Transactional(readOnly = false)
+    public void updateFaq(AiFaq entity) {
+        update(entity);
+    }
+
+    @Override
+    public AiFaq matchFaq(String keyword) {
+        return matchFaq(null, keyword);
+    }
+
+    @Override
+    public AiFaq matchFaq(String categoryId, String keyword) {
+        if (StrUtil.isBlank(keyword)) {
+            return null;
+        }
+        String trimmed = keyword.trim();
+        AiFaqQueryVO query = new AiFaqQueryVO();
+        query.setCategoryId(categoryId);
+        query.setStatus(1);
+        AiFaq keywordHit = null;
+        for (AiFaq faq : findList(query)) {
+            if (StrUtil.containsIgnoreCase(faq.getQuestion(), trimmed)) {
+                return faq;
+            }
+            if (keywordHit == null && StrUtil.containsIgnoreCase(StrUtil.nullToEmpty(faq.getKeywords()), trimmed)) {
+                keywordHit = faq;
+            }
+            if (keywordHit == null && matchUserMessageContainsFaqKeyword(trimmed, faq.getKeywords())) {
+                keywordHit = faq;
+            }
+        }
+        return keywordHit;
+    }
+
+    /**
+     * 用户输入包含 FAQ 关键词(逗号分隔)时命中,便于「我想问门票」类自然语言。
+     */
+    private boolean matchUserMessageContainsFaqKeyword(String userMessage, String keywords) {
+        if (StrUtil.isBlank(keywords)) {
+            return false;
+        }
+        for (String token : StrUtil.splitTrim(keywords, ',')) {
+            if (StrUtil.isNotBlank(token) && StrUtil.containsIgnoreCase(userMessage, token)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

+ 29 - 0
jeesharp-business/business-ai/pom.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.xinghuacuntravel</groupId>
+        <artifactId>jeesharp-business</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>business-ai</artifactId>
+    <packaging>pom</packaging>
+    <name>business-ai</name>
+    <description>AI客服模块</description>
+
+    <properties>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <modules>
+        <module>business-ai-bean</module>
+        <module>business-ai-service</module>
+        <module>business-ai-application</module>
+        <module>business-ai-controller</module>
+    </modules>
+</project>

+ 70 - 0
jeesharp-business/business-common/README.md

@@ -0,0 +1,70 @@
+# business-common 公共业务模块
+
+## 模块概述
+
+提供跨业态复用的公共能力。当前首期实现 **业务业态场景注册表**(`cfg_biz_scene`),用于统一维护场景编号、管理端选品元数据与扩展 JSON(小程序路径等)。
+
+> 设计文档:`xinghuacun_travel_doc/architecture/业务业态场景注册表设计.md`
+
+## 模块结构
+
+```
+business-common/
+├── business-common-api/          # BizSceneApi 跨模块契约
+├── business-common-bean/         # 含 BizScenePickerSupport、BizSceneCouponLegacySupport 等
+├── business-common-service/
+├── business-common-application/
+└── business-common-controller/
+```
+
+### 扩展解析(bean/support)
+
+| 类 | 用途 |
+|----|------|
+| `BizScenePickerSupport` | `ext_json.pickers`(`COUPON_SCOPE` / `BANNER_TARGET`)覆盖 `admin_list_url` |
+| `BizSceneCouponLegacySupport` | 券 legacy `scene_code` ↔ 注册表 canonical 映射 |
+| `BizSceneMobileJumpSupport` | `ext_json.mobile` 小程序路径 |
+| `BizSceneAdminListRecordSupport` | 管理端分页列表 `records` 解析 |
+
+### 框架枚举(FRAME)
+
+| 类 | 用途 |
+|----|------|
+| `IntValueEnum` / `StringValueEnum` | 整型/字符串存库枚举标记接口 |
+| `EnumValueSupport` | `fromValue` / `getDescByValue` 统一实现 |
+| `ShelfStatusEnum`、`VerticalPayStatusEnum` 等 | 跨业态复用枚举,均实现上述接口 |
+
+## 数据库
+
+| 脚本 | 说明 |
+|------|------|
+| `doc/sql/business-common/cfg_biz_scene_schema.sql` | 建表 |
+| `doc/sql/business-common/cfg_biz_scene_seed.sql` | 种子数据(节选) |
+| `doc/sql/business-common/cfg_biz_scene_menu_permission.sql` | 菜单与权限 |
+| `doc/sql/business-common/cfg_biz_scene_upgrade_existing.sql` | **存量库升级顺序**(联调前置) |
+
+## 管理端接口
+
+前缀:`${jeesharp.web.adminPath}/common/bizScene/v1`
+
+| 操作 | 方法 | URL | 权限 |
+|------|------|-----|------|
+| 分页列表 | POST | `list` | `manager:business:common:bizScene:list` |
+| 详情 | GET | `info?id=` | `manager:business:common:bizScene:info` |
+| 新增 | POST | `create` | `manager:business:common:bizScene:create` |
+| 更新 | POST | `update` | `manager:business:common:bizScene:update` |
+| 删除 | GET | `delete?ids=` | `manager:business:common:bizScene:delete` |
+| 启用选项 | GET | `enabledOptions?capability=` | `manager:business:common:bizScene:list` |
+
+`capability` 可选值:`COUPON`、`BANNER`、`NAV`。
+
+## 版本记录
+
+| 版本 | 日期 | 说明 |
+|------|------|------|
+| 1.5 | 2026-07-03 | E:`biz_coupon_scene` 退役种子;轮播 `create`/`update` 拆分;前端删除 presets |
+| 1.4 | 2026-07-03 | ARCH:`FoodFinanceReadApi`/`HotelFinanceReadApi` 上收 `-api`;PAY:scenic/hotel `*BookingPayAssembler` |
+| 1.3 | 2026-07-03 | ARCH:`ScenicFinanceReadApi` 上收 `scenic-api`;PAY 退款:`BookingRefundTailSupport` 商城售后接入 |
+| 1.2 | 2026-07-03 | FRAME:`IntValueEnum`/`EnumValueSupport`;PAY:`VerticalPayPrepareSupport`(pay-bean) |
+| 1.1 | 2026-07-03 | `BizSceneApi`、Picker 解析;券/轮播桥接 |
+| 1.0 | 2026-07-03 | 首期:`cfg_biz_scene` CRUD + `enabledOptions` |

+ 23 - 0
jeesharp-business/business-common/business-common-api/pom.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.xinghuacuntravel</groupId>
+        <artifactId>business-common</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>business-common-api</artifactId>
+    <name>business-common-api</name>
+    <description>公共业务对外 API(业务业态场景等跨模块契约)</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.xinghuacuntravel</groupId>
+            <artifactId>business-common-bean</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 29 - 0
jeesharp-business/business-common/business-common-api/src/main/java/com/jeesharp/business/modules/common/api/BizSceneApi.java

@@ -0,0 +1,29 @@
+package com.jeesharp.business.modules.common.api;
+
+import com.jeesharp.business.modules.common.bizscene.vo.BizSceneOptionVO;
+
+import java.util.List;
+
+/**
+ * 业务业态场景对外 API:跨模块校验场景编号、查询启用场景选项。
+ *
+ * @author jeesharp
+ * @since 2026-07-03
+ */
+public interface BizSceneApi {
+
+    /**
+     * 断言场景在指定租户下已启用;未配置或已禁用时抛出业务异常。
+     */
+    void assertSceneEnabled(String sceneCode, String tenantId);
+
+    /**
+     * 按场景编号查询启用中的场景选项;不存在或未启用时返回 {@code null}。
+     */
+    BizSceneOptionVO getOptionByCode(String sceneCode, String tenantId);
+
+    /**
+     * 按能力标签查询启用场景选项(如 {@code COUPON}、{@code BANNER}、{@code NAV})。
+     */
+    List<BizSceneOptionVO> listEnabledOptions(String capability, String tenantId);
+}

+ 39 - 0
jeesharp-business/business-common/business-common-application/pom.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.xinghuacuntravel</groupId>
+        <artifactId>business-common</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>business-common-application</artifactId>
+    <name>business-common-application</name>
+    <description>公共业务模块 - Application 层</description>
+
+    <properties>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.xinghuacuntravel</groupId>
+            <artifactId>business-common-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.xinghuacuntravel</groupId>
+            <artifactId>business-common-service</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jeesharp.boot</groupId>
+            <artifactId>jeesharp-core</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 37 - 0
jeesharp-business/business-common/business-common-application/src/main/java/com/jeesharp/business/modules/common/application/api/BizSceneApiImpl.java

@@ -0,0 +1,37 @@
+package com.jeesharp.business.modules.common.application.api;
+
+import com.jeesharp.business.modules.common.api.BizSceneApi;
+import com.jeesharp.business.modules.common.bizscene.application.BizSceneApplicationService;
+import com.jeesharp.business.modules.common.bizscene.vo.BizSceneOptionVO;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * {@link BizSceneApi} 实现,委托 {@link BizSceneApplicationService}。
+ *
+ * @author jeesharp
+ * @since 2026-07-03
+ */
+@Service
+@AllArgsConstructor
+public class BizSceneApiImpl implements BizSceneApi {
+
+    private final BizSceneApplicationService bizSceneApplicationService;
+
+    @Override
+    public void assertSceneEnabled(String sceneCode, String tenantId) {
+        bizSceneApplicationService.assertSceneEnabled(sceneCode, tenantId);
+    }
+
+    @Override
+    public BizSceneOptionVO getOptionByCode(String sceneCode, String tenantId) {
+        return bizSceneApplicationService.getOptionByCode(sceneCode, tenantId);
+    }
+
+    @Override
+    public List<BizSceneOptionVO> listEnabledOptions(String capability, String tenantId) {
+        return bizSceneApplicationService.listEnabledOptions(capability, tenantId);
+    }
+}

+ 119 - 0
jeesharp-business/business-common/business-common-application/src/main/java/com/jeesharp/business/modules/common/bizscene/application/BizSceneApplicationService.java

@@ -0,0 +1,119 @@
+package com.jeesharp.business.modules.common.bizscene.application;
+
+import cn.hutool.core.util.StrUtil;
+import com.jeesharp.business.modules.common.bizscene.convert.BizSceneConvert;
+import com.jeesharp.business.modules.common.bizscene.dto.BizSceneSaveDTO;
+import com.jeesharp.business.modules.common.bizscene.entity.BizScene;
+import com.jeesharp.business.modules.common.bizscene.enums.BizSceneGroupEnum;
+import com.jeesharp.business.modules.common.bizscene.enums.BizSceneStatusEnum;
+import com.jeesharp.business.modules.common.bizscene.service.BizSceneService;
+import com.jeesharp.business.modules.common.bizscene.vo.BizSceneInfoVO;
+import com.jeesharp.business.modules.common.bizscene.vo.BizSceneListVO;
+import com.jeesharp.business.modules.common.bizscene.vo.BizSceneOptionVO;
+import com.jeesharp.business.modules.common.bizscene.vo.BizSceneQueryVO;
+import com.jeesharp.framework.exception.BizErrorException;
+import com.jeesharp.framework.pojo.Page;
+import com.jeesharp.framework.pojo.query.PageRespVO;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 业务业态场景应用服务:管理端 CRUD 与启用场景选项查询。
+ *
+ * @author jeesharp
+ * @since 2026-07-03
+ */
+@Service
+@AllArgsConstructor
+@Transactional(readOnly = true)
+public class BizSceneApplicationService {
+
+    private final BizSceneService bizSceneService;
+
+    public BizSceneInfoVO get(String id) {
+        BizScene entity = bizSceneService.get(id);
+        if (entity == null) {
+            throw new BizErrorException("业务场景不存在");
+        }
+        BizSceneInfoVO vo = BizSceneConvert.INSTANCE.toInfoVO(entity);
+        fillDisplay(vo);
+        return vo;
+    }
+
+    public PageRespVO<BizSceneListVO> findPage(Page<BizScene> page, BizSceneQueryVO query) {
+        Page<BizScene> result = bizSceneService.findPage(page, query);
+        PageRespVO<BizSceneListVO> resp = BizSceneConvert.INSTANCE.toPageRespVO(result);
+        if (resp.getRecords() != null) {
+            resp.getRecords().forEach(this::fillDisplay);
+        }
+        return resp;
+    }
+
+    public List<BizSceneOptionVO> listEnabledOptions(String capability, String tenantId) {
+        return bizSceneService.listEnabled(tenantId, capability).stream()
+                .map(this::toOption)
+                .collect(Collectors.toList());
+    }
+
+    public BizSceneOptionVO getOptionByCode(String sceneCode, String tenantId) {
+        BizScene scene = bizSceneService.findBySceneCode(sceneCode, tenantId);
+        if (scene == null || !BizSceneStatusEnum.ENABLED.getValue().equals(scene.getStatus())) {
+            return null;
+        }
+        return toOption(scene);
+    }
+
+    @Transactional(readOnly = false)
+    public void create(BizSceneSaveDTO dto) {
+        if (StrUtil.isNotBlank(dto.getId())) {
+            throw new BizErrorException("新增请求不应携带主键 id");
+        }
+        bizSceneService.createBizScene(BizSceneConvert.INSTANCE.toEntity(dto));
+    }
+
+    @Transactional(readOnly = false)
+    public void update(BizSceneSaveDTO dto) {
+        if (StrUtil.isBlank(dto.getId())) {
+            throw new BizErrorException("更新请求必须携带主键 id");
+        }
+        bizSceneService.updateBizScene(BizSceneConvert.INSTANCE.toEntity(dto));
+    }
+
+    @Transactional(readOnly = false)
+    public void delete(String ids) {
+        bizSceneService.batchDelete(Arrays.asList(ids.split(",")));
+    }
+
+    public void assertSceneEnabled(String sceneCode, String tenantId) {
+        bizSceneService.assertSceneEnabled(sceneCode, tenantId);
+    }
+
+    private BizSceneOptionVO toOption(BizScene scene) {
+        BizSceneOptionVO vo = new BizSceneOptionVO();
+        vo.setSceneCode(scene.getSceneCode());
+        vo.setSceneName(scene.getSceneName());
+        vo.setSceneGroup(scene.getSceneGroup());
+        vo.setAdminListUrl(scene.getAdminListUrl());
+        vo.setKeywordField(scene.getKeywordField());
+        vo.setNameField(scene.getNameField());
+        vo.setCoverField(scene.getCoverField());
+        vo.setCapabilities(scene.getCapabilities());
+        vo.setExtJson(scene.getExtJson());
+        return vo;
+    }
+
+    private void fillDisplay(BizSceneInfoVO vo) {
+        vo.setStatusName(BizSceneStatusEnum.getDescByValue(vo.getStatus()));
+        vo.setSceneGroupName(BizSceneGroupEnum.getDescByCode(vo.getSceneGroup()));
+    }
+
+    private void fillDisplay(BizSceneListVO vo) {
+        vo.setStatusName(BizSceneStatusEnum.getDescByValue(vo.getStatus()));
+        vo.setSceneGroupName(BizSceneGroupEnum.getDescByCode(vo.getSceneGroup()));
+    }
+}

+ 29 - 0
jeesharp-business/business-common/business-common-bean/pom.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.xinghuacuntravel</groupId>
+        <artifactId>business-common</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>business-common-bean</artifactId>
+    <name>business-common-bean</name>
+    <description>公共业务模块 - Bean 层</description>
+
+    <properties>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jeesharp.boot</groupId>
+            <artifactId>jeesharp-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 22 - 0
jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/convert/BizSceneConvert.java

@@ -0,0 +1,22 @@
+package com.jeesharp.business.modules.common.bizscene.convert;
+
+import com.jeesharp.business.modules.common.bizscene.dto.BizSceneSaveDTO;
+import com.jeesharp.business.modules.common.bizscene.entity.BizScene;
+import com.jeesharp.business.modules.common.bizscene.vo.BizSceneInfoVO;
+import com.jeesharp.business.modules.common.bizscene.vo.BizSceneListVO;
+import com.jeesharp.business.modules.common.bizscene.vo.BizSceneQueryVO;
+import com.jeesharp.framework.common.pojo.convert.BasePojoConvert;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * 业务业态场景 MapStruct 转换器。
+ *
+ * @author jeesharp
+ * @since 2026-07-03
+ */
+@Mapper
+public interface BizSceneConvert extends BasePojoConvert<BizScene, BizSceneSaveDTO, BizSceneInfoVO, BizSceneListVO, BizSceneQueryVO> {
+
+    BizSceneConvert INSTANCE = Mappers.getMapper(BizSceneConvert.class);
+}

+ 59 - 0
jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/dto/BizSceneSaveDTO.java

@@ -0,0 +1,59 @@
+package com.jeesharp.business.modules.common.bizscene.dto;
+
+import com.jeesharp.framework.pojo.dto.BaseSaveDTO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 业务业态场景保存 DTO。
+ *
+ * @author jeesharp
+ * @since 2026-07-03
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "业务业态场景保存")
+public class BizSceneSaveDTO extends BaseSaveDTO {
+
+    @NotBlank(message = "场景名称不能为空")
+    @Schema(description = "场景名称", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String sceneName;
+
+    @NotBlank(message = "场景编号不能为空")
+    @Schema(description = "场景编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String sceneCode;
+
+    @Schema(description = "场景分组:SCENIC/STAY/CONTENT/MALL")
+    private String sceneGroup;
+
+    @Schema(description = "默认管理端选目标 POST 分页路径")
+    private String adminListUrl;
+
+    @Schema(description = "列表搜索参数字段名")
+    private String keywordField;
+
+    @Schema(description = "列表展示名称字段")
+    private String nameField;
+
+    @Schema(description = "封面字段")
+    private String coverField;
+
+    @Schema(description = "排序")
+    private Integer sort;
+
+    @NotNull(message = "状态不能为空")
+    @Schema(description = "状态:0-禁用 1-启用", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Integer status;
+
+    @Schema(description = "能力标签,逗号分隔:COUPON,BANNER,NAV")
+    private String capabilities;
+
+    @Schema(description = "扩展 JSON")
+    private String extJson;
+
+    @Schema(description = "备注")
+    private String remarks;
+}

+ 60 - 0
jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/entity/BizScene.java

@@ -0,0 +1,60 @@
+package com.jeesharp.business.modules.common.bizscene.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.jeesharp.framework.mybatis.core.dataobject.BaseEntity;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 业务业态场景注册实体,映射表 {@code cfg_biz_scene}。
+ * <p>供优惠券、轮播、导航等消费方统一引用场景编号与选品元数据。</p>
+ *
+ * @author jeesharp
+ * @since 2026-07-03
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("cfg_biz_scene")
+@Schema(description = "业务业态场景")
+public class BizScene extends BaseEntity<BizScene> {
+
+    @Schema(description = "场景名称")
+    private String sceneName;
+
+    @Schema(description = "场景编号(租户内唯一)")
+    private String sceneCode;
+
+    /**
+     * @see com.jeesharp.business.modules.common.bizscene.enums.BizSceneGroupEnum
+     */
+    @Schema(description = "场景分组:SCENIC/STAY/CONTENT/MALL")
+    private String sceneGroup;
+
+    @Schema(description = "默认管理端选目标 POST 分页路径")
+    private String adminListUrl;
+
+    @Schema(description = "列表搜索参数字段名")
+    private String keywordField;
+
+    @Schema(description = "列表展示名称字段")
+    private String nameField;
+
+    @Schema(description = "封面字段(可选)")
+    private String coverField;
+
+    @Schema(description = "排序")
+    private Integer sort;
+
+    /**
+     * @see com.jeesharp.business.modules.common.bizscene.enums.BizSceneStatusEnum
+     */
+    @Schema(description = "状态:0-禁用 1-启用")
+    private Integer status;
+
+    @Schema(description = "能力标签,逗号分隔:COUPON,BANNER,NAV")
+    private String capabilities;
+
+    @Schema(description = "扩展 JSON:mobile/coupon/banner/pickers 等")
+    private String extJson;
+}

+ 17 - 0
jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/enums/BizSceneCapabilityEnum.java

@@ -0,0 +1,17 @@
+package com.jeesharp.business.modules.common.bizscene.enums;
+
+/**
+ * 业务业态场景能力标签常量(存于 {@code capabilities} 逗号分隔字段)。
+ *
+ * @author jeesharp
+ * @since 2026-07-03
+ */
+public final class BizSceneCapabilityEnum {
+
+    public static final String COUPON = "COUPON";
+    public static final String BANNER = "BANNER";
+    public static final String NAV = "NAV";
+
+    private BizSceneCapabilityEnum() {
+    }
+}

+ 41 - 0
jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/enums/BizSceneGroupEnum.java

@@ -0,0 +1,41 @@
+package com.jeesharp.business.modules.common.bizscene.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 业务业态场景分组枚举,用于管理端下拉分组展示。
+ *
+ * @author jeesharp
+ * @since 2026-07-03
+ */
+@Getter
+@AllArgsConstructor
+public enum BizSceneGroupEnum {
+
+    SCENIC("SCENIC", "景区与导览"),
+    STAY("STAY", "住宿与休闲"),
+    CONTENT("CONTENT", "内容与活动"),
+    MALL("MALL", "商城与优惠");
+
+    private final String code;
+    private final String desc;
+
+    public static BizSceneGroupEnum fromCode(String code) {
+        if (code == null || code.isBlank()) {
+            return null;
+        }
+        String c = code.trim();
+        for (BizSceneGroupEnum item : values()) {
+            if (item.code.equalsIgnoreCase(c)) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    public static String getDescByCode(String code) {
+        BizSceneGroupEnum item = fromCode(code);
+        return item != null ? item.getDesc() : "";
+    }
+}

+ 31 - 0
jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/enums/BizSceneStatusEnum.java

@@ -0,0 +1,31 @@
+package com.jeesharp.business.modules.common.bizscene.enums;
+
+import com.jeesharp.business.modules.common.enums.support.EnumValueSupport;
+import com.jeesharp.business.modules.common.enums.support.IntValueEnum;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 业务业态场景状态枚举。
+ *
+ * @author jeesharp
+ * @since 2026-07-03
+ */
+@Getter
+@AllArgsConstructor
+public enum BizSceneStatusEnum implements IntValueEnum {
+
+    DISABLED(0, "禁用"),
+    ENABLED(1, "启用");
+
+    private final Integer value;
+    private final String desc;
+
+    public static BizSceneStatusEnum fromValue(Integer value) {
+        return EnumValueSupport.fromIntValue(BizSceneStatusEnum.class, value);
+    }
+
+    public static String getDescByValue(Integer value) {
+        return EnumValueSupport.descByIntValue(BizSceneStatusEnum.class, value);
+    }
+}

+ 152 - 0
jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/support/BizSceneAdminListRecordSupport.java

@@ -0,0 +1,152 @@
+package com.jeesharp.business.modules.common.bizscene.support;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import lombok.Getter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 解析管理端分页列表接口响应({@code AjaxResult.data.records}),供轮播/优惠券等按注册表 URL 选目标。
+ *
+ * @author jeesharp
+ * @since 2026-07-03
+ */
+public final class BizSceneAdminListRecordSupport {
+
+    private static final String[] NAME_FALLBACK_FIELDS = {
+            "spuName", "name", "headline", "productName", "packageName", "specLabel", "title", "programName"
+    };
+
+    private BizSceneAdminListRecordSupport() {
+    }
+
+    public static String buildListRequestBody(String keywordField, String keyword, int pageSize) {
+        JSONObject body = new JSONObject();
+        body.set("pageNo", 1);
+        body.set("pageSize", pageSize);
+        body.set("status", 1);
+        if (StrUtil.isNotBlank(keyword) && StrUtil.isNotBlank(keywordField)) {
+            body.set(keywordField.trim(), keyword.trim());
+        }
+        return body.toString();
+    }
+
+    public static List<AdminListRow> parseRows(String responseBody, String nameField) {
+        if (StrUtil.isBlank(responseBody)) {
+            return List.of();
+        }
+        JSONObject root;
+        try {
+            root = JSONUtil.parseObj(responseBody);
+        } catch (Exception ex) {
+            return List.of();
+        }
+        if (root.getInt("code", 0) != 200) {
+            return List.of();
+        }
+        JSONObject data = root.getJSONObject("data");
+        if (data == null) {
+            return List.of();
+        }
+        JSONArray records = data.getJSONArray("records");
+        if (records == null || records.isEmpty()) {
+            return List.of();
+        }
+        List<AdminListRow> out = new ArrayList<>();
+        for (int i = 0; i < records.size(); i++) {
+            JSONObject row = records.getJSONObject(i);
+            if (row == null) {
+                continue;
+            }
+            String id = row.getStr("id");
+            if (StrUtil.isBlank(id)) {
+                continue;
+            }
+            String label = resolveName(row, nameField);
+            out.add(new AdminListRow(id.trim(), label));
+        }
+        return out;
+    }
+
+    /**
+     * 解析管理端详情接口 {@code AjaxResult.data}。
+     */
+    public static JSONObject parseInfoData(String responseBody) {
+        if (StrUtil.isBlank(responseBody)) {
+            return null;
+        }
+        try {
+            JSONObject root = JSONUtil.parseObj(responseBody);
+            if (root.getInt("code", 0) != 200) {
+                return null;
+            }
+            return root.getJSONObject("data");
+        } catch (Exception ex) {
+            return null;
+        }
+    }
+
+    /**
+     * 详情行是否处于可跳转的上线/启用态(兼容 status / publishStatus / shelfStatus 等字段)。
+     */
+    public static boolean isSelectableRow(JSONObject row) {
+        if (row == null || row.isEmpty()) {
+            return false;
+        }
+        if (row.containsKey("status")) {
+            Integer status = row.getInt("status");
+            return status != null && status == 1;
+        }
+        if (row.containsKey("publishStatus")) {
+            Integer publishStatus = row.getInt("publishStatus");
+            return publishStatus != null && publishStatus == 1;
+        }
+        if (row.containsKey("shelfStatus")) {
+            Integer shelfStatus = row.getInt("shelfStatus");
+            return shelfStatus != null && shelfStatus == 1;
+        }
+        return true;
+    }
+
+    public static String resolveRowLabel(JSONObject row, String nameField) {
+        if (row == null) {
+            return "";
+        }
+        return resolveName(row, nameField);
+    }
+
+    private static String resolveName(JSONObject row, String nameField) {
+        if (StrUtil.isNotBlank(nameField)) {
+            String v = row.getStr(nameField.trim());
+            if (StrUtil.isNotBlank(v)) {
+                return v.trim();
+            }
+        }
+        for (String field : NAME_FALLBACK_FIELDS) {
+            String v = row.getStr(field);
+            if (StrUtil.isNotBlank(v)) {
+                return v.trim();
+            }
+        }
+        return row.getStr("id");
+    }
+
+  /**
+     * 管理端列表行(id + 展示名)。
+     */
+    @Getter
+    public static final class AdminListRow {
+
+        private final String id;
+        private final String label;
+
+        public AdminListRow(String id, String label) {
+            this.id = id;
+            this.label = label;
+        }
+    }
+}

+ 130 - 0
jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/support/BizSceneCouponLegacySupport.java

@@ -0,0 +1,130 @@
+package com.jeesharp.business.modules.common.bizscene.support;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.jeesharp.business.modules.common.bizscene.vo.BizSceneOptionVO;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 优惠券 legacy {@code scene_code} 与 {@code cfg_biz_scene.scene_code} 双向映射及 ext_json 解析。
+ *
+ * @author jeesharp
+ * @since 2026-07-03
+ */
+public final class BizSceneCouponLegacySupport {
+
+  private static final Map<String, String> LEGACY_TO_CANONICAL;
+
+  static {
+    Map<String, String> map = new LinkedHashMap<>();
+    map.put("MALL", "MALL_SPU");
+    map.put("HOTEL", "HOTEL");
+    map.put("FOOD", "FOOD_PACKAGE");
+    map.put("SCENIC", "TICKET_VENUE");
+    map.put("STUDY", "STUDY_PLAN");
+    map.put("YEARCARD", "YEARCARD");
+    map.put("FOOD_MEAL", "FOOD_RESTAURANT");
+    LEGACY_TO_CANONICAL = Collections.unmodifiableMap(map);
+  }
+
+  private BizSceneCouponLegacySupport() {
+  }
+
+  /**
+   * 将优惠券模板/会员券上的 legacy 或 canonical 编号解析为注册表 canonical {@code scene_code}。
+   */
+  public static String resolveCanonicalSceneCode(String sceneCode) {
+    if (StrUtil.isBlank(sceneCode)) {
+      return null;
+    }
+    String normalized = sceneCode.trim().toUpperCase();
+    return LEGACY_TO_CANONICAL.getOrDefault(normalized, normalized);
+  }
+
+  /**
+   * 将注册表场景转为优惠券模块对外使用的 legacy {@code scene_code}(模板表单、历史数据兼容)。
+   */
+  public static String toCouponLegacySceneCode(BizSceneOptionVO scene) {
+    if (scene == null) {
+      return null;
+    }
+    CouponExtMeta meta = parseCouponExt(scene.getExtJson());
+    if (StrUtil.isNotBlank(meta.legacySceneCode())) {
+      return meta.legacySceneCode();
+    }
+    String canonical = StrUtil.nullToEmpty(scene.getSceneCode()).trim().toUpperCase();
+    for (Map.Entry<String, String> entry : LEGACY_TO_CANONICAL.entrySet()) {
+      if (entry.getValue().equals(canonical)) {
+        return entry.getKey();
+      }
+    }
+    return scene.getSceneCode();
+  }
+
+  /**
+   * 由注册表选项组装优惠券场景下拉项字段(legacy 编号 + 选品 URL)。
+   */
+  public static CouponScenePickerFields toCouponPickerFields(BizSceneOptionVO scene) {
+    CouponScenePickerFields fields = new CouponScenePickerFields();
+    fields.setSceneCode(toCouponLegacySceneCode(scene));
+    fields.setSceneName(scene.getSceneName());
+
+    CouponExtMeta meta = parseCouponExt(scene.getExtJson());
+    fields.setSupportCategoryBrand(meta.supportCategoryBrand());
+
+    BizScenePickerSupport.PickerFields picker =
+        BizScenePickerSupport.resolve(scene, BizScenePickerSupport.PICKER_COUPON_SCOPE);
+    fields.setRequestUrl(picker.getListUrl());
+    fields.setKeywordField(picker.getKeywordField());
+    fields.setNameField(picker.getNameField());
+    fields.setScopeItemLabel(BizScenePickerSupport.resolveCouponScopeItemLabel(scene));
+    return fields;
+  }
+
+  public static boolean hasRegistryMapping(String sceneCode) {
+    if (StrUtil.isBlank(sceneCode)) {
+      return false;
+    }
+    String normalized = sceneCode.trim().toUpperCase();
+    return LEGACY_TO_CANONICAL.containsKey(normalized)
+        || LEGACY_TO_CANONICAL.containsValue(normalized);
+  }
+
+  private static CouponExtMeta parseCouponExt(String extJson) {
+    if (StrUtil.isBlank(extJson)) {
+      return CouponExtMeta.EMPTY;
+    }
+    JSONObject root = JSONUtil.parseObj(extJson);
+    JSONObject coupon = root.getJSONObject("coupon");
+    String legacySceneCode = coupon != null ? coupon.getStr("legacySceneCode") : null;
+    Integer supportCategoryBrand = coupon != null ? coupon.getInt("supportCategoryBrand") : null;
+    JSONObject pickers = root.getJSONObject("pickers");
+    return new CouponExtMeta(legacySceneCode, supportCategoryBrand, pickers, coupon);
+  }
+
+  private record CouponExtMeta(String legacySceneCode, Integer supportCategoryBrand, JSONObject pickers,
+      JSONObject coupon) {
+
+    private static final CouponExtMeta EMPTY = new CouponExtMeta(null, null, null, null);
+  }
+
+  /**
+   * 优惠券场景选品元数据(由注册表映射而来)。
+   */
+  @lombok.Data
+  public static class CouponScenePickerFields {
+
+    private String sceneCode;
+    private String sceneName;
+    private String requestUrl;
+    private String keywordField;
+    private String nameField;
+    private Integer supportCategoryBrand;
+    /** 券模板「指定适用项」展示名,如商品、房型、套餐 */
+    private String scopeItemLabel;
+  }
+}

+ 79 - 0
jeesharp-business/business-common/business-common-bean/src/main/java/com/jeesharp/business/modules/common/bizscene/support/BizSceneMobileJumpSupport.java

@@ -0,0 +1,79 @@
+package com.jeesharp.business.modules.common.bizscene.support;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import lombok.Getter;
+
+/**
+ * 从 {@code cfg_biz_scene.ext_json.mobile} 解析小程序轮播/跳转路径。
+ *
+ * @author jeesharp
+ * @since 2026-07-03
+ */
+public final class BizSceneMobileJumpSupport {
+
+    private BizSceneMobileJumpSupport() {
+    }
+
+    /**
+     * 解析 ext_json 中的 mobile 节点。
+     */
+    public static MobileJump parse(String extJson) {
+        if (StrUtil.isBlank(extJson)) {
+            return null;
+        }
+        JSONObject root = JSONUtil.parseObj(extJson);
+        JSONObject mobile = root.getJSONObject("mobile");
+        if (mobile == null) {
+            return null;
+        }
+        String detailPath = mobile.getStr("detailPath");
+        if (StrUtil.isBlank(detailPath)) {
+            return null;
+        }
+        String detailIdParam = mobile.getStr("detailIdParam");
+        if (StrUtil.isBlank(detailIdParam)) {
+            detailIdParam = "id";
+        }
+        String listPath = mobile.getStr("listPath");
+        return new MobileJump(detailPath.trim(), detailIdParam.trim(),
+                StrUtil.isBlank(listPath) ? null : listPath.trim());
+    }
+
+  public static String resolveDetailPath(MobileJump jump, String bizId) {
+        if (jump == null || StrUtil.isBlank(bizId)) {
+            return null;
+        }
+        return jump.getDetailPath() + "?" + jump.getDetailIdParam() + "=" + encodeQueryValue(bizId.trim());
+    }
+
+    public static String resolveListPath(MobileJump jump) {
+        return jump == null ? null : jump.getListPath();
+    }
+
+    private static String encodeQueryValue(String value) {
+        try {
+            return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8);
+        } catch (Exception ex) {
+            return value;
+        }
+    }
+
+    /**
+     * 小程序跳转路径配置。
+     */
+    @Getter
+    public static final class MobileJump {
+
+        private final String detailPath;
+        private final String detailIdParam;
+        private final String listPath;
+
+        public MobileJump(String detailPath, String detailIdParam, String listPath) {
+            this.detailPath = detailPath;
+            this.detailIdParam = detailIdParam;
+            this.listPath = listPath;
+        }
+    }
+}

Some files were not shown because too many files changed in this diff