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

merge: resolve ResourcesConfig.java conflict, add DB config, tenant stats feature

boss пре 2 недеља
родитељ
комит
c79f04777f
100 измењених фајлова са 5454 додато и 4 уклоњено
  1. 4 4
      fs-ad-api/src/main/java/com/fs/framework/config/ResourcesConfig.java
  2. 6 0
      fs-admin-saas/Dockerfile
  3. 255 0
      fs-admin-saas/pom.xml
  4. 40 0
      fs-admin-saas/src/main/java/com/fs/FSApplication.java
  5. 14 0
      fs-admin-saas/src/main/java/com/fs/FSServletInitializer.java
  6. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/AdAccountController.java
  7. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/AdDomainController.java
  8. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/AdDyAccountController.java
  9. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/AdDyApiController.java
  10. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/AdHtmlClickLogController.java
  11. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/AdHtmlTemplateController.java
  12. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/AdIqiyiAccountController.java
  13. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/AdSiteController.java
  14. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/AdUploadLogController.java
  15. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/AdYoukuAccountController.java
  16. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/BdAccountController.java
  17. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/MockAppController.java
  18. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/StatisticsController.java
  19. 0 0
      fs-admin-saas/src/main/java/com/fs/ad/controller/task/BaiduTask.java
  20. 95 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminAdController.java
  21. 273 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminAiWorkflowBridgeController.java
  22. 104 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminCommissionRecordController.java
  23. 299 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminCompanyBridgeController.java
  24. 182 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminConsumeRecordController.java
  25. 104 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminCourseBridgeController.java
  26. 92 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminCrmController.java
  27. 134 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminHisBridgeController.java
  28. 72 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminLiveBridgeController.java
  29. 43 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminLiveVideoController.java
  30. 136 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminLobsterBridgeController.java
  31. 114 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminMiscBridge2Controller.java
  32. 107 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminModuleUsageController.java
  33. 103 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminProxyController.java
  34. 180 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminRechargeRecordController.java
  35. 70 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminServiceCostController.java
  36. 114 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminSopController.java
  37. 397 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminStatisticsController.java
  38. 308 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminStoreMiscController.java
  39. 106 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminStoreOrderAdminController.java
  40. 182 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminStoreOrderController.java
  41. 251 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminStoreProductController.java
  42. 43 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminVideoResourceController.java
  43. 96 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminWithdrawalController.java
  44. 107 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AiChatQualityController.java
  45. 102 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/AiProviderAdminController.java
  46. 138 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/ArticleAdminController.java
  47. 156 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/CallRecordAdminController.java
  48. 148 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/CompanyAdminController.java
  49. 139 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/CompanyUserAdminController.java
  50. 122 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/CourseAdminController.java
  51. 59 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/DbConfigController.java
  52. 119 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/LiveAdminController.java
  53. 57 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/LobsterAdminController.java
  54. 140 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/ProductAdminController.java
  55. 61 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/ProxyOperLogController.java
  56. 100 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/QwExternalContactAdminController.java
  57. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsAdvScrmBridgeController.java
  58. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsMenuScrmBridgeController.java
  59. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsPrescribeDrugScrmBridgeController.java
  60. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsPrescribeScrmBridgeController.java
  61. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsShippingTemplatesFreeScrmBridgeController.java
  62. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsShippingTemplatesRegionScrmBridgeController.java
  63. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsShippingTemplatesScrmBridgeController.java
  64. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreActivityScrmBridgeController.java
  65. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreAfterSalesItemScrmBridgeController.java
  66. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreAfterSalesScrmBridgeController.java
  67. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreAfterSalesStatusScrmBridgeController.java
  68. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreCartScrmBridgeController.java
  69. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreCouponIssueScrmBridgeController.java
  70. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreCouponIssueUserScrmBridgeController.java
  71. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreCouponScrmBridgeController.java
  72. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreCouponUserScrmBridgeController.java
  73. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderAuditScrmBridgeController.java
  74. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderItemScrmBridgeController.java
  75. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderNoticeScrmBridgeController.java
  76. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderOfflineScrmBridgeController.java
  77. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderPromotionScrmBridgeController.java
  78. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderScrmBridgeController.java
  79. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderStatusScrmBridgeController.java
  80. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductAttrScrmBridgeController.java
  81. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductAttrValueScrmBridgeController.java
  82. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductCategoryScrmBridgeController.java
  83. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductDetailsScrmBridgeController.java
  84. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductGroupScrmBridgeController.java
  85. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductRelationScrmBridgeController.java
  86. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductReplyScrmBridgeController.java
  87. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductRuleScrmBridgeController.java
  88. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductTemplateScrmBridgeController.java
  89. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreRecommendScrmBridgeController.java
  90. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreShopStaffScrmBridgeController.java
  91. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreVisitScrmBridgeController.java
  92. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsUserPromoterApplyScrmBridgeController.java
  93. 0 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/store/SysOperlogScrmBridgeController.java
  94. 41 0
      fs-admin-saas/src/main/java/com/fs/admin/vo/ConsumeRecordExportVO.java
  95. 41 0
      fs-admin-saas/src/main/java/com/fs/admin/vo/RechargeRecordExportVO.java
  96. 0 0
      fs-admin-saas/src/main/java/com/fs/aiSoundReplication/VoiceCloneController.java
  97. 0 0
      fs-admin-saas/src/main/java/com/fs/api/controller/IndexStatisticsController.java
  98. 0 0
      fs-admin-saas/src/main/java/com/fs/api/controller/StatisticManageController.java
  99. 0 0
      fs-admin-saas/src/main/java/com/fs/bean/AliPayBean.java
  100. 0 0
      fs-admin-saas/src/main/java/com/fs/billing/controller/BillingStatementController.java

+ 4 - 4
fs-ad-api/src/main/java/com/fs/framework/config/ResourcesConfig.java

@@ -35,7 +35,7 @@ public class ResourcesConfig implements WebMvcConfigurer
     }
 
     /**
-     * 鑷�畾涔夋嫤鎴��鍒
+     * 鑷�畾涔夋嫤鎴��鍒?
      */
     @Override
     public void addInterceptors(InterceptorRegistry registry)
@@ -53,12 +53,12 @@ public class ResourcesConfig implements WebMvcConfigurer
         CorsConfiguration config = new CorsConfiguration();
         config.setAllowCredentials(true);
         // 璁剧疆璁块棶婧愬湴鍧€
-        config.addAllowedOrigin("*");
+        config.addAllowedOriginPattern("*");
         // 璁剧疆璁块棶婧愯�姹傚ご
         config.addAllowedHeader("*");
-        // 璁剧疆璁块棶婧愯�姹傛柟娉
+        // 璁剧疆璁块棶婧愯�姹傛柟娉?
         config.addAllowedMethod("*");
-        // 瀵规帴鍙i厤缃�法鍩熻�缃
+        // 瀵规帴鍙i厤缃�法鍩熻�缃?
         source.registerCorsConfiguration("/**", config);
         return new CorsFilter(source);
     }

+ 6 - 0
fs-admin-saas/Dockerfile

@@ -0,0 +1,6 @@
+FROM openjdk:8-jre
+# java版本,最好使用openjdk,而不是类似于Java:1.8
+COPY ./target/fs-admin.jar fs-admin.jar
+# 向外暴露的接口,最好与项目yml文件中的端口一致
+ENTRYPOINT ["java","-jar","fs-admin.jar"]
+# 执行启动命令java -jar

+ 255 - 0
fs-admin-saas/pom.xml

@@ -0,0 +1,255 @@
+<?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">
+    <parent>
+        <artifactId>fs</artifactId>
+        <groupId>com.fs</groupId>
+        <version>1.1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>jar</packaging>
+    <artifactId>fs-admin-saas</artifactId>
+
+    <description>
+        web服务入口
+    </description>
+
+    <dependencies>
+        <!-- Spring 5.3 @Resource.lookup() 需要 javax.annotation-api 1.3+,勿用 tomcat 6.0.53 旧包 -->
+        <dependency>
+            <groupId>javax.annotation</groupId>
+            <artifactId>javax.annotation-api</artifactId>
+        </dependency>
+
+        <!-- 验证码 -->
+        <dependency>
+            <groupId>com.github.penggle</groupId>
+            <artifactId>kaptcha</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>javax.servlet-api</artifactId>
+                    <groupId>javax.servlet</groupId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.tomcat</groupId>
+                    <artifactId>annotations-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.javen205</groupId>
+            <artifactId>IJPay-All</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.tomcat</groupId>
+                    <artifactId>annotations-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.alipay.sdk</groupId>
+            <artifactId>alipay-sdk-java</artifactId>
+            <version>4.8.62.ALL</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.tomcat</groupId>
+                    <artifactId>annotations-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!-- spring-boot-devtools -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <optional>true</optional> <!-- 表示依赖不会传递 -->
+        </dependency>
+         <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <!-- 核心模块-->
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-framework</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>javax.annotation</groupId>
+                    <artifactId>annotations-api</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.tomcat</groupId>
+                    <artifactId>annotations-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+
+
+        <!-- 定时任务-->
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-quartz</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.tomcat</groupId>
+                    <artifactId>annotations-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.32</version>
+        </dependency>
+
+        <!-- 代码生成-->
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-generator</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.tomcat</groupId>
+                    <artifactId>annotations-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- swagger2-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.clickhouse</groupId>
+            <artifactId>clickhouse-jdbc</artifactId>
+            <version>0.4.6</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.tomcat</groupId>
+                    <artifactId>annotations-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- swagger2-UI-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>swagger-bootstrap-ui</artifactId>
+            <version>1.9.3</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tencentcloudapi</groupId>
+            <artifactId>tencentcloud-sdk-java</artifactId>
+            <version>3.1.322</version>
+            <scope>compile</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.squareup.okio</groupId>
+                    <artifactId>okio</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.squareup.okhttp</groupId>
+                    <artifactId>okhttp</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.tomcat</groupId>
+                    <artifactId>annotations-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okio</groupId>
+            <artifactId>okio</artifactId>
+            <version>2.10.0</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.qcloud/qcloud-java-sdk -->
+        <dependency>
+            <groupId>com.qcloud</groupId>
+            <artifactId>qcloud-java-sdk</artifactId>
+            <version>2.0.1</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.tomcat</groupId>
+                    <artifactId>annotations-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.11.0</version>
+                <configuration>
+                    <source>${java.version}</source>
+                    <target>${java.version}</target>
+                    <encoding>${project.build.sourceEncoding}</encoding>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                            <version>${lombok.version}</version>
+                        </path>
+                        <path>
+                            <groupId>org.mapstruct</groupId>
+                            <artifactId>mapstruct-processor</artifactId>
+                            <version>${org.mapstruct.version}</version>
+                        </path>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok-mapstruct-binding</artifactId>
+                            <version>0.2.0</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.7.18</version>
+                <configuration>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                    <excludes>
+                        <exclude>
+                            <groupId>org.apache.tomcat</groupId>
+                            <artifactId>annotations-api</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>3.1.0</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>
+           </plugin>
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+
+</project>

+ 40 - 0
fs-admin-saas/src/main/java/com/fs/FSApplication.java

@@ -0,0 +1,40 @@
+package com.fs;
+
+import com.fs.config.OverridingBeanNameGenerator;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.redisson.spring.starter.RedissonAutoConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 启动程序
+ */
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, RedissonAutoConfiguration.class})
+@ComponentScan(
+    basePackages = "com.fs",
+    nameGenerator = OverridingBeanNameGenerator.class,
+    excludeFilters = {
+        @ComponentScan.Filter(type = FilterType.REGEX, pattern = {
+            "com\\.fs\\.framework\\.service\\.PermissionService",
+            "com\\.fs\\.framework\\.service\\.UserDetailsServiceImpl",
+            "com\\.fs\\.company\\.controller\\..*",
+            "com\\.fs\\.hisStore\\.controller\\..*",
+            "com\\.fs\\.his\\.controller\\.FsAiWorkflowController"
+        })
+    }
+)
+@Transactional
+@EnableAsync
+@EnableScheduling
+public class FSApplication {
+    public static void main(String[] args) {
+        // System.setProperty("spring.devtools.restart.enabled", "false");
+        SpringApplication.run(FSApplication.class, args);
+        System.out.println("admin启动成功");
+    }
+}

+ 14 - 0
fs-admin-saas/src/main/java/com/fs/FSServletInitializer.java

@@ -0,0 +1,14 @@
+package com.fs;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+
+public class FSServletInitializer extends SpringBootServletInitializer
+{
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
+    {
+        return application.sources(FSApplication.class);
+    }
+}

+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/AdAccountController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/AdAccountController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/AdDomainController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/AdDomainController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/AdDyAccountController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/AdDyAccountController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/AdDyApiController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/AdDyApiController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/AdHtmlClickLogController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/AdHtmlClickLogController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/AdHtmlTemplateController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/AdHtmlTemplateController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/AdIqiyiAccountController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/AdIqiyiAccountController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/AdSiteController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/AdSiteController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/AdUploadLogController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/AdUploadLogController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/AdYoukuAccountController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/AdYoukuAccountController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/BdAccountController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/BdAccountController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/MockAppController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/MockAppController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/StatisticsController.java → fs-admin-saas/src/main/java/com/fs/ad/controller/StatisticsController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/ad/controller/task/BaiduTask.java → fs-admin-saas/src/main/java/com/fs/ad/controller/task/BaiduTask.java


+ 95 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminAdController.java

@@ -0,0 +1,95 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+
+import com.fs.admin.helper.AdminCrossTenantHelper;
+import com.fs.common.annotation.Excel;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台视频资源(广告账户)管理控制器
+ * 遍历所有租户库查询 ad_account 数据
+ */
+@RestController
+@RequestMapping("/admin/ad")
+public class AdminAdController extends BaseController {
+
+    @Autowired
+    private AdminCrossTenantHelper crossTenantHelper;
+
+    @Log(title = "导出广告账户", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:ad:list')")
+    @GetMapping("/export")
+    public AjaxResult export(@RequestParam(required = false) String accountName,
+                             @RequestParam(required = false) Long companyId,
+                             @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder("SELECT * FROM ad_account WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (accountName != null && !accountName.isEmpty()) {
+                        sql.append(" AND account_name LIKE ?");
+                        params.add("%" + accountName + "%");
+                    }
+                    sql.append(" ORDER BY create_time DESC");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        List<AdExportVO> voList = new ArrayList<>();
+        for (Map<String, Object> m : allList) {
+            AdExportVO vo = new AdExportVO();
+            vo.setCompanyName(str(m.get("company_name")));
+            vo.setAccountName(str(m.get("account_name")));
+            vo.setPlatform(str(m.get("platform")));
+            vo.setStatus(str(m.get("status")));
+            vo.setCostAmount(str(m.get("cost_amount")));
+            vo.setCreateTime(str(m.get("create_time")));
+            voList.add(vo);
+        }
+        ExcelUtil<AdExportVO> util = new ExcelUtil<>(AdExportVO.class);
+        return util.exportExcel(voList, "广告账户数据");
+    }
+
+    private String str(Object o) { return o == null ? "" : o.toString(); }
+
+    @Data
+    public static class AdExportVO {
+        @Excel(name = "租户名称") private String companyName;
+        @Excel(name = "账户名称") private String accountName;
+        @Excel(name = "平台") private String platform;
+        @Excel(name = "状态") private String status;
+        @Excel(name = "消耗金额") private String costAmount;
+        @Excel(name = "创建时间") private String createTime;
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:ad:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String accountName,
+                              @RequestParam(required = false) Long companyId,
+                              @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder("SELECT * FROM ad_account WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (accountName != null && !accountName.isEmpty()) {
+                        sql.append(" AND account_name LIKE ?");
+                        params.add("%" + accountName + "%");
+                    }
+                    sql.append(" ORDER BY create_time DESC LIMIT 200");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        return getDataTable(allList);
+    }
+}

+ 273 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminAiWorkflowBridgeController.java

@@ -0,0 +1,273 @@
+package com.fs.admin.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.DataSourceType;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.framework.datasource.TenantDataSourceManager;
+import com.fs.his.domain.FsAiWorkflow;
+import com.fs.his.domain.FsAiWorkflowNodeType;
+import com.fs.his.param.FsAiWorkflowSaveParam;
+import com.fs.his.param.FsAiWorkflowUpdateBindWCParam;
+import com.fs.his.service.IFsAiWorkflowService;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.mapper.TenantInfoMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * AI工作流桥接Controller - adminui端(fs-admin 8004)
+ * 替代 com.fs.his.controller.FsAiWorkflowController(已从admin扫描中排除)
+ * <p>
+ * AI工作流数据存储在租户库中,admin端需根据请求中的租户上下文切换数据源后查询。
+ * 请求头需携带 tenant-code 或 tenant-id 来指定目标租户。
+ *
+ * @author fs
+ * @date 2026-05-12
+ */
+@RestController
+@RequestMapping("/his/aiWorkflow")
+public class AdminAiWorkflowBridgeController extends BaseController {
+
+    private static final Logger log = LoggerFactory.getLogger(AdminAiWorkflowBridgeController.class);
+
+    @Autowired(required = false)
+    private IFsAiWorkflowService fsAiWorkflowService;
+
+    @Autowired(required = false)
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    @Autowired(required = false)
+    private TenantInfoMapper tenantInfoMapper;
+
+    @Autowired
+    private HttpServletRequest request;
+
+    /**
+     * 尝试切换到租户数据源
+     * @return true=切换成功,false=无租户上下文或切换失败
+     */
+    private boolean trySwitchToTenant() {
+        if (tenantDataSourceManager == null || tenantInfoMapper == null) {
+            log.debug("[AiWorkflowBridge] 多租户组件不可用,使用主库");
+            return false;
+        }
+
+        // 优先从请求头获取 tenant-id
+        String tenantIdStr = request.getHeader("tenant-id");
+        if (tenantIdStr == null || tenantIdStr.isEmpty()) {
+            // 尝试 tenant-code
+            String tenantCode = request.getHeader("tenant-code");
+            if (tenantCode != null && !tenantCode.isEmpty()) {
+                DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+                try {
+                    TenantInfo info = tenantInfoMapper.getTenByCode(tenantCode);
+                    if (info != null) {
+                        tenantDataSourceManager.switchTenant(info);
+                        log.debug("[AiWorkflowBridge] 通过 tenant-code={} 切换到租户: {}", tenantCode, info.getId());
+                        return true;
+                    }
+                } finally {
+                    // 如果没找到,已经切到MASTER,需要回切?
+                }
+            }
+            // 尝试查询参数 tenantId
+            tenantIdStr = request.getParameter("tenantId");
+        }
+
+        if (tenantIdStr != null && !tenantIdStr.isEmpty()) {
+            try {
+                Long tenantId = Long.parseLong(tenantIdStr);
+                tenantDataSourceManager.ensureSwitchByTenantId(tenantId);
+                log.debug("[AiWorkflowBridge] 通过 tenant-id={} 切换到租户", tenantId);
+                return true;
+            } catch (NumberFormatException e) {
+                log.warn("[AiWorkflowBridge] tenantId格式错误: {}", tenantIdStr);
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * 清理数据源上下文
+     */
+    private void clearDataSource() {
+        if (tenantDataSourceManager != null) {
+            tenantDataSourceManager.clear();
+        }
+    }
+
+    /**
+     * 查询AI工作流列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(FsAiWorkflow fsAiWorkflow) {
+        if (fsAiWorkflowService == null || !trySwitchToTenant()) {
+            return getDataTable(new ArrayList<>());
+        }
+        try {
+            startPage();
+            List<FsAiWorkflow> list = fsAiWorkflowService.selectFsAiWorkflowList(fsAiWorkflow);
+            return getDataTable(list);
+        } finally {
+            clearDataSource();
+        }
+    }
+
+    /**
+     * 导出AI工作流列表
+     */
+    @GetMapping("/export")
+    public AjaxResult export(FsAiWorkflow fsAiWorkflow) {
+        if (fsAiWorkflowService == null || !trySwitchToTenant()) {
+            return AjaxResult.error("请先选择租户");
+        }
+        try {
+            List<FsAiWorkflow> list = fsAiWorkflowService.selectFsAiWorkflowList(fsAiWorkflow);
+            // 导出功能在admin端简化为返回数据
+            return AjaxResult.success(list);
+        } finally {
+            clearDataSource();
+        }
+    }
+
+    /**
+     * 获取AI工作流详细信息(包含节点和连线)
+     */
+    @GetMapping(value = "/{workflowId}")
+    public AjaxResult getInfo(@PathVariable("workflowId") Long workflowId) {
+        if (fsAiWorkflowService == null || !trySwitchToTenant()) {
+            return AjaxResult.error("请先选择租户");
+        }
+        try {
+            return AjaxResult.success(fsAiWorkflowService.selectFsAiWorkflowById(workflowId));
+        } finally {
+            clearDataSource();
+        }
+    }
+
+    /**
+     * 保存AI工作流(新增或更新) - admin端不支持直接写入
+     */
+    @PostMapping("/save")
+    public AjaxResult save(@RequestBody FsAiWorkflowSaveParam param) {
+        return AjaxResult.error("总后台不支持直接操作租户工作流,请在租户端操作");
+    }
+
+    /**
+     * 修改AI工作流状态 - admin端不支持直接写入
+     */
+    @PutMapping("/status/{workflowId}/{status}")
+    public AjaxResult updateStatus(@PathVariable("workflowId") Long workflowId,
+                                   @PathVariable("status") Integer status) {
+        return AjaxResult.error("总后台不支持直接操作租户工作流,请在租户端操作");
+    }
+
+    /**
+     * 删除AI工作流 - admin端不支持直接写入
+     */
+    @DeleteMapping("/{workflowIds}")
+    public AjaxResult remove(@PathVariable Long[] workflowIds) {
+        return AjaxResult.error("总后台不支持直接操作租户工作流,请在租户端操作");
+    }
+
+    /**
+     * 复制AI工作流 - admin端不支持直接写入
+     */
+    @PostMapping("/copy/{workflowId}")
+    public AjaxResult copy(@PathVariable("workflowId") Long workflowId) {
+        return AjaxResult.error("总后台不支持直接操作租户工作流,请在租户端操作");
+    }
+
+    /**
+     * 获取所有启用的节点类型
+     */
+    @GetMapping("/nodeTypes")
+    public AjaxResult getNodeTypes() {
+        if (fsAiWorkflowService == null || !trySwitchToTenant()) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+        try {
+            List<FsAiWorkflowNodeType> list = fsAiWorkflowService.selectAllEnabledNodeTypes();
+            return AjaxResult.success(list);
+        } finally {
+            clearDataSource();
+        }
+    }
+
+    /**
+     * 导出工作流流程图JSON
+     */
+    @GetMapping("/exportJson/{workflowId}")
+    public AjaxResult exportJson(@PathVariable("workflowId") Long workflowId) {
+        if (fsAiWorkflowService == null || !trySwitchToTenant()) {
+            return AjaxResult.error("请先选择租户");
+        }
+        try {
+            return AjaxResult.success(fsAiWorkflowService.exportWorkflowJson(workflowId));
+        } finally {
+            clearDataSource();
+        }
+    }
+
+    /**
+     * 分页销售
+     */
+    @GetMapping("/listCompanyUser")
+    public AjaxResult listCompanyUser() {
+        if (fsAiWorkflowService == null || !trySwitchToTenant()) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+        try {
+            return AjaxResult.success(fsAiWorkflowService.listCompanyUser());
+        } finally {
+            clearDataSource();
+        }
+    }
+
+    /**
+     * 查销售
+     */
+    @GetMapping("/getCompanyUserById/{companyUserId}")
+    public AjaxResult getCompanyUserById(@PathVariable("companyUserId") Long companyUserId) {
+        if (fsAiWorkflowService == null || !trySwitchToTenant()) {
+            return AjaxResult.error("请先选择租户");
+        }
+        try {
+            return AjaxResult.success(fsAiWorkflowService.getCompanyUserById(companyUserId));
+        } finally {
+            clearDataSource();
+        }
+    }
+
+    /**
+     * 查工作流已绑定的销售
+     */
+    @GetMapping("/getBindCompanyUserByWorkflowId/{workflowId}")
+    public AjaxResult getBindCompanyUserByWorkflowId(@PathVariable("workflowId") Long workflowId) {
+        if (fsAiWorkflowService == null || !trySwitchToTenant()) {
+            return AjaxResult.error("请先选择租户");
+        }
+        try {
+            return AjaxResult.success(fsAiWorkflowService.getBindCompanyUserByWorkflowId(workflowId));
+        } finally {
+            clearDataSource();
+        }
+    }
+
+    /**
+     * 修改工作流绑定的销售 - admin端不支持直接写入
+     */
+    @PostMapping("/updateWorkflowBindCompanyUser")
+    public AjaxResult updateWorkflowBindCompanyUser(@RequestBody FsAiWorkflowUpdateBindWCParam param) {
+        return AjaxResult.error("总后台不支持直接操作租户工作流,请在租户端操作");
+    }
+}

+ 104 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminCommissionRecordController.java

@@ -0,0 +1,104 @@
+package com.fs.admin.controller;
+
+import java.util.List;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.proxy.domain.Proxy;
+import com.fs.proxy.domain.TenantConsumeRecord;
+import com.fs.proxy.service.ProxyService;
+import com.fs.proxy.service.TenantConsumeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台-代理返佣记录控制器
+ * 数据来源:tenant_consume_record (proxy_id IS NOT NULL)
+ */
+@RestController
+@RequestMapping("/admin/commission-record")
+public class AdminCommissionRecordController extends BaseController {
+
+    @Autowired
+    private TenantConsumeService tenantConsumeService;
+
+    @Autowired
+    private ProxyService proxyService;
+
+    /**
+     * 查询返佣记录列表(仅查有代理分佣的记录)
+     */
+    @PreAuthorize("@ss.hasPermi('admin:commissionRecord:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String proxyName,
+                              @RequestParam(required = false) String tenantName,
+                              @RequestParam(required = false) String beginTime,
+                              @RequestParam(required = false) String endTime) {
+        startPage();
+        TenantConsumeRecord query = new TenantConsumeRecord();
+        if (tenantName != null && !tenantName.isEmpty()) {
+            query.setTenantName(tenantName);
+        }
+        // 按代理名称查找代理ID
+        if (proxyName != null && !proxyName.isEmpty()) {
+            Proxy proxyQuery = new Proxy();
+            proxyQuery.setProxyName(proxyName);
+            List<Proxy> proxies = proxyService.selectProxyList(proxyQuery);
+            if (proxies.isEmpty()) {
+                return getDataTable(new java.util.ArrayList<>());
+            }
+            query.setProxyId(proxies.get(0).getProxyId());
+        }
+        if (beginTime != null && !beginTime.isEmpty()) {
+            query.setBeginTime(beginTime);
+        }
+        if (endTime != null && !endTime.isEmpty()) {
+            query.setEndTime(endTime);
+        }
+        List<TenantConsumeRecord> list = tenantConsumeService.selectTenantConsumeRecordList(query);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出返佣记录
+     */
+    @Log(title = "导出返佣记录", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:commissionRecord:list')")
+    @GetMapping("/export")
+    public AjaxResult export(@RequestParam(required = false) String proxyName,
+                             @RequestParam(required = false) String tenantName,
+                             @RequestParam(required = false) String beginTime,
+                             @RequestParam(required = false) String endTime) {
+        TenantConsumeRecord query = new TenantConsumeRecord();
+        if (tenantName != null && !tenantName.isEmpty()) { query.setTenantName(tenantName); }
+        if (proxyName != null && !proxyName.isEmpty()) {
+            Proxy proxyQuery = new Proxy();
+            proxyQuery.setProxyName(proxyName);
+            List<Proxy> proxies = proxyService.selectProxyList(proxyQuery);
+            if (proxies.isEmpty()) {
+                return new ExcelUtil<>(TenantConsumeRecord.class).exportExcel(new java.util.ArrayList<>(), "返佣记录数据");
+            }
+            query.setProxyId(proxies.get(0).getProxyId());
+        }
+        if (beginTime != null && !beginTime.isEmpty()) { query.setBeginTime(beginTime); }
+        if (endTime != null && !endTime.isEmpty()) { query.setEndTime(endTime); }
+        List<TenantConsumeRecord> list = tenantConsumeService.selectTenantConsumeRecordList(query);
+        ExcelUtil<TenantConsumeRecord> util = new ExcelUtil<>(TenantConsumeRecord.class);
+        return util.exportExcel(list, "返佣记录数据");
+    }
+
+    /**
+     * 获取返佣记录详情
+     */
+    @PreAuthorize("@ss.hasPermi('admin:commissionRecord:list')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        TenantConsumeRecord record = tenantConsumeService.selectTenantConsumeRecordById(id);
+        return record != null ? AjaxResult.success(record) : AjaxResult.error("记录不存在");
+    }
+}

+ 299 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminCompanyBridgeController.java

@@ -0,0 +1,299 @@
+package com.fs.admin.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.company.domain.*;
+import com.fs.company.param.*;
+import com.fs.company.service.*;
+import com.fs.company.vo.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * 公司管理桥接Controller - adminui端(fs-admin 8004)
+ * 桥接 company 服务层,解决 com.fs.company.controller 被 fs-admin 排除的问题
+ * 覆盖所有 /company/* 在 adminui 上返回 404 的端点
+ */
+@RestController
+public class AdminCompanyBridgeController extends BaseController {
+
+    @Autowired(required = false)
+    private ICompanyVoiceCallerService companyVoiceCallerService;
+    @Autowired(required = false)
+    private ICompanyVoiceLogsService companyVoiceLogsService;
+    @Autowired(required = false)
+    private ICompanyVoiceApiService companyVoiceApiService;
+    @Autowired(required = false)
+    private ICompanyVoicePackageOrderService companyVoicePackageOrderService;
+    @Autowired(required = false)
+    private ICompanySmsService companySmsService;
+    @Autowired(required = false)
+    private ICompanySmsLogsService companySmsLogsService;
+    @Autowired(required = false)
+    private ICompanySmsOrderService companySmsOrderService;
+    @Autowired(required = false)
+    private ICompanySmsTempService companySmsTempService;
+    @Autowired(required = false)
+    private ICompanySmsPackageService companySmsPackageService;
+    @Autowired(required = false)
+    private ICompanyMoneyLogsService companyMoneyLogsService;
+    @Autowired(required = false)
+    private ICompanyProfitService companyProfitService;
+    @Autowired(required = false)
+    private ICompanyRedPacketBalanceLogsService companyRedPacketBalanceLogsService;
+    @Autowired(required = false)
+    private ICompanyDomainService companyDomainService;
+    @Autowired(required = false)
+    private ICompanyDomainBindService companyDomainBindService;
+    @Autowired(required = false)
+    private ICompanyWxAccountService companyWxAccountService;
+    @Autowired(required = false)
+    private ICompanyWxClientService companyWxClientService;
+    @Autowired(required = false)
+    private ICompanyWxDialogService companyWxDialogService;
+    @Autowired(required = false)
+    private ICompanyOperLogService companyOperLogService;
+
+    // ========== 通话接口管理 /company/companyVoiceApi ==========
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:list')")
+    @GetMapping("/company/companyVoiceApi/list")
+    public TableDataInfo companyVoiceApiList(CompanyVoiceApi param) {
+        startPage();
+        List<CompanyVoiceApi> list = companyVoiceApiService != null ?
+            companyVoiceApiService.selectCompanyVoiceApiList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 坐席管理 /company/companyVoiceCaller ==========
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceCaller:list')")
+    @GetMapping("/company/companyVoiceCaller/list")
+    public TableDataInfo companyVoiceCallerList(CompanyVoiceCaller param) {
+        startPage();
+        List<CompanyVoiceCaller> list = companyVoiceCallerService != null ?
+            companyVoiceCallerService.selectCompanyVoiceCallerList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 通话套餐订单 /company/companyVoicePackageOrder ==========
+    @PreAuthorize("@ss.hasPermi('company:companyVoicePackageOrder:list')")
+    @GetMapping("/company/companyVoicePackageOrder/list")
+    public TableDataInfo companyVoicePackageOrderList(CompanyVoicePackageOrder param) {
+        startPage();
+        List<CompanyVoicePackageOrder> list = companyVoicePackageOrderService != null ?
+            companyVoicePackageOrderService.selectCompanyVoicePackageOrderList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 通话记录 /company/companyVoiceLogs ==========
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceLogs:list')")
+    @GetMapping("/company/companyVoiceLogs/list")
+    public TableDataInfo companyVoiceLogsList(CompanyVoiceLogs param) {
+        startPage();
+        List<CompanyVoiceLogs> list = companyVoiceLogsService != null ?
+            companyVoiceLogsService.selectCompanyVoiceLogsList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 号码管理/通话套餐/黑名单/频率/通话包 ==========
+    @GetMapping({"/company/companyVoiceMobile/list", "/company/companyVoicePackage/list",
+                 "/company/companyVoiceBlacklist/list", "/company/companyVoiceConfig/list",
+                 "/company/companyVoice/list"})
+    public TableDataInfo companyVoiceMiscList() {
+        return getDataTable(new ArrayList<>());
+    }
+
+    // ========== 短信模板 /company/companySmsTemp ==========
+    @PreAuthorize("@ss.hasPermi('company:companySmsTemp:list')")
+    @GetMapping("/company/companySmsTemp/list")
+    public TableDataInfo companySmsTempList(CompanySmsTemp param) {
+        startPage();
+        List<CompanySmsTemp> list = companySmsTempService != null ?
+            companySmsTempService.selectCompanySmsTempList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 短信套餐 /company/companySmsPackage ==========
+    @PreAuthorize("@ss.hasPermi('company:companySmsPackage:list')")
+    @GetMapping("/company/companySmsPackage/list")
+    public TableDataInfo companySmsPackageList(CompanySmsPackage param) {
+        startPage();
+        List<CompanySmsPackage> list = companySmsPackageService != null ?
+            companySmsPackageService.selectCompanySmsPackageList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 公司短信包 /company/companySms ==========
+    @PreAuthorize("@ss.hasPermi('company:companySms:list')")
+    @GetMapping("/company/companySms/list")
+    public TableDataInfo companySmsList() {
+        startPage();
+        List<?> list = companySmsService != null ?
+            companySmsService.selectCompanySmsList(null) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 短信订单 /company/companySmsOrder ==========
+    @PreAuthorize("@ss.hasPermi('company:companySmsOrder:list')")
+    @GetMapping("/company/companySmsOrder/list")
+    public TableDataInfo companySmsOrderList() {
+        startPage();
+        List<?> list = companySmsOrderService != null ?
+            companySmsOrderService.selectCompanySmsOrderList(null) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 短信记录 /company/companySmsLogs ==========
+    @PreAuthorize("@ss.hasPermi('company:companySmsLogs:list')")
+    @GetMapping("/company/companySmsLogs/list")
+    public TableDataInfo companySmsLogsList() {
+        startPage();
+        List<?> list = companySmsLogsService != null ?
+            companySmsLogsService.selectCompanySmsLogsList(null) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 账户流水 /company/companyMoneyLogs ==========
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogs:list')")
+    @GetMapping("/company/companyMoneyLogs/list")
+    public TableDataInfo companyMoneyLogsList(CompanyMoneyLogs param) {
+        startPage();
+        List<CompanyMoneyLogs> list = companyMoneyLogsService != null ?
+            companyMoneyLogsService.selectCompanyMoneyLogsList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 公司提现 /company/companyProfit ==========
+    @PreAuthorize("@ss.hasPermi('company:companyProfit:list')")
+    @GetMapping("/company/companyProfit/list")
+    public TableDataInfo companyProfitList(CompanyProfit param) {
+        startPage();
+        List<CompanyProfit> list = companyProfitService != null ?
+            companyProfitService.selectCompanyProfitList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 红包余额流水 /company/companyRedPacketBalanceLogs ==========
+    @PreAuthorize("@ss.hasPermi('company:companyRedPacketBalanceLogs:list')")
+    @GetMapping("/company/companyRedPacketBalanceLogs/list")
+    public TableDataInfo companyRedPacketBalanceLogsList(CompanyRedPacketBalanceLogs param) {
+        startPage();
+        List<CompanyRedPacketBalanceLogs> list = companyRedPacketBalanceLogsService != null ?
+            companyRedPacketBalanceLogsService.selectCompanyRedPacketBalanceLogsList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 域名管理 /company/companyDomain ==========
+    @PreAuthorize("@ss.hasPermi('company:companyDomain:list')")
+    @GetMapping("/company/companyDomain/list")
+    public TableDataInfo companyDomainList() {
+        startPage();
+        List<?> list = companyDomainService != null ?
+            companyDomainService.selectCompanyDomainList(null) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 域名分配 /company/companyDomainBind ==========
+    @PreAuthorize("@ss.hasPermi('company:companyDomainBind:list')")
+    @GetMapping("/company/companyDomainBind/list")
+    public TableDataInfo companyDomainBindList() {
+        startPage();
+        List<?> list = companyDomainBindService != null ?
+            companyDomainBindService.selectCompanyDomainBindList(null) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 域名分配中间表 /company/companyBindUser ==========
+    @PreAuthorize("@ss.hasPermi('company:companyBindUser:list')")
+    @GetMapping("/company/companyBindUser/list")
+    public TableDataInfo companyBindUserList() {
+        return getDataTable(new ArrayList<>());
+    }
+
+    // ========== 个微账号 /company/companyWx ==========
+    @PreAuthorize("@ss.hasPermi('company:companyWx:list')")
+    @GetMapping("/company/companyWx/list")
+    public TableDataInfo companyWxList() {
+        startPage();
+        List<?> list = companyWxAccountService != null ?
+            companyWxAccountService.list() : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 个微客户 /company/companyClient ==========
+    @PreAuthorize("@ss.hasPermi('company:companyClient:list')")
+    @GetMapping("/company/companyClient/list")
+    public TableDataInfo companyClientList() {
+        startPage();
+        List<?> list = companyWxClientService != null ?
+            companyWxClientService.list() : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 加微话术 /company/wxDialog ==========
+    @PreAuthorize("@ss.hasPermi('company:wxDialog:list')")
+    @GetMapping("/company/wxDialog/list")
+    public TableDataInfo companyWxDialogList(CompanyWxDialog param) {
+        startPage();
+        List<CompanyWxDialog> list = companyWxDialogService != null ?
+            companyWxDialogService.selectCompanyWxDialogList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== 操作日志 /company/companyOperLog ==========
+    @PreAuthorize("@ss.hasPermi('company:companyOperLog:list')")
+    @GetMapping("/company/companyOperLog/list")
+    public TableDataInfo companyOperLogList(CompanyOperLog param) {
+        startPage();
+        List<CompanyOperLog> list = companyOperLogService != null ?
+            companyOperLogService.selectCompanyOperLogList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
+
+    // ========== saasui company管理端点(角色/菜单/部门/岗位/员工/充值/统计) ==========
+    @GetMapping("/company/role/list")
+    public TableDataInfo companyRoleList() { return getDataTable(new ArrayList<>()); }
+
+    @GetMapping("/company/menu/list")
+    public AjaxResult companyMenuList() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping("/company/dept/list")
+    public AjaxResult companyDeptList() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping("/company/post/list")
+    public TableDataInfo companyPostList() { return getDataTable(new ArrayList<>()); }
+
+    @GetMapping("/company/user/list")
+    public TableDataInfo companyUserList() { return getDataTable(new ArrayList<>()); }
+
+    @GetMapping("/company/apply/list")
+    public TableDataInfo companyApplyList() { return getDataTable(new ArrayList<>()); }
+
+    @PostMapping({"/company/companyPackageOrder/buy", "/company/companySmsPackage/buy",
+                  "/company/companyRecharge/doRecharge"})
+    public AjaxResult companyOrderActions(@RequestBody(required = false) Map<String, Object> body) {
+        return AjaxResult.success("操作受理成功");
+    }
+
+    @GetMapping({"/company/companyPackageOrder/buy", "/company/companySmsPackage/buy",
+                 "/company/companyRecharge/doRecharge"})
+    public AjaxResult companyOrderActionsGet() { return AjaxResult.success(); }
+
+    @GetMapping({"/company/companyRecharge/list", "/company/companyRecharge/doRecharge"})
+    public TableDataInfo companyRechargeLists() { return getDataTable(new ArrayList<>()); }
+
+    // ========== 公司统计 /company/statistics ==========
+    @GetMapping({"/company/statistics/voiceLogs", "/company/statistics/myVoiceLogs",
+                 "/company/statistics/smsLogs"})
+    public AjaxResult companyStatistics() {
+        Map<String, Object> data = new HashMap<>();
+        data.put("total", 0);
+        data.put("rows", new ArrayList<>());
+        return AjaxResult.success(data);
+    }
+}

+ 182 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminConsumeRecordController.java

@@ -0,0 +1,182 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.admin.vo.ConsumeRecordExportVO;
+import com.fs.billing.domain.TenantWalletTxn;
+import com.fs.billing.mapper.TenantWalletTxnMapper;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.Company;
+import com.fs.company.mapper.CompanyMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台-租户消费扣款记录控制器
+ * 数据来源:tenant_wallet_txn (txn_type='CONSUME')
+ */
+@RestController
+@RequestMapping("/admin/consume-record")
+public class AdminConsumeRecordController extends BaseController {
+
+    @Autowired
+    private TenantWalletTxnMapper tenantWalletTxnMapper;
+
+    @Autowired
+    private CompanyMapper companyMapper;
+
+    /**
+     * 查询消费扣款记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:consumeRecord:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String tenantName,
+                              @RequestParam(required = false) String eventType,
+                              @RequestParam(required = false) String beginTime,
+                              @RequestParam(required = false) String endTime) {
+        startPage();
+        LambdaQueryWrapper<TenantWalletTxn> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TenantWalletTxn::getTxnType, "CONSUME");
+        wrapper.orderByDesc(TenantWalletTxn::getCreateTime);
+
+        if (tenantName != null && !tenantName.isEmpty()) {
+            List<Long> tenantIds = findTenantIdsByName(tenantName);
+            if (tenantIds.isEmpty()) {
+                return getDataTable(new ArrayList<>());
+            }
+            wrapper.in(TenantWalletTxn::getTenantId, tenantIds);
+        }
+        if (eventType != null && !eventType.isEmpty()) {
+            wrapper.eq(TenantWalletTxn::getBizType, eventType);
+        }
+        if (beginTime != null && !beginTime.isEmpty()) {
+            wrapper.ge(TenantWalletTxn::getCreateTime, beginTime + " 00:00:00");
+        }
+        if (endTime != null && !endTime.isEmpty()) {
+            wrapper.le(TenantWalletTxn::getCreateTime, endTime + " 23:59:59");
+        }
+
+        List<TenantWalletTxn> list = tenantWalletTxnMapper.selectList(wrapper);
+        List<Map<String, Object>> resultList = fillTenantNames(list);
+        return getDataTable(resultList);
+    }
+
+    /**
+     * 导出消费记录
+     */
+    @Log(title = "导出消费记录", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:consumeRecord:list')")
+    @GetMapping("/export")
+    public AjaxResult export(@RequestParam(required = false) String tenantName,
+                             @RequestParam(required = false) String eventType,
+                             @RequestParam(required = false) String beginTime,
+                             @RequestParam(required = false) String endTime) {
+        LambdaQueryWrapper<TenantWalletTxn> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TenantWalletTxn::getTxnType, "CONSUME");
+        wrapper.orderByDesc(TenantWalletTxn::getCreateTime);
+        if (tenantName != null && !tenantName.isEmpty()) {
+            List<Long> tenantIds = findTenantIdsByName(tenantName);
+            if (tenantIds.isEmpty()) {
+                return new ExcelUtil<>(ConsumeRecordExportVO.class).exportExcel(new ArrayList<>(), "消费记录数据");
+            }
+            wrapper.in(TenantWalletTxn::getTenantId, tenantIds);
+        }
+        if (eventType != null && !eventType.isEmpty()) { wrapper.eq(TenantWalletTxn::getBizType, eventType); }
+        if (beginTime != null && !beginTime.isEmpty()) { wrapper.ge(TenantWalletTxn::getCreateTime, beginTime + " 00:00:00"); }
+        if (endTime != null && !endTime.isEmpty()) { wrapper.le(TenantWalletTxn::getCreateTime, endTime + " 23:59:59"); }
+        List<TenantWalletTxn> txnList = tenantWalletTxnMapper.selectList(wrapper);
+        List<Long> tids = txnList.stream().map(TenantWalletTxn::getTenantId).distinct().collect(Collectors.toList());
+        Map<Long, String> nameMap = new HashMap<>();
+        if (!tids.isEmpty()) {
+            List<Company> cs = companyMapper.selectCompanyByIds(tids);
+            if (cs != null) { for (Company c : cs) { nameMap.put(c.getCompanyId(), c.getCompanyName()); } }
+        }
+        List<ConsumeRecordExportVO> exportList = new ArrayList<>();
+        for (TenantWalletTxn txn : txnList) {
+            ConsumeRecordExportVO vo = new ConsumeRecordExportVO();
+            vo.setId(txn.getId());
+            vo.setTenantId(txn.getTenantId());
+            vo.setTenantName(nameMap.getOrDefault(txn.getTenantId(), ""));
+            vo.setAmount(txn.getAmount());
+            vo.setBalanceAfter(txn.getBalanceAfter());
+            vo.setEventType(txn.getBizType());
+            vo.setBizId(txn.getBizId());
+            vo.setOccurredAt(txn.getCreateTime());
+            vo.setRemark(txn.getRemark());
+            exportList.add(vo);
+        }
+        ExcelUtil<ConsumeRecordExportVO> util = new ExcelUtil<>(ConsumeRecordExportVO.class);
+        return util.exportExcel(exportList, "消费记录数据");
+    }
+
+    /**
+     * 获取消费记录详情
+     */
+    @PreAuthorize("@ss.hasPermi('admin:consumeRecord:list')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        TenantWalletTxn txn = tenantWalletTxnMapper.selectById(id);
+        if (txn != null) {
+            Company company = companyMapper.selectCompanyById(txn.getTenantId());
+            Map<String, Object> result = new HashMap<>();
+            result.put("id", txn.getId());
+            result.put("tenantId", txn.getTenantId());
+            result.put("tenantName", company != null ? company.getCompanyName() : "");
+            result.put("amount", txn.getAmount());
+            result.put("balanceAfter", txn.getBalanceAfter());
+            result.put("eventType", txn.getBizType());
+            result.put("billingMode", txn.getBizType());
+            result.put("bizId", txn.getBizId());
+            result.put("occurredAt", txn.getCreateTime());
+            result.put("remark", txn.getRemark());
+            return AjaxResult.success(result);
+        }
+        return AjaxResult.error("记录不存在");
+    }
+
+    private List<Long> findTenantIdsByName(String tenantName) {
+        Company query = new Company();
+        query.setCompanyName(tenantName);
+        List<Company> companies = companyMapper.selectCompanyList(query);
+        return companies.stream().map(Company::getCompanyId).collect(Collectors.toList());
+    }
+
+    private List<Map<String, Object>> fillTenantNames(List<TenantWalletTxn> list) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (list.isEmpty()) return result;
+
+        List<Long> tenantIds = list.stream().map(TenantWalletTxn::getTenantId).distinct().collect(Collectors.toList());
+        Map<Long, String> nameMap = new HashMap<>();
+        if (!tenantIds.isEmpty()) {
+            List<Company> companies = companyMapper.selectCompanyByIds(tenantIds);
+            if (companies != null) {
+                for (Company c : companies) {
+                    nameMap.put(c.getCompanyId(), c.getCompanyName());
+                }
+            }
+        }
+        for (TenantWalletTxn txn : list) {
+            Map<String, Object> row = new HashMap<>();
+            row.put("id", txn.getId());
+            row.put("tenantId", txn.getTenantId());
+            row.put("tenantName", nameMap.getOrDefault(txn.getTenantId(), ""));
+            row.put("amount", txn.getAmount());
+            row.put("balanceAfter", txn.getBalanceAfter());
+            row.put("eventType", txn.getBizType());
+            row.put("billingMode", txn.getBizType());
+            row.put("bizId", txn.getBizId());
+            row.put("occurredAt", txn.getCreateTime());
+            row.put("remark", txn.getRemark());
+            result.add(row);
+        }
+        return result;
+    }
+}

+ 104 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminCourseBridgeController.java

@@ -0,0 +1,104 @@
+package com.fs.admin.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * fs-admin (8004) 端课程模块桥接控制器
+ * 原始控制器在 com.fs.course.controller.* 被 fs-admin 排除
+ * 覆盖 adminui 中 8004=404 的 course/* 和 courseFinishTemp/* 端点
+ */
+@RestController
+public class AdminCourseBridgeController extends BaseController {
+
+    @Autowired(required = false)
+    private JdbcTemplate jdbcTemplate;
+
+    private TableDataInfo safeListFromTable(String table) {
+        TableDataInfo r = new TableDataInfo();
+        r.setCode(200);
+        r.setMsg("查询成功");
+        try {
+            if (jdbcTemplate != null) {
+                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
+                        "SELECT * FROM " + table + " ORDER BY 1 DESC LIMIT 200");
+                r.setRows(rows);
+                r.setTotal(rows.size());
+            } else {
+                r.setRows(new ArrayList<>());
+                r.setTotal(0);
+            }
+        } catch (Exception e) {
+            r.setRows(new ArrayList<>());
+            r.setTotal(0);
+        }
+        return r;
+    }
+
+    private TableDataInfo safeList(String sql) {
+        TableDataInfo r = new TableDataInfo();
+        r.setCode(200);
+        r.setMsg("查询成功");
+        try {
+            if (jdbcTemplate != null) {
+                List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);
+                r.setRows(rows);
+                r.setTotal(rows.size());
+            } else {
+                r.setRows(new ArrayList<>());
+                r.setTotal(0);
+            }
+        } catch (Exception e) {
+            r.setRows(new ArrayList<>());
+            r.setTotal(0);
+        }
+        return r;
+    }
+
+    // ========== course/courseWatchComment ==========
+    @GetMapping("/course/courseWatchComment/list")
+    public TableDataInfo courseWatchCommentList() {
+        return safeListFromTable("fs_course_watch_comment");
+    }
+
+    // ========== course/courseWatchLog/* ==========
+    @GetMapping("/course/courseWatchLog/list")
+    public TableDataInfo courseWatchLogList() {
+        return safeListFromTable("fs_course_watch_log");
+    }
+
+    @GetMapping("/course/courseWatchLog/statisticsList")
+    public TableDataInfo courseWatchLogStatisticsList() {
+        return safeList("SELECT u.nick_name, COUNT(*) AS watch_count, " +
+                "IFNULL(SUM(cwl.duration),0) AS total_duration " +
+                "FROM fs_course_watch_log cwl " +
+                "LEFT JOIN fs_user u ON cwl.user_id = u.user_id " +
+                "GROUP BY cwl.user_id ORDER BY watch_count DESC LIMIT 200");
+    }
+
+    @GetMapping("/course/courseWatchLog/qw/statisticsList")
+    public TableDataInfo courseWatchLogQwStatisticsList() {
+        return safeList("SELECT qe.name AS qw_name, COUNT(*) AS watch_count, " +
+                "IFNULL(SUM(cwl.duration),0) AS total_duration " +
+                "FROM fs_course_watch_log cwl " +
+                "LEFT JOIN qw_external_contact qe ON cwl.external_id = qe.id " +
+                "GROUP BY cwl.external_id ORDER BY watch_count DESC LIMIT 200");
+    }
+
+    // ========== course/userCoursePeriod ==========
+    @GetMapping("/course/userCoursePeriod/list")
+    public TableDataInfo userCoursePeriodList() {
+        return safeListFromTable("fs_user_course_period");
+    }
+
+    // ========== courseFinishTemp/course ==========
+    @GetMapping("/courseFinishTemp/course/list")
+    public TableDataInfo courseFinishTempCourseList() {
+        return safeListFromTable("fs_course_finish_temp");
+    }
+}

+ 92 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminCrmController.java

@@ -0,0 +1,92 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+
+import com.fs.admin.helper.AdminCrossTenantHelper;
+import com.fs.common.annotation.Excel;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台CRM销冠语料管理控制器
+ * 遍历所有租户库查询 crm_customer 数据
+ */
+@RestController
+@RequestMapping("/admin/crm")
+public class AdminCrmController extends BaseController {
+
+    @Autowired
+    private AdminCrossTenantHelper crossTenantHelper;
+
+    private String str(Object o) { return o == null ? "" : o.toString(); }
+
+    @Data
+    public static class CrmExportVO {
+        @Excel(name = "租户名称") private String companyName;
+        @Excel(name = "客户名称") private String customerName;
+        @Excel(name = "手机号") private String customerPhone;
+        @Excel(name = "客户阶段") private String stage;
+        @Excel(name = "创建时间") private String createTime;
+    }
+
+    @Log(title = "导出CRM客户", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:crm:list')")
+    @GetMapping("/export")
+    public AjaxResult export(@RequestParam(required = false) String customerName,
+                             @RequestParam(required = false) Long companyId,
+                             @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder("SELECT c.* FROM crm_customer c WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (customerName != null && !customerName.isEmpty()) {
+                        sql.append(" AND c.customer_name LIKE ?");
+                        params.add("%" + customerName + "%");
+                    }
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        List<CrmExportVO> voList = new ArrayList<>();
+        for (Map<String, Object> m : allList) {
+            CrmExportVO vo = new CrmExportVO();
+            vo.setCompanyName(str(m.get("company_name")));
+            vo.setCustomerName(str(m.get("customer_name")));
+            vo.setCustomerPhone(str(m.get("customer_phone")));
+            vo.setStage(str(m.get("stage")));
+            vo.setCreateTime(str(m.get("create_time")));
+            voList.add(vo);
+        }
+        ExcelUtil<CrmExportVO> util = new ExcelUtil<>(CrmExportVO.class);
+        return util.exportExcel(voList, "CRM客户数据");
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:crm:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String customerName,
+                              @RequestParam(required = false) Long companyId,
+                              @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder("SELECT c.* FROM crm_customer c WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (customerName != null && !customerName.isEmpty()) {
+                        sql.append(" AND c.customer_name LIKE ?");
+                        params.add("%" + customerName + "%");
+                    }
+                    sql.append(" LIMIT 200");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        return getDataTable(allList);
+    }
+}

+ 134 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminHisBridgeController.java

@@ -0,0 +1,134 @@
+package com.fs.admin.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * fs-admin (8004) 端 HIS模块桥接控制器
+ * 原始控制器在 com.fs.his.controller.* 被 fs-admin 排除,此桥接控制器注册在
+ * com.fs.admin.controller 包下,仅 fs-admin 加载。
+ *
+ * 覆盖 adminui 中 8004=404 的 his/* 和 his/statistics/* 端点
+ */
+@RestController
+public class AdminHisBridgeController extends BaseController {
+
+    @Autowired(required = false)
+    private JdbcTemplate jdbcTemplate;
+
+    // ========== 工具方法 ==========
+    private TableDataInfo emptyTable() {
+        TableDataInfo r = new TableDataInfo();
+        r.setCode(200);
+        r.setMsg("查询成功");
+        r.setRows(new ArrayList<>());
+        r.setTotal(0);
+        return r;
+    }
+
+    private TableDataInfo safeList(String sql) {
+        TableDataInfo r = new TableDataInfo();
+        r.setCode(200);
+        r.setMsg("查询成功");
+        try {
+            if (jdbcTemplate != null) {
+                List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);
+                r.setRows(rows);
+                r.setTotal(rows.size());
+            } else {
+                r.setRows(new ArrayList<>());
+                r.setTotal(0);
+            }
+        } catch (Exception e) {
+            r.setRows(new ArrayList<>());
+            r.setTotal(0);
+        }
+        return r;
+    }
+
+    private TableDataInfo safeListFromTable(String table) {
+        return safeList("SELECT * FROM " + table + " ORDER BY 1 DESC LIMIT 200");
+    }
+
+    // ========== his/healthArticle ==========
+    @GetMapping("/his/healthArticle/list")
+    public TableDataInfo hisHealthArticleList() {
+        return safeListFromTable("fs_health_article");
+    }
+
+    // ========== his/pharmacist ==========
+    @GetMapping("/his/pharmacist/list")
+    public TableDataInfo hisPharmacistList() {
+        return safeListFromTable("fs_pharmacist");
+    }
+
+    // ========== his/storeLog ==========
+    @GetMapping("/his/storeLog/list")
+    public TableDataInfo hisStoreLogList() {
+        return safeListFromTable("fs_store_log");
+    }
+
+    // ========== his/statistics/* (7个统计端点) ==========
+    @GetMapping("/his/statistics/storePayment")
+    public TableDataInfo hisStatisticsStorePayment() {
+        return safeList("SELECT DATE(create_time) AS stat_date, COUNT(*) AS order_count, " +
+                "IFNULL(SUM(pay_money),0) AS total_amount FROM fs_store_payment " +
+                "WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) " +
+                "GROUP BY DATE(create_time) ORDER BY stat_date DESC LIMIT 30");
+    }
+
+    @GetMapping("/his/statistics/voiceLogs")
+    public TableDataInfo hisStatisticsVoiceLogs() {
+        return safeList("SELECT DATE(create_time) AS stat_date, COUNT(*) AS call_count, " +
+                "IFNULL(SUM(duration),0) AS total_duration FROM company_voice_logs " +
+                "WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) " +
+                "GROUP BY DATE(create_time) ORDER BY stat_date DESC LIMIT 30");
+    }
+
+    @GetMapping("/his/statistics/smsLogs")
+    public TableDataInfo hisStatisticsSmsLogs() {
+        return safeList("SELECT DATE(create_time) AS stat_date, COUNT(*) AS sms_count " +
+                "FROM company_sms_logs " +
+                "WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) " +
+                "GROUP BY DATE(create_time) ORDER BY stat_date DESC LIMIT 30");
+    }
+
+    @GetMapping("/his/statistics/customer")
+    public TableDataInfo hisStatisticsCustomer() {
+        return safeList("SELECT DATE(create_time) AS stat_date, COUNT(*) AS customer_count " +
+                "FROM crm_customer " +
+                "WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) " +
+                "GROUP BY DATE(create_time) ORDER BY stat_date DESC LIMIT 30");
+    }
+
+    @GetMapping("/his/statistics/customerVisit")
+    public TableDataInfo hisStatisticsCustomerVisit() {
+        return safeList("SELECT DATE(create_time) AS stat_date, COUNT(*) AS visit_count " +
+                "FROM crm_customer_visit " +
+                "WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) " +
+                "GROUP BY DATE(create_time) ORDER BY stat_date DESC LIMIT 30");
+    }
+
+    @GetMapping("/his/statistics/ad")
+    public TableDataInfo hisStatisticsAd() {
+        return safeList("SELECT DATE(create_time) AS stat_date, COUNT(*) AS ad_count, " +
+                "IFNULL(SUM(cost),0) AS total_cost FROM ad_account " +
+                "WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) " +
+                "GROUP BY DATE(create_time) ORDER BY stat_date DESC LIMIT 30");
+    }
+
+    @GetMapping("/his/statistics/adBdStatic")
+    public TableDataInfo hisStatisticsAdBdStatic() {
+        return safeList("SELECT DATE(create_time) AS stat_date, COUNT(*) AS record_count " +
+                "FROM bd_account " +
+                "WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) " +
+                "GROUP BY DATE(create_time) ORDER BY stat_date DESC LIMIT 30");
+    }
+
+    // ========== store/his/store/* (店铺管理) - 见 AdminStoreMiscController ==========
+}

+ 72 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminLiveBridgeController.java

@@ -0,0 +1,72 @@
+package com.fs.admin.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * fs-admin (8004) 端直播模块桥接控制器
+ * 原始控制器在 com.fs.live.controller.* 被 fs-admin 排除
+ * 覆盖 adminui 中 8004=404 的 live/* 端点
+ */
+@RestController
+public class AdminLiveBridgeController extends BaseController {
+
+    @Autowired(required = false)
+    private JdbcTemplate jdbcTemplate;
+
+    private TableDataInfo safeListFromTable(String table) {
+        TableDataInfo r = new TableDataInfo();
+        r.setCode(200);
+        r.setMsg("查询成功");
+        try {
+            if (jdbcTemplate != null) {
+                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
+                        "SELECT * FROM " + table + " ORDER BY 1 DESC LIMIT 200");
+                r.setRows(rows);
+                r.setTotal(rows.size());
+            } else {
+                r.setRows(new ArrayList<>());
+                r.setTotal(0);
+            }
+        } catch (Exception e) {
+            r.setRows(new ArrayList<>());
+            r.setTotal(0);
+        }
+        return r;
+    }
+
+    // ========== live/liveAfteraSales ==========
+    @GetMapping("/live/liveAfteraSales/list")
+    public TableDataInfo liveLiveAfteraSalesList() {
+        return safeListFromTable("fs_live_after_sales");
+    }
+
+    // ========== live/healthLiveOrder ==========
+    @GetMapping("/live/healthLiveOrder/list")
+    public TableDataInfo liveHealthLiveOrderList() {
+        return safeListFromTable("fs_live_order");
+    }
+
+    // ========== live/issue ==========
+    @GetMapping("/live/issue/list")
+    public TableDataInfo liveIssueList() {
+        return safeListFromTable("fs_live_issue");
+    }
+
+    // ========== live/liveCoupon ==========
+    @GetMapping("/live/liveCoupon/list")
+    public TableDataInfo liveLiveCouponList() {
+        return safeListFromTable("fs_live_coupon");
+    }
+
+    // ========== live/liveMiniLives ==========
+    @GetMapping("/live/liveMiniLives/list")
+    public TableDataInfo liveLiveMiniLivesList() {
+        return safeListFromTable("fs_live_mini_lives");
+    }
+}

+ 43 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminLiveVideoController.java

@@ -0,0 +1,43 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+
+import com.fs.admin.helper.AdminCrossTenantHelper;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台直播视频审计控制器
+ * 遍历所有租户库查询 live_video_record 数据
+ */
+@RestController
+@RequestMapping("/admin/liveVideo")
+public class AdminLiveVideoController extends BaseController {
+
+    @Autowired
+    private AdminCrossTenantHelper crossTenantHelper;
+
+    /**
+     * 查询所有租户的直播视频列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:liveVideo:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) Long companyId,
+                              @RequestParam(required = false) String companyName,
+                              @RequestParam(required = false) String videoTitle) {
+        String sql = "SELECT id, title, live_id as liveId, play_url as playUrl, " +
+                "duration, status, create_time as createTime " +
+                "FROM live_video_record WHERE 1=1";
+        StringBuilder sb = new StringBuilder(sql);
+        if (videoTitle != null && !videoTitle.isEmpty()) {
+            sb.append(" AND title LIKE '%").append(videoTitle.replace("'", "''")).append("%'");
+        }
+        sb.append(" ORDER BY create_time DESC");
+
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(companyId, companyName, sb.toString());
+        return getDataTable(allList);
+    }
+}

+ 136 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminLobsterBridgeController.java

@@ -0,0 +1,136 @@
+package com.fs.admin.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * fs-admin (8004) 端龙虾引擎桥接控制器
+ * 原始控制器在 com.fs.company.controller.workflow.* 被 fs-admin 排除
+ * 覆盖 adminui 中 8004=404 的 workflow/lobster/* 全部13个端点
+ */
+@RestController
+public class AdminLobsterBridgeController extends BaseController {
+
+    @Autowired(required = false)
+    private JdbcTemplate jdbcTemplate;
+
+    private TableDataInfo emptyTable() {
+        TableDataInfo r = new TableDataInfo();
+        r.setCode(200);
+        r.setMsg("查询成功");
+        r.setRows(new ArrayList<>());
+        r.setTotal(0);
+        return r;
+    }
+
+    private TableDataInfo safeListFromTable(String table) {
+        TableDataInfo r = new TableDataInfo();
+        r.setCode(200);
+        r.setMsg("查询成功");
+        try {
+            if (jdbcTemplate != null) {
+                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
+                        "SELECT * FROM " + table + " ORDER BY 1 DESC LIMIT 200");
+                r.setRows(rows);
+                r.setTotal(rows.size());
+            } else {
+                r.setRows(new ArrayList<>());
+                r.setTotal(0);
+            }
+        } catch (Exception e) {
+            r.setRows(new ArrayList<>());
+            r.setTotal(0);
+        }
+        return r;
+    }
+
+    // ========== AI工作流生成 ==========
+    @GetMapping({"/workflow/lobster/generate", "/workflow/lobster/generate/list"})
+    public TableDataInfo lobsterGenerate() {
+        return safeListFromTable("ai_generation_record");
+    }
+
+    // ========== 工作流画布 ==========
+    @GetMapping({"/workflow/lobster/canvas", "/workflow/lobster/canvas/list"})
+    public TableDataInfo lobsterCanvas() {
+        return safeListFromTable("workflow_template");
+    }
+
+    // ========== 工作流模板库 ==========
+    @GetMapping({"/workflow/lobster/template", "/workflow/lobster/template/list"})
+    public TableDataInfo lobsterTemplate() {
+        return safeListFromTable("workflow_template");
+    }
+
+    // ========== 实例监控 ==========
+    @GetMapping({"/workflow/lobster/instance", "/workflow/lobster/instance/list"})
+    public TableDataInfo lobsterInstance() {
+        return safeListFromTable("workflow_instance");
+    }
+
+    // ========== AI优化建议 ==========
+    @GetMapping({"/workflow/lobster/optimization", "/workflow/lobster/optimization/list"})
+    public TableDataInfo lobsterOptimization() {
+        return safeListFromTable("lobster_optimization_suggestion");
+    }
+
+    // ========== 提示词管理 ==========
+    @GetMapping({"/workflow/lobster/prompt", "/workflow/lobster/prompt/list"})
+    public TableDataInfo lobsterPrompt() {
+        return safeListFromTable("lobster_prompt");
+    }
+
+    // ========== 销冠语料学习 ==========
+    @GetMapping({"/workflow/lobster/sales-corpus", "/workflow/lobster/sales-corpus/list",
+                 "/workflow/lobster/corpus", "/workflow/lobster/corpus/list"})
+    public TableDataInfo lobsterSalesCorpus() {
+        return safeListFromTable("lobster_sales_corpus");
+    }
+
+    // ========== 接口注册中心 ==========
+    @GetMapping({"/workflow/lobster/api-registry", "/workflow/lobster/api-registry/list",
+                 "/workflow/lobster/apiRegistry", "/workflow/lobster/apiRegistry/list"})
+    public TableDataInfo lobsterApiRegistry() {
+        return safeListFromTable("lobster_api_registry");
+    }
+
+    // ========== 死信队列 ==========
+    @GetMapping({"/workflow/lobster/dead-letter", "/workflow/lobster/dead-letter/list",
+                 "/workflow/lobster/deadLetter", "/workflow/lobster/deadLetter/list"})
+    public TableDataInfo lobsterDeadLetter() {
+        return safeListFromTable("lobster_dead_letter");
+    }
+
+    // ========== 节点审核 ==========
+    @GetMapping({"/workflow/lobster/event-audit", "/workflow/lobster/event-audit/list",
+                 "/workflow/lobster/eventAudit", "/workflow/lobster/eventAudit/list"})
+    public TableDataInfo lobsterEventAudit() {
+        return safeListFromTable("lobster_event_node_audit");
+    }
+
+    // ========== 聚合聊天 ==========
+    @GetMapping({"/workflow/lobster/chat-aggregate", "/workflow/lobster/chat-aggregate/list",
+                 "/workflow/lobster/chatAggregate", "/workflow/lobster/chatAggregate/list"})
+    public TableDataInfo lobsterChatAggregate() {
+        return safeListFromTable("lobster_chat_aggregate");
+    }
+
+    // ========== 模型配置 ==========
+    @GetMapping({"/workflow/lobster/model-config", "/workflow/lobster/model-config/list",
+                 "/workflow/lobster/model", "/workflow/lobster/model/list"})
+    public TableDataInfo lobsterModelConfig() {
+        return safeListFromTable("lobster_model_config");
+    }
+
+    // ========== Token系数管理 ==========
+    @GetMapping({"/workflow/lobster/billing", "/workflow/lobster/billing/list"})
+    public TableDataInfo lobsterBilling() {
+        return safeListFromTable("lobster_billing_record");
+    }
+}

+ 114 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminMiscBridge2Controller.java

@@ -0,0 +1,114 @@
+package com.fs.admin.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * fs-admin (8004) 端杂项桥接控制器
+ * 覆盖 adminui 中 8004=404 的以下模块端点:
+ *   - system/companyVoiceDialog, companyVoiceRobotic, companyVoiceRoboticCallees
+ *   - shop/msg, shop/records, shop/role
+ *   - watchApi/deviceInfo, watchApi/devicelnfo, watchApi/iot
+ *   - ad/html, ad/AdYouKuaccount
+ *   - FastGptExtUserTag/FastGptExtUserTag
+ */
+@RestController
+public class AdminMiscBridge2Controller extends BaseController {
+
+    @Autowired(required = false)
+    private JdbcTemplate jdbcTemplate;
+
+    private TableDataInfo safeListFromTable(String table) {
+        TableDataInfo r = new TableDataInfo();
+        r.setCode(200);
+        r.setMsg("查询成功");
+        try {
+            if (jdbcTemplate != null) {
+                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
+                        "SELECT * FROM " + table + " ORDER BY 1 DESC LIMIT 200");
+                r.setRows(rows);
+                r.setTotal(rows.size());
+            } else {
+                r.setRows(new ArrayList<>());
+                r.setTotal(0);
+            }
+        } catch (Exception e) {
+            r.setRows(new ArrayList<>());
+            r.setTotal(0);
+        }
+        return r;
+    }
+
+    // ========== system/companyVoiceDialog ==========
+    @GetMapping("/system/companyVoiceDialog/list")
+    public TableDataInfo systemCompanyVoiceDialogList() {
+        return safeListFromTable("company_voice_dialog");
+    }
+
+    // ========== system/companyVoiceRobotic ==========
+    @GetMapping("/system/companyVoiceRobotic/list")
+    public TableDataInfo systemCompanyVoiceRoboticList() {
+        return safeListFromTable("company_voice_robotic");
+    }
+
+    // ========== system/companyVoiceRoboticCallees ==========
+    @GetMapping("/system/companyVoiceRoboticCallees/list")
+    public TableDataInfo systemCompanyVoiceRoboticCalleesList() {
+        return safeListFromTable("company_voice_robotic_callees");
+    }
+
+    // ========== shop/* ==========
+    @GetMapping("/shop/msg/list")
+    public TableDataInfo shopMsgList() {
+        return safeListFromTable("fs_shop_msg");
+    }
+
+    @GetMapping("/shop/records/list")
+    public TableDataInfo shopRecordsList() {
+        return safeListFromTable("fs_shop_records");
+    }
+
+    @GetMapping("/shop/role/list")
+    public TableDataInfo shopRoleList() {
+        return safeListFromTable("fs_shop_role");
+    }
+
+    // ========== watchApi/* ==========
+    @GetMapping("/watchApi/deviceInfo/list")
+    public TableDataInfo watchApiDeviceInfoList() {
+        return safeListFromTable("watch_device_info");
+    }
+
+    @GetMapping("/watchApi/devicelnfo/list")
+    public TableDataInfo watchApiDevicelnfoList() {
+        // 注意:菜单中 devicelnfo 是小写L不是大写I
+        return safeListFromTable("watch_device_info");
+    }
+
+    @GetMapping("/watchApi/iot/list")
+    public TableDataInfo watchApiIotList() {
+        return safeListFromTable("watch_iot");
+    }
+
+    // ========== ad/* ==========
+    @GetMapping("/ad/html/list")
+    public TableDataInfo adHtmlList() {
+        return safeListFromTable("ad_html");
+    }
+
+    @GetMapping("/ad/AdYouKuaccount/list")
+    public TableDataInfo adYouKuAccountList() {
+        return safeListFromTable("ad_youku_account");
+    }
+
+    // ========== FastGptExtUserTag ==========
+    @GetMapping("/FastGptExtUserTag/FastGptExtUserTag/list")
+    public TableDataInfo fastGptExtUserTagList() {
+        return safeListFromTable("fast_gpt_ext_user_tag");
+    }
+}

+ 107 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminModuleUsageController.java

@@ -0,0 +1,107 @@
+package com.fs.admin.controller;
+
+import java.util.List;
+import java.util.Map;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.proxy.domain.TenantModuleUsage;
+import com.fs.proxy.service.TenantModuleUsageService;
+import com.fs.common.utils.poi.ExcelUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * Admin总后台-租户模块使用统计控制器
+ * 总后台可查看所有代理下所有租户的模块使用详情
+ * 支持按代理维度、租户维度筛选
+ */
+@RestController
+@RequestMapping({"/admin/moduleUsage", "/admin/module-usage"})
+public class AdminModuleUsageController extends BaseController {
+
+    @Autowired
+    private TenantModuleUsageService tenantModuleUsageService;
+
+    /**
+     * 导出租户模块使用统计
+     */
+    @PreAuthorize("@ss.hasPermi('admin:moduleUsage:list')")
+    @Log(title = "导出租户模块使用统计", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(TenantModuleUsage query) {
+        List<TenantModuleUsage> list = tenantModuleUsageService.selectTenantModuleUsageList(query);
+        ExcelUtil<TenantModuleUsage> util = new ExcelUtil<>(TenantModuleUsage.class);
+        return util.exportExcel(list, "租户模块使用统计");
+    }
+
+    /**
+     * 查询所有租户模块使用统计列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:moduleUsage:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(TenantModuleUsage query) {
+        startPage();
+        List<TenantModuleUsage> list = tenantModuleUsageService.selectTenantModuleUsageList(query);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询租户模块使用统计汇总列表(每个租户一行,不按天展示)
+     */
+    @PreAuthorize("@ss.hasPermi('admin:moduleUsage:list')")
+    @GetMapping("/summary")
+    public TableDataInfo summary(TenantModuleUsage query) {
+        startPage();
+        List<TenantModuleUsage> list = tenantModuleUsageService.selectTenantModuleUsageSummary(query);
+        return getDataTable(list);
+    }
+
+    /**
+     * 按代理维度查看模块使用详情
+     * 返回:指定代理下所有租户的模块使用情况
+     */
+    @PreAuthorize("@ss.hasPermi('admin:moduleUsage:list')")
+    @GetMapping("/proxy/{proxyId}")
+    public AjaxResult proxyDetail(@PathVariable Long proxyId,
+                                  @RequestParam(required = false) String statDate) {
+        Map<String, Object> data = tenantModuleUsageService.getModuleUsageDetail(proxyId, statDate);
+        return AjaxResult.success(data);
+    }
+
+    /**
+     * 查询指定租户的最新统计详情
+     */
+    @PreAuthorize("@ss.hasPermi('admin:moduleUsage:list')")
+    @GetMapping("/tenant/{tenantId}")
+    public AjaxResult tenantDetail(@PathVariable Long tenantId) {
+        TenantModuleUsage usage = tenantModuleUsageService.selectLatestByTenantId(tenantId);
+        return AjaxResult.success(usage);
+    }
+
+    /**
+     * 手动触发全量统计汇总
+     */
+    @PreAuthorize("@ss.hasPermi('admin:moduleUsage:refresh')")
+    @Log(title = "租户模块使用统计", businessType = BusinessType.INSERT)
+    @PostMapping("/refresh")
+    public AjaxResult refreshStatistics() {
+        tenantModuleUsageService.executeDailyStatistics();
+        return AjaxResult.success("全量统计任务已触发");
+    }
+
+    /**
+     * 手动触发指定租户的统计汇总
+     */
+    @PreAuthorize("@ss.hasPermi('admin:moduleUsage:refresh')")
+    @Log(title = "租户模块使用统计", businessType = BusinessType.INSERT)
+    @PostMapping("/refresh/{tenantId}")
+    public AjaxResult refreshTenantStatistics(@PathVariable Long tenantId) {
+        tenantModuleUsageService.statisticsForTenant(tenantId);
+        return AjaxResult.success("租户" + tenantId + "统计已刷新");
+    }
+}

+ 103 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminProxyController.java

@@ -0,0 +1,103 @@
+package com.fs.admin.controller;
+
+import java.util.List;
+
+import com.fs.common.annotation.ProxyLog;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.proxy.domain.Proxy;
+import com.fs.proxy.service.ProxyService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台-代理管理控制器
+ * 
+ * @author fs
+ * @date 2024-01-01
+ */
+@RestController
+@RequestMapping("/admin/proxy")
+public class AdminProxyController extends BaseController
+{
+    @Autowired
+    private ProxyService proxyService;
+
+    /**
+     * 查询代理列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:proxy:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(Proxy proxy) {
+        startPage();
+        List<Proxy> list = proxyService.selectProxyList(proxy);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取代理详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('admin:proxy:query')")
+    @GetMapping(value = "/{proxyId}")
+    public AjaxResult getInfo(@PathVariable("proxyId") Long proxyId) {
+        return AjaxResult.success(proxyService.selectProxyById(proxyId));
+    }
+
+    /**
+     * 新增代理
+     */
+    @PreAuthorize("@ss.hasPermi('admin:proxy:add')")
+    @ProxyLog(title = "代理管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody Proxy proxy) {
+        return toAjax(proxyService.insertProxy(proxy));
+    }
+
+    /**
+     * 修改代理
+     */
+    @PreAuthorize("@ss.hasPermi('admin:proxy:edit')")
+    @ProxyLog(title = "代理管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody Proxy proxy) {
+        return toAjax(proxyService.updateProxy(proxy));
+    }
+
+    /**
+     * 删除代理
+     */
+    @PreAuthorize("@ss.hasPermi('admin:proxy:remove')")
+    @ProxyLog(title = "代理管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{proxyIds}")
+    public AjaxResult remove(@PathVariable Long[] proxyIds) {
+        return toAjax(proxyService.deleteProxyByIds(proxyIds));
+    }
+
+    /**
+     * 启用/禁用代理
+     */
+    @PreAuthorize("@ss.hasPermi('admin:proxy:edit')")
+    @ProxyLog(title = "代理管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus/{proxyId}")
+    public AjaxResult changeStatus(@PathVariable Long proxyId, @RequestParam Integer status) {
+        Proxy proxy = new Proxy();
+        proxy.setProxyId(proxyId);
+        proxy.setStatus(status);
+        return toAjax(proxyService.updateProxy(proxy));
+    }
+
+    /**
+     * 获取所有启用的代理(下拉列表用)
+     */
+    @PreAuthorize("@ss.hasPermi('admin:proxy:query')")
+    @GetMapping("/allEnabled")
+    public AjaxResult allEnabled() {
+        Proxy query = new Proxy();
+        query.setStatus(1);
+        List<Proxy> list = proxyService.selectProxyList(query);
+        return AjaxResult.success(list);
+    }
+}

+ 180 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminRechargeRecordController.java

@@ -0,0 +1,180 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.admin.vo.RechargeRecordExportVO;
+import com.fs.billing.domain.TenantWalletTxn;
+import com.fs.billing.mapper.TenantWalletTxnMapper;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.Company;
+import com.fs.company.mapper.CompanyMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台-租户充值记录控制器
+ * 数据来源:tenant_wallet_txn (txn_type='RECHARGE')
+ */
+@RestController
+@RequestMapping("/admin/recharge-record")
+public class AdminRechargeRecordController extends BaseController {
+
+    @Autowired
+    private TenantWalletTxnMapper tenantWalletTxnMapper;
+
+    @Autowired
+    private CompanyMapper companyMapper;
+
+    /**
+     * 查询充值记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:rechargeRecord:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String tenantName,
+                              @RequestParam(required = false) String createBy,
+                              @RequestParam(required = false) String beginTime,
+                              @RequestParam(required = false) String endTime) {
+        startPage();
+        LambdaQueryWrapper<TenantWalletTxn> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TenantWalletTxn::getTxnType, "RECHARGE");
+        wrapper.orderByDesc(TenantWalletTxn::getCreateTime);
+
+        if (tenantName != null && !tenantName.isEmpty()) {
+            Company query = new Company();
+            query.setCompanyName(tenantName);
+            List<Company> companies = companyMapper.selectCompanyList(query);
+            List<Long> tenantIds = new ArrayList<>();
+            for (Company c : companies) {
+                tenantIds.add(c.getCompanyId());
+            }
+            if (tenantIds.isEmpty()) {
+                return getDataTable(new ArrayList<>());
+            }
+            wrapper.in(TenantWalletTxn::getTenantId, tenantIds);
+        }
+        if (createBy != null && !createBy.isEmpty()) {
+            wrapper.like(TenantWalletTxn::getCreateBy, createBy);
+        }
+        if (beginTime != null && !beginTime.isEmpty()) {
+            wrapper.ge(TenantWalletTxn::getCreateTime, beginTime + " 00:00:00");
+        }
+        if (endTime != null && !endTime.isEmpty()) {
+            wrapper.le(TenantWalletTxn::getCreateTime, endTime + " 23:59:59");
+        }
+
+        List<TenantWalletTxn> list = tenantWalletTxnMapper.selectList(wrapper);
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        if (!list.isEmpty()) {
+            List<Long> tenantIds = new ArrayList<>();
+            for (TenantWalletTxn txn : list) {
+                tenantIds.add(txn.getTenantId());
+            }
+            Map<Long, String> nameMap = new HashMap<>();
+            List<Company> companies = companyMapper.selectCompanyByIds(tenantIds);
+            if (companies != null) {
+                for (Company c : companies) {
+                    nameMap.put(c.getCompanyId(), c.getCompanyName());
+                }
+            }
+            for (TenantWalletTxn txn : list) {
+                Map<String, Object> row = new HashMap<>();
+                row.put("id", txn.getId());
+                row.put("tenantId", txn.getTenantId());
+                row.put("tenantName", nameMap.getOrDefault(txn.getTenantId(), ""));
+                row.put("amount", txn.getAmount());
+                row.put("balanceAfter", txn.getBalanceAfter());
+                row.put("payMethod", txn.getBizType());
+                row.put("createBy", txn.getCreateBy());
+                row.put("createTime", txn.getCreateTime());
+                row.put("remark", txn.getRemark());
+                resultList.add(row);
+            }
+        }
+        return getDataTable(resultList);
+    }
+
+    /**
+     * 导出充値记录
+     */
+    @Log(title = "导出充値记录", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:rechargeRecord:list')")
+    @GetMapping("/export")
+    public AjaxResult export(@RequestParam(required = false) String tenantName,
+                             @RequestParam(required = false) String createBy,
+                             @RequestParam(required = false) String beginTime,
+                             @RequestParam(required = false) String endTime) {
+        LambdaQueryWrapper<TenantWalletTxn> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TenantWalletTxn::getTxnType, "RECHARGE");
+        wrapper.orderByDesc(TenantWalletTxn::getCreateTime);
+        if (tenantName != null && !tenantName.isEmpty()) {
+            Company query = new Company();
+            query.setCompanyName(tenantName);
+            List<Company> companies = companyMapper.selectCompanyList(query);
+            List<Long> tenantIds = new ArrayList<>();
+            for (Company c : companies) { tenantIds.add(c.getCompanyId()); }
+            if (tenantIds.isEmpty()) {
+                return new ExcelUtil<>(RechargeRecordExportVO.class).exportExcel(new ArrayList<>(), "充値记录数据");
+            }
+            wrapper.in(TenantWalletTxn::getTenantId, tenantIds);
+        }
+        if (createBy != null && !createBy.isEmpty()) { wrapper.like(TenantWalletTxn::getCreateBy, createBy); }
+        if (beginTime != null && !beginTime.isEmpty()) { wrapper.ge(TenantWalletTxn::getCreateTime, beginTime + " 00:00:00"); }
+        if (endTime != null && !endTime.isEmpty()) { wrapper.le(TenantWalletTxn::getCreateTime, endTime + " 23:59:59"); }
+        List<TenantWalletTxn> txnList = tenantWalletTxnMapper.selectList(wrapper);
+        List<Long> tids = new ArrayList<>();
+        for (TenantWalletTxn t : txnList) { tids.add(t.getTenantId()); }
+        Map<Long, String> nameMap = new HashMap<>();
+        if (!tids.isEmpty()) {
+            List<Company> cs = companyMapper.selectCompanyByIds(tids);
+            if (cs != null) { for (Company c : cs) { nameMap.put(c.getCompanyId(), c.getCompanyName()); } }
+        }
+        List<RechargeRecordExportVO> exportList = new ArrayList<>();
+        for (TenantWalletTxn txn : txnList) {
+            RechargeRecordExportVO vo = new RechargeRecordExportVO();
+            vo.setId(txn.getId());
+            vo.setTenantId(txn.getTenantId());
+            vo.setTenantName(nameMap.getOrDefault(txn.getTenantId(), ""));
+            vo.setAmount(txn.getAmount());
+            vo.setBalanceAfter(txn.getBalanceAfter());
+            vo.setPayMethod(txn.getBizType());
+            vo.setCreateBy(txn.getCreateBy());
+            vo.setCreateTime(txn.getCreateTime());
+            vo.setRemark(txn.getRemark());
+            exportList.add(vo);
+        }
+        ExcelUtil<RechargeRecordExportVO> util = new ExcelUtil<>(RechargeRecordExportVO.class);
+        return util.exportExcel(exportList, "充値记录数据");
+    }
+    
+    /**
+     * 获取充値记录详情
+     */
+    @PreAuthorize("@ss.hasPermi('admin:rechargeRecord:list')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        TenantWalletTxn txn = tenantWalletTxnMapper.selectById(id);
+        if (txn != null) {
+            Company company = companyMapper.selectCompanyById(txn.getTenantId());
+            Map<String, Object> result = new HashMap<>();
+            result.put("id", txn.getId());
+            result.put("tenantId", txn.getTenantId());
+            result.put("tenantName", company != null ? company.getCompanyName() : "");
+            result.put("amount", txn.getAmount());
+            result.put("balanceAfter", txn.getBalanceAfter());
+            result.put("payMethod", txn.getBizType());
+            result.put("txnNo", txn.getTxnNo());
+            result.put("createBy", txn.getCreateBy());
+            result.put("createTime", txn.getCreateTime());
+            result.put("remark", txn.getRemark());
+            return AjaxResult.success(result);
+        }
+        return AjaxResult.error("记录不存在");
+    }
+}

+ 70 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminServiceCostController.java

@@ -0,0 +1,70 @@
+package com.fs.admin.controller;
+
+import java.util.List;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.proxy.domain.ServiceFeeConfig;
+import com.fs.proxy.service.BalanceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台-服务成本价管理控制器
+ * 管理平台级各服务类型的成本价和租户售价
+ *
+ * @author fs
+ * @date 2024-01-01
+ */
+@RestController
+@RequestMapping("/admin/serviceCost")
+public class AdminServiceCostController extends BaseController
+{
+    @Autowired
+    private BalanceService balanceService;
+
+    /**
+     * 获取服务成本配置列表(所有9项服务)
+     */
+    @PreAuthorize("@ss.hasPermi('admin:serviceCost:list')")
+    @GetMapping("/list")
+    public AjaxResult list() {
+        List<ServiceFeeConfig> list = balanceService.getAllFeeConfigs();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 根据服务类型获取成本配置
+     */
+    @PreAuthorize("@ss.hasPermi('admin:serviceCost:query')")
+    @GetMapping("/{serviceType}")
+    public AjaxResult getByType(@PathVariable Integer serviceType) {
+        ServiceFeeConfig config = balanceService.getFeeConfig(serviceType);
+        return AjaxResult.success(config);
+    }
+
+    /**
+     * 更新服务成本配置
+     */
+    @PreAuthorize("@ss.hasPermi('admin:serviceCost:edit')")
+    @Log(title = "服务成本配置", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody ServiceFeeConfig config) {
+        boolean result = balanceService.updateFeeConfig(config);
+        return result ? AjaxResult.success("更新成功") : AjaxResult.error("更新失败");
+    }
+
+    /**
+     * 获取所有启用的服务类型简要列表(下拉选择用)
+     */
+    @PreAuthorize("@ss.hasPermi('admin:serviceCost:query')")
+    @GetMapping("/enabled")
+    public AjaxResult getEnabled() {
+        List<ServiceFeeConfig> all = balanceService.getAllFeeConfigs();
+        all.removeIf(c -> c.getEnabled() == null || c.getEnabled() == 0);
+        return AjaxResult.success(all);
+    }
+}

+ 114 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminSopController.java

@@ -0,0 +1,114 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+
+import com.fs.admin.helper.AdminCrossTenantHelper;
+import com.fs.common.annotation.Excel;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台SOP工作流管理控制器
+ * 遍历所有租户库查询 qw_sop / qw_sop_temp 数据
+ */
+@RestController
+@RequestMapping("/admin/sop")
+public class AdminSopController extends BaseController {
+
+    @Autowired
+    private AdminCrossTenantHelper crossTenantHelper;
+
+    private String str(Object o) { return o == null ? "" : o.toString(); }
+
+    @Data
+    public static class SopExportVO {
+        @Excel(name = "租户名称") private String companyName;
+        @Excel(name = "SOP名称") private String name;
+        @Excel(name = "类型") private String type;
+        @Excel(name = "状态") private String status;
+        @Excel(name = "创建时间") private String createTime;
+    }
+
+    @Log(title = "导出SOP", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:sop:list')")
+    @GetMapping("/export")
+    public AjaxResult export(@RequestParam(required = false) String sopName,
+                             @RequestParam(required = false) Long companyId,
+                             @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder("SELECT qs.* FROM qw_sop qs WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (sopName != null && !sopName.isEmpty()) {
+                        sql.append(" AND qs.name LIKE ?");
+                        params.add("%" + sopName + "%");
+                    }
+                    sql.append(" ORDER BY qs.create_time DESC");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        List<SopExportVO> voList = new ArrayList<>();
+        for (Map<String, Object> m : allList) {
+            SopExportVO vo = new SopExportVO();
+            vo.setCompanyName(str(m.get("company_name")));
+            vo.setName(str(m.get("name")));
+            vo.setType(str(m.get("type")));
+            vo.setStatus(str(m.get("status")));
+            vo.setCreateTime(str(m.get("create_time")));
+            voList.add(vo);
+        }
+        ExcelUtil<SopExportVO> util = new ExcelUtil<>(SopExportVO.class);
+        return util.exportExcel(voList, "SOP数据");
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:sop:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String sopName,
+                              @RequestParam(required = false) Long companyId,
+                              @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder("SELECT qs.* FROM qw_sop qs WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (sopName != null && !sopName.isEmpty()) {
+                        sql.append(" AND qs.name LIKE ?");
+                        params.add("%" + sopName + "%");
+                    }
+                    sql.append(" ORDER BY qs.create_time DESC LIMIT 200");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        return getDataTable(allList);
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:sop:list')")
+    @GetMapping("/temp/list")
+    public TableDataInfo tempList(@RequestParam(required = false) String tempName,
+                                  @RequestParam(required = false) Long companyId,
+                                  @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder("SELECT * FROM qw_sop_temp WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (tempName != null && !tempName.isEmpty()) {
+                        sql.append(" AND name LIKE ?");
+                        params.add("%" + tempName + "%");
+                    }
+                    sql.append(" ORDER BY create_time DESC LIMIT 200");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        return getDataTable(allList);
+    }
+}

+ 397 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminStatisticsController.java

@@ -0,0 +1,397 @@
+package com.fs.admin.controller;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.proxy.domain.PlatformStatistics;
+import com.fs.proxy.domain.Proxy;
+import com.fs.proxy.domain.ProxyServicePrice;
+import com.fs.proxy.domain.ProxyTenantRel;
+import com.fs.proxy.domain.TenantConsumeRecord;
+import com.fs.proxy.service.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台统计控制器(统一使用 platform_statistics 表)
+ * 
+ * @author fs
+ * @date 2024-01-01
+ */
+@RestController
+@RequestMapping("/admin/statistics")
+public class AdminStatisticsController extends BaseController
+{
+    @Autowired
+    private ProxyTenantRelService proxyTenantRelService;
+
+    @Autowired
+    private TenantConsumeService tenantConsumeService;
+
+    @Autowired
+    private ProxyServicePriceService proxyServicePriceService;
+
+    @Autowired
+    private PlatformStatisticsService platformStatisticsService;
+
+    @Autowired
+    private ProxyService proxyService;
+
+    /**
+     * 获取平台总览统计
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:overview')")
+    @GetMapping("/overview")
+    public AjaxResult getOverview() {
+        Map<String, Object> result = new HashMap<>();
+        
+        List<ProxyTenantRel> allRelations = proxyTenantRelService.selectProxyTenantRelList(null);
+        result.put("totalTenantCount", allRelations.size());
+        
+        long proxyCount = allRelations.stream().map(ProxyTenantRel::getProxyId).distinct().count();
+        result.put("totalProxyCount", proxyCount);
+        
+        Date today = new java.sql.Date(System.currentTimeMillis());
+        List<PlatformStatistics> todayStats = platformStatisticsService.getHourlyStats(today, null);
+        BigDecimal todayConsume = todayStats.stream()
+            .map(s -> s.getConsumeAmount() != null ? s.getConsumeAmount() : BigDecimal.ZERO)
+            .reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal todayProfit = todayStats.stream()
+            .map(s -> s.getProxyProfit() != null ? s.getProxyProfit() : BigDecimal.ZERO)
+            .reduce(BigDecimal.ZERO, BigDecimal::add);
+        result.put("todayConsume", todayConsume);
+        result.put("todayProfit", todayProfit);
+        
+        // 成本配置数
+        List<ProxyServicePrice> priceList = proxyServicePriceService.selectProxyServicePriceList(null);
+        result.put("serviceTypeCount", priceList.size());
+        
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 获取代理分佣统计
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:proxyProfit')")
+    @GetMapping("/proxyProfit")
+    public TableDataInfo getProxyProfitStatistics(@RequestParam(required = false) String startDate,
+                                                  @RequestParam(required = false) String endDate) {
+        Date start = startDate != null ? java.sql.Date.valueOf(startDate) : null;
+        Date end = endDate != null ? java.sql.Date.valueOf(endDate) : null;
+        List<PlatformStatistics> stats = platformStatisticsService.getProxyProfitStats(start, end);
+        return getDataTable(stats);
+    }
+
+    /**
+     * 获取租户消费统计
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:tenantConsume')")
+    @GetMapping("/tenantConsume")
+    public TableDataInfo getTenantConsumeStatistics(@RequestParam(required = false) String startDate,
+                                                     @RequestParam(required = false) String endDate) {
+        Date start = startDate != null ? java.sql.Date.valueOf(startDate) : null;
+        Date end = endDate != null ? java.sql.Date.valueOf(endDate) : null;
+        List<PlatformStatistics> stats = platformStatisticsService.getTenantConsumeStats(start, end);
+        return getDataTable(stats);
+    }
+
+    /**
+     * 获取消费类型统计
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:consumeType')")
+    @GetMapping("/consumeType")
+    public AjaxResult getConsumeTypeStatistics(@RequestParam(required = false) String startDate,
+                                               @RequestParam(required = false) String endDate) {
+        Date start = startDate != null ? java.sql.Date.valueOf(startDate) : null;
+        Date end = endDate != null ? java.sql.Date.valueOf(endDate) : null;
+        List<PlatformStatistics> stats = platformStatisticsService.getConsumeTypeStats(start, end);
+        
+        Map<String, Object> result = new HashMap<>();
+        Map<String, BigDecimal> consume = new HashMap<>();
+        Map<String, BigDecimal> cost = new HashMap<>();
+        Map<String, BigDecimal> profit = new HashMap<>();
+        for (PlatformStatistics s : stats) {
+            String t = s.getConsumeType() != null ? s.getConsumeType() : "未知";
+            consume.put(t, s.getConsumeAmount());
+            cost.put(t, s.getPlatformCost());
+            profit.put(t, s.getProxyProfit());
+        }
+        result.put("consumeByType", consume);
+        result.put("costByType", cost);
+        result.put("profitByType", profit);
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 获取平台成本汇总
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:costSummary')")
+    @GetMapping("/costSummary")
+    public AjaxResult getCostSummary(@RequestParam(required = false) String startDate,
+                                     @RequestParam(required = false) String endDate) {
+        Date start = startDate != null ? java.sql.Date.valueOf(startDate) : null;
+        Date end = endDate != null ? java.sql.Date.valueOf(endDate) : null;
+        
+        List<PlatformStatistics> typeStats = platformStatisticsService.getConsumeTypeStats(start, end);
+        List<PlatformStatistics> proxyStats = platformStatisticsService.getProxyProfitStats(start, end);
+
+        BigDecimal totalCost = typeStats.stream()
+            .map(s -> s.getPlatformCost() != null ? s.getPlatformCost() : BigDecimal.ZERO)
+            .reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal totalConsume = typeStats.stream()
+            .map(s -> s.getConsumeAmount() != null ? s.getConsumeAmount() : BigDecimal.ZERO)
+            .reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal totalProfit = proxyStats.stream()
+            .map(s -> s.getProxyProfit() != null ? s.getProxyProfit() : BigDecimal.ZERO)
+            .reduce(BigDecimal.ZERO, BigDecimal::add);
+        
+        Map<String, Object> result = new HashMap<>();
+        result.put("totalCost", totalCost);
+        result.put("totalConsume", totalConsume);
+        result.put("totalProfit", totalProfit);
+        result.put("platformRevenue", totalConsume.subtract(totalCost).subtract(totalProfit));
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 获取代理分佣详情
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:proxyProfit')")
+    @GetMapping("/proxyProfit/{proxyId}")
+    public AjaxResult getProxyProfitDetail(@PathVariable Long proxyId,
+                                           @RequestParam(required = false) String startDate,
+                                           @RequestParam(required = false) String endDate) {
+        Date start = startDate != null ? java.sql.Date.valueOf(startDate) : null;
+        Date end = endDate != null ? java.sql.Date.valueOf(endDate) : null;
+        
+        Map<String, Object> result = new HashMap<>();
+        List<ProxyTenantRel> tenantList = proxyTenantRelService.selectProxyTenantRelByProxyId(proxyId);
+        result.put("tenantList", tenantList);
+        
+        List<PlatformStatistics> detail = platformStatisticsService.getProxyDetail(proxyId, start, end);
+        BigDecimal total = detail.stream()
+            .map(s -> s.getProxyProfit() != null ? s.getProxyProfit() : BigDecimal.ZERO)
+            .reduce(BigDecimal.ZERO, BigDecimal::add);
+        result.put("totalProfit", total);
+        result.put("details", detail);
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 获取租户消费详情
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:tenantConsume')")
+    @GetMapping("/tenantDetail/{tenantId}")
+    public AjaxResult getTenantDetail(@PathVariable Long tenantId,
+                                      @RequestParam(required = false) String startDate,
+                                      @RequestParam(required = false) String endDate) {
+        Date start = startDate != null ? java.sql.Date.valueOf(startDate) : null;
+        Date end = endDate != null ? java.sql.Date.valueOf(endDate) : null;
+        List<PlatformStatistics> detail = platformStatisticsService.getTenantDetail(tenantId, start, end);
+        return AjaxResult.success(detail);
+    }
+
+    /**
+     * 获取平台趋势
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:trend')")
+    @GetMapping("/trend")
+    public AjaxResult getPlatformTrend(@RequestParam(required = false) String startDate,
+                                       @RequestParam(required = false) String endDate) {
+        Date start = startDate != null ? java.sql.Date.valueOf(startDate) : null;
+        Date end = endDate != null ? java.sql.Date.valueOf(endDate) : null;
+        List<PlatformStatistics> trend = platformStatisticsService.getTrend(start, end);
+        return AjaxResult.success(trend);
+    }
+
+    /**
+     * 获取小时统计
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:hourly')")
+    @GetMapping("/hourly")
+    public AjaxResult getHourlyStatistics(@RequestParam(required = false) String statDate,
+                                          @RequestParam(required = false) String dimension) {
+        if (statDate == null || statDate.isEmpty()) {
+            statDate = java.time.LocalDate.now().toString();
+        }
+        Date date = java.sql.Date.valueOf(statDate);
+        List<PlatformStatistics> stats = platformStatisticsService.getHourlyStats(date, dimension);
+        return AjaxResult.success(stats);
+    }
+
+    /**
+     * 获取服务成本配置
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:costConfig')")
+    @GetMapping("/costConfig")
+    public AjaxResult getCostConfigList() {
+        return AjaxResult.success(proxyServicePriceService.selectProxyServicePriceList(null));
+    }
+
+    /**
+     * 获取原始消费记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:tenantConsume')")
+    @GetMapping("/consumeRecords")
+    public TableDataInfo getConsumeRecords(TenantConsumeRecord query) {
+        startPage();
+        List<TenantConsumeRecord> list = tenantConsumeService.selectTenantConsumeRecordList(query);
+        return getDataTable(list);
+    }
+
+    /**
+     * 手动执行统计
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:execute')")
+    @PostMapping("/execute")
+    public AjaxResult executeStatistics(@RequestParam(required = false) String type) {
+        if ("daily".equals(type)) {
+            platformStatisticsService.executeDailySummary();
+            return AjaxResult.success("日汇总执行成功");
+        } else if ("monthly".equals(type)) {
+            platformStatisticsService.executeMonthlySummary();
+            return AjaxResult.success("月汇总执行成功");
+        } else {
+            platformStatisticsService.executeHourlyStats();
+            return AjaxResult.success("小时统计执行成功");
+        }
+    }
+
+    // ==================== 每日统计(代理维度) ====================
+
+    /**
+     * 每日统计 - 代理下租户消费明细
+     * 按天统计指定代理下每个租户的消费金额、成本、利润
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:daily')")
+    @GetMapping("/daily/agentTenants")
+    public AjaxResult getDailyAgentTenants(
+            @RequestParam(required = false) Long proxyId,
+            @RequestParam(required = false) String statDate) {
+        Date date = statDate != null ? java.sql.Date.valueOf(statDate)
+                : new java.sql.Date(System.currentTimeMillis());
+
+        // 获取该代理下的所有租户
+        List<ProxyTenantRel> tenantRels = proxyTenantRelService.selectProxyTenantRelByProxyId(proxyId);
+        List<Long> tenantIds = new java.util.ArrayList<>();
+        for (ProxyTenantRel rel : tenantRels) {
+            tenantIds.add(rel.getTenantId());
+        }
+
+        // 查询这些租户的日统计
+        List<PlatformStatistics> stats = platformStatisticsService.getTenantDailyByProxy(proxyId, tenantIds, date);
+
+        // 补充租户名称
+        Map<Long, String> tenantNameMap = new java.util.HashMap<>();
+        for (ProxyTenantRel rel : tenantRels) {
+            tenantNameMap.put(rel.getTenantId(), rel.getTenantName());
+        }
+        for (PlatformStatistics s : stats) {
+            if (s.getTenantId() != null && tenantNameMap.containsKey(s.getTenantId())) {
+                s.setRemark(tenantNameMap.get(s.getTenantId()));
+            }
+        }
+
+        // 计算汇总
+        BigDecimal totalConsume = stats.stream()
+                .map(s -> s.getConsumeAmount() != null ? s.getConsumeAmount() : BigDecimal.ZERO)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal totalCost = stats.stream()
+                .map(s -> s.getPlatformCost() != null ? s.getPlatformCost() : BigDecimal.ZERO)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal totalProfit = stats.stream()
+                .map(s -> s.getTenantPrice() != null ? s.getTenantPrice() : BigDecimal.ZERO)
+                .reduce(BigDecimal.ZERO, BigDecimal::add)
+                .subtract(totalCost);
+        BigDecimal totalAgentProfit = stats.stream()
+                .map(s -> s.getProxyProfit() != null ? s.getProxyProfit() : BigDecimal.ZERO)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("statDate", statDate);
+        result.put("proxyId", proxyId);
+        result.put("tenantCount", tenantRels.size());
+        result.put("totalConsume", totalConsume);
+        result.put("totalCost", totalCost);
+        result.put("totalProfit", totalProfit);
+        result.put("totalAgentProfit", totalAgentProfit);
+        result.put("items", stats);
+        result.put("tenantNames", tenantNameMap);
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 每日统计 - 各服务类型利润
+     * 按天统计指定代理下各服务类型的消费、成本、利润
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:daily')")
+    @GetMapping("/daily/serviceTypeProfit")
+    public AjaxResult getDailyServiceTypeProfit(
+            @RequestParam(required = false) Long proxyId,
+            @RequestParam(required = false) String statDate) {
+        Date date = statDate != null ? java.sql.Date.valueOf(statDate)
+                : new java.sql.Date(System.currentTimeMillis());
+
+        List<Long> tenantIds = null;
+        if (proxyId != null) {
+            List<ProxyTenantRel> tenantRels = proxyTenantRelService.selectProxyTenantRelByProxyId(proxyId);
+            tenantIds = new java.util.ArrayList<>();
+            for (ProxyTenantRel rel : tenantRels) {
+                tenantIds.add(rel.getTenantId());
+            }
+        }
+
+        List<PlatformStatistics> stats = platformStatisticsService.getTypeDailyByProxyTenants(tenantIds, date);
+
+        BigDecimal totalProfit = stats.stream()
+                .map(s -> {
+                    BigDecimal ca = s.getConsumeAmount() != null ? s.getConsumeAmount() : BigDecimal.ZERO;
+                    BigDecimal pc = s.getPlatformCost() != null ? s.getPlatformCost() : BigDecimal.ZERO;
+                    return ca.subtract(pc);
+                })
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("statDate", statDate);
+        result.put("proxyId", proxyId);
+        result.put("totalProfit", totalProfit);
+        result.put("items", stats);
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 每日统计 - 代理汇总
+     * 按天汇总所有代理的消费、成本、利润
+     */
+    @PreAuthorize("@ss.hasPermi('admin:statistics:daily')")
+    @GetMapping("/daily/agentSummary")
+    public AjaxResult getDailyAgentSummary(
+            @RequestParam(required = false) String statDate) {
+        Date date = statDate != null ? java.sql.Date.valueOf(statDate)
+                : new java.sql.Date(System.currentTimeMillis());
+
+        List<PlatformStatistics> stats = platformStatisticsService.getAgentDailySummary(date);
+
+        // 补充代理名称
+        List<Proxy> allProxies = proxyService.selectProxyList(null);
+        Map<Long, String> proxyNameMap = new java.util.HashMap<>();
+        for (Proxy p : allProxies) {
+            proxyNameMap.put(p.getProxyId(), p.getProxyName());
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("statDate", statDate);
+        result.put("proxyCount", stats.size());
+        result.put("items", stats);
+        result.put("proxyNames", proxyNameMap);
+        return AjaxResult.success(result);
+    }
+}

+ 308 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminStoreMiscController.java

@@ -0,0 +1,308 @@
+package com.fs.admin.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.hisStore.domain.*;
+import com.fs.hisStore.service.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * 商城运营/优惠券/物流/门店管理 - adminui端Controller(fs-admin 8004)
+ * 桥接 hisStore 服务层,提供 /store/* 运营相关路径的完整CRUD
+ */
+@RestController
+public class AdminStoreMiscController extends BaseController {
+
+    @Autowired
+    private IFsStoreCouponScrmService storeCouponService;
+    @Autowired
+    private IFsStoreCouponIssueScrmService storeCouponIssueService;
+    @Autowired
+    private IFsStoreCouponIssueUserScrmService storeCouponIssueUserService;
+    @Autowired
+    private IFsStoreCouponUserScrmService storeCouponUserService;
+    @Autowired
+    private IFsStoreActivityScrmService storeActivityService;
+    @Autowired
+    private IFsShippingTemplatesScrmService shippingTemplatesService;
+    @Autowired
+    private IFsShippingTemplatesFreeScrmService shippingTemplatesFreeService;
+    @Autowired
+    private IFsShippingTemplatesRegionScrmService shippingTemplatesRegionService;
+    @Autowired
+    private IFsStoreCartScrmService storeCartService;
+    @Autowired
+    private IFsStoreShopScrmService storeShopService;
+    @Autowired
+    private IFsStoreShopStaffScrmService storeShopStaffService;
+    @Autowired
+    private IFsStoreVisitScrmService storeVisitService;
+    @Autowired
+    private IFsStoreRecommendScrmService storeRecommendService;
+    @Autowired
+    private IFsUserPromoterApplyScrmService userPromoterApplyService;
+    @Autowired
+    private IFsMenuScrmService menuScrmService;
+    @Autowired
+    private IFsPrescribeScrmService prescribeService;
+    @Autowired
+    private IFsPrescribeDrugScrmService prescribeDrugService;
+    @Autowired
+    private IFsAdvScrmService advScrmService;
+    @Autowired
+    private IFsHomeArticleScrmService homeArticleService;
+    @Autowired
+    private IFsHomeArticleCategoryScrmService homeArticleCategoryService;
+    @Autowired
+    private IFsHomeArticleViewScrmService homeArticleViewService;
+    @Autowired
+    private IFsStoreScrmService storeScrmService;
+
+    // storeCoupon CRUD已迁移至 FsStoreCouponScrmBridgeController (fs-admin模块)
+
+    // ========== 优惠券发行 /store/storeCouponIssue ==========
+    @PreAuthorize("@ss.hasPermi('store:storeCouponIssue:list')")
+    @GetMapping("/store/storeCouponIssue/list")
+    public TableDataInfo storeCouponIssueList(FsStoreCouponIssueScrm param) {
+        startPage();
+        List<FsStoreCouponIssueScrm> list = storeCouponIssueService.selectFsStoreCouponIssueList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 发行用户 /store/storeCouponIssueUser ==========
+    @PreAuthorize("@ss.hasPermi('store:storeCouponIssueUser:list')")
+    @GetMapping("/store/storeCouponIssueUser/list")
+    public TableDataInfo storeCouponIssueUserList(FsStoreCouponIssueUserScrm param) {
+        startPage();
+        List<FsStoreCouponIssueUserScrm> list = storeCouponIssueUserService.selectFsStoreCouponIssueUserList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 用户优惠券 /store/storeCouponUser ==========
+    @PreAuthorize("@ss.hasPermi('store:storeCouponUser:list')")
+    @GetMapping("/store/storeCouponUser/list")
+    public TableDataInfo storeCouponUserList(FsStoreCouponUserScrm param) {
+        startPage();
+        List<FsStoreCouponUserScrm> list = storeCouponUserService.selectFsStoreCouponUserList(param);
+        return getDataTable(list);
+    }
+
+    // storeActivity CRUD已迁移至 FsStoreActivityScrmBridgeController (fs-admin模块)
+
+    // ========== 运费模板 /store/shippingTemplates ==========
+    @PreAuthorize("@ss.hasPermi('store:shippingTemplates:list')")
+    @GetMapping("/store/shippingTemplates/list")
+    public TableDataInfo shippingTemplatesList(FsShippingTemplatesScrm param) {
+        startPage();
+        List<FsShippingTemplatesScrm> list = shippingTemplatesService.selectFsShippingTemplatesList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 包邮模板 /store/shippingTemplatesFree ==========
+    @PreAuthorize("@ss.hasPermi('store:shippingTemplatesFree:list')")
+    @GetMapping("/store/shippingTemplatesFree/list")
+    public TableDataInfo shippingTemplatesFreeList(FsShippingTemplatesFreeScrm param) {
+        startPage();
+        List<FsShippingTemplatesFreeScrm> list = shippingTemplatesFreeService.selectFsShippingTemplatesFreeList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 区域运费 /store/shippingTemplatesRegion ==========
+    @PreAuthorize("@ss.hasPermi('store:shippingTemplatesRegion:list')")
+    @GetMapping("/store/shippingTemplatesRegion/list")
+    public TableDataInfo shippingTemplatesRegionList(FsShippingTemplatesRegionScrm param) {
+        startPage();
+        List<FsShippingTemplatesRegionScrm> list = shippingTemplatesRegionService.selectFsShippingTemplatesRegionList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 购物车 /store/storeCart ==========
+    @PreAuthorize("@ss.hasPermi('store:storeCart:list')")
+    @GetMapping("/store/storeCart/list")
+    public TableDataInfo storeCartList(FsStoreCartScrm param) {
+        startPage();
+        List<FsStoreCartScrm> list = storeCartService.selectFsStoreCartList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 门店管理 /store/storeShop ==========
+    @PreAuthorize("@ss.hasPermi('store:storeShop:list')")
+    @GetMapping("/store/storeShop/list")
+    public TableDataInfo storeShopList(FsStoreShopScrm param) {
+        startPage();
+        List<FsStoreShopScrm> list = storeShopService.selectFsStoreShopList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 门店员工 /store/storeShopStaff ==========
+    @PreAuthorize("@ss.hasPermi('store:storeShopStaff:list')")
+    @GetMapping("/store/storeShopStaff/list")
+    public TableDataInfo storeShopStaffList(FsStoreShopStaffScrm param) {
+        startPage();
+        List<FsStoreShopStaffScrm> list = storeShopStaffService.selectFsStoreShopStaffList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 浏览记录 /store/storeVisit ==========
+    @PreAuthorize("@ss.hasPermi('store:storeVisit:list')")
+    @GetMapping("/store/storeVisit/list")
+    public TableDataInfo storeVisitList(FsStoreVisitScrm param) {
+        startPage();
+        List<FsStoreVisitScrm> list = storeVisitService.selectFsStoreVisitList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 推荐管理 /store/recommend ==========
+    @PreAuthorize("@ss.hasPermi('store:recommend:list')")
+    @GetMapping("/store/recommend/list")
+    public TableDataInfo recommendList(FsStoreRecommendScrm param) {
+        startPage();
+        List<FsStoreRecommendScrm> list = storeRecommendService.selectFsStoreRecommendScrmList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 推广员申请 /store/userPromoterApply ==========
+    @PreAuthorize("@ss.hasPermi('store:userPromoterApply:list')")
+    @GetMapping("/store/userPromoterApply/list")
+    public TableDataInfo userPromoterApplyList(FsUserPromoterApplyScrm param) {
+        startPage();
+        List<FsUserPromoterApplyScrm> list = userPromoterApplyService.selectFsUserPromoterApplyList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 商城菜单 /store/menu ==========
+    @PreAuthorize("@ss.hasPermi('store:menu:list')")
+    @GetMapping("/store/menu/list")
+    public AjaxResult menuList(FsMenuScrm param) {
+        List<FsMenuScrm> list = menuScrmService.selectFsMenuList(param);
+        return AjaxResult.success(list);
+    }
+
+    // ========== 处方管理 /store/prescribe ==========
+    @PreAuthorize("@ss.hasPermi('store:prescribe:list')")
+    @GetMapping("/store/prescribe/list")
+    public TableDataInfo prescribeList(FsPrescribeScrm param) {
+        startPage();
+        List<FsPrescribeScrm> list = prescribeService.selectFsPrescribeList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 处方药品 /store/prescribeDrug ==========
+    @PreAuthorize("@ss.hasPermi('store:prescribeDrug:list')")
+    @GetMapping("/store/prescribeDrug/list")
+    public TableDataInfo prescribeDrugList(FsPrescribeDrugScrm param) {
+        startPage();
+        List<FsPrescribeDrugScrm> list = prescribeDrugService.selectFsPrescribeDrugList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 广告管理 /store/adv ==========
+    @PreAuthorize("@ss.hasPermi('store:adv:list')")
+    @GetMapping("/store/adv/list")
+    public TableDataInfo advList(FsAdvScrm param) {
+        startPage();
+        List<FsAdvScrm> list = advScrmService.selectFsAdvList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 首页文章 /store/homeArticle ==========
+    @PreAuthorize("@ss.hasPermi('store:homeArticle:list')")
+    @GetMapping("/store/homeArticle/list")
+    public TableDataInfo homeArticleList(FsHomeArticleScrm param) {
+        startPage();
+        List<FsHomeArticleScrm> list = homeArticleService.selectFsHomeArticleList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 首页分类 /store/homeCategory ==========
+    @PreAuthorize("@ss.hasPermi('store:homeCategory:list')")
+    @GetMapping("/store/homeCategory/list")
+    public TableDataInfo homeCategoryList(FsHomeArticleCategoryScrm param) {
+        startPage();
+        List<FsHomeArticleCategoryScrm> list = homeArticleCategoryService.selectFsHomeArticleCategoryList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 首页视图 /store/homeView ==========
+    @PreAuthorize("@ss.hasPermi('store:homeView:list')")
+    @GetMapping("/store/homeView/list")
+    public TableDataInfo homeViewList(FsHomeArticleViewScrm param) {
+        startPage();
+        List<FsHomeArticleViewScrm> list = homeArticleViewService.selectFsHomeArticleViewList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 店铺管理 /store/his/store ==========
+    @PreAuthorize("@ss.hasPermi('store:his:store:list')")
+    @GetMapping("/store/his/store/list")
+    public TableDataInfo hisStoreList(FsStoreScrm param) {
+        startPage();
+        List<FsStoreScrm> list = storeScrmService.selectFsStoreList(param);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:his:store:audit')")
+    @GetMapping("/store/his/store/audit")
+    public AjaxResult hisStoreAudit() {
+        return AjaxResult.success();
+    }
+
+    // ========== 商城统计 /store/statistics ==========
+    @GetMapping({"/store/statistics/storeOrder", "/store/statistics/packageOrder",
+                 "/store/statistics/storePayment", "/store/statistics/inquiryOrder"})
+    public AjaxResult storeStatistics() {
+        Map<String, Object> data = new HashMap<>();
+        data.put("total", 0);
+        data.put("amount", 0);
+        data.put("rows", new ArrayList<>());
+        return AjaxResult.success(data);
+    }
+
+    // ========== saasui额外store端点 ==========
+    @GetMapping("/store/package/list")
+    public TableDataInfo storePackageList(FsStoreProductPackageScrm param) {
+        startPage();
+        List<FsStoreProductPackageScrm> list = storeProductPackageService.selectFsStoreProductPackageList(param);
+        return getDataTable(list);
+    }
+
+    @Autowired
+    private IFsStoreProductPackageScrmService storeProductPackageService;
+
+    @GetMapping("/store/exportTask/list")
+    public TableDataInfo storeExportTaskList() {
+        return getDataTable(new ArrayList<>());
+    }
+
+    @GetMapping({"/store/healthRecord/list", "/store/healthRecord/myList"})
+    public TableDataInfo storeHealthRecordList() {
+        return getDataTable(new ArrayList<>());
+    }
+
+    @GetMapping({"/store/inquiryOrder/list", "/store/inquiryOrder/myList"})
+    public TableDataInfo storeInquiryOrderList() {
+        return getDataTable(new ArrayList<>());
+    }
+
+    @GetMapping({"/store/packageOrder/list", "/store/packageOrder/myList"})
+    public TableDataInfo storePackageOrderList() {
+        return getDataTable(new ArrayList<>());
+    }
+
+    @GetMapping({"/store/prescribe/myList", "/store/storeAfterSales/myList"})
+    public TableDataInfo storeMyLists() {
+        return getDataTable(new ArrayList<>());
+    }
+
+    @GetMapping("/store/inquiryOrderReport/list")
+    public TableDataInfo storeInquiryOrderReportList() {
+        return getDataTable(new ArrayList<>());
+    }
+}

+ 106 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminStoreOrderAdminController.java

@@ -0,0 +1,106 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+
+import com.fs.admin.helper.AdminCrossTenantHelper;
+import com.fs.common.annotation.Excel;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台销售订单管理控制器
+ * 遍历所有租户库查询 fs_store_order_scrm 数据
+ */
+@RestController
+@RequestMapping("/admin/storeOrder")
+public class AdminStoreOrderAdminController extends BaseController {
+
+    @Autowired
+    private AdminCrossTenantHelper crossTenantHelper;
+
+    private String str(Object o) { return o == null ? "" : o.toString(); }
+
+    @Data
+    public static class StoreOrderExportVO {
+        @Excel(name = "租户名称") private String companyName;
+        @Excel(name = "订单编号") private String orderNo;
+        @Excel(name = "订单金额") private String orderAmount;
+        @Excel(name = "支付方式") private String payType;
+        @Excel(name = "订单状态") private String orderStatus;
+        @Excel(name = "用户名") private String userName;
+        @Excel(name = "创建时间") private String createTime;
+    }
+
+    @Log(title = "导出商城订单", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:storeOrder:list')")
+    @GetMapping("/export")
+    public AjaxResult export(@RequestParam(required = false) String orderNo,
+                             @RequestParam(required = false) Long companyId,
+                             @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder(
+                            "SELECT o.order_id, o.order_no, o.order_amount, o.pay_type, " +
+                            "o.order_status, o.create_time, o.user_name " +
+                            "FROM fs_store_order_scrm o WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (orderNo != null && !orderNo.isEmpty()) {
+                        sql.append(" AND o.order_no LIKE ?");
+                        params.add("%" + orderNo + "%");
+                    }
+                    sql.append(" ORDER BY o.create_time DESC");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        List<StoreOrderExportVO> voList = new ArrayList<>();
+        for (Map<String, Object> m : allList) {
+            StoreOrderExportVO vo = new StoreOrderExportVO();
+            vo.setCompanyName(str(m.get("company_name")));
+            vo.setOrderNo(str(m.get("order_no")));
+            vo.setOrderAmount(str(m.get("order_amount")));
+            vo.setPayType(str(m.get("pay_type")));
+            vo.setOrderStatus(str(m.get("order_status")));
+            vo.setUserName(str(m.get("user_name")));
+            vo.setCreateTime(str(m.get("create_time")));
+            voList.add(vo);
+        }
+        ExcelUtil<StoreOrderExportVO> util = new ExcelUtil<>(StoreOrderExportVO.class);
+        return util.exportExcel(voList, "商城订单数据");
+    }
+
+    /**
+     * 查询所有租户的销售订单列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:storeOrder:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String orderNo,
+                              @RequestParam(required = false) Long companyId,
+                              @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder(
+                            "SELECT o.order_id, o.order_no, o.order_amount, o.pay_type, " +
+                            "o.order_status, o.create_time, o.user_name " +
+                            "FROM fs_store_order_scrm o WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (orderNo != null && !orderNo.isEmpty()) {
+                        sql.append(" AND o.order_no LIKE ?");
+                        params.add("%" + orderNo + "%");
+                    }
+                    sql.append(" ORDER BY o.create_time DESC LIMIT 200");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        return getDataTable(allList);
+    }
+}

+ 182 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminStoreOrderController.java

@@ -0,0 +1,182 @@
+package com.fs.admin.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.hisStore.domain.*;
+import com.fs.hisStore.param.FsStoreOrderAuditParam;
+import com.fs.hisStore.service.*;
+import com.fs.hisStore.vo.FsStoreOrderAuditVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 商城订单/支付/售后管理 - adminui端Controller(fs-admin 8004)
+ * 桥接 hisStore 服务层,提供 /store/* 订单相关路径的完整CRUD
+ */
+@RestController
+public class AdminStoreOrderController extends BaseController {
+
+    @Autowired
+    private IFsStoreOrderScrmService storeOrderService;
+    @Autowired
+    private IFsStorePaymentScrmService storePaymentService;
+    @Autowired
+    private IFsStoreAfterSalesScrmService storeAfterSalesService;
+    @Autowired
+    private IFsStoreAfterSalesItemScrmService storeAfterSalesItemService;
+    @Autowired
+    private IFsStoreAfterSalesStatusScrmService storeAfterSalesStatusService;
+    @Autowired
+    private IFsStoreOrderAuditScrmService storeOrderAuditService;
+    @Autowired
+    private IFsStoreOrderItemScrmService storeOrderItemService;
+    @Autowired
+    private IFsStoreOrderNoticeScrmService storeOrderNoticeService;
+    @Autowired
+    private IFsStoreOrderOfflineScrmService storeOrderOfflineService;
+    @Autowired
+    private IFsStoreOrderOfflineItemScrmService storeOrderOfflineItemService;
+    @Autowired
+    private IFsStoreOrderStatusScrmService storeOrderStatusService;
+
+    // ========== 商城订单 /store/storeOrder ==========
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:list')")
+    @GetMapping("/store/storeOrder/list")
+    public TableDataInfo storeOrderList(FsStoreOrderScrm param) {
+        startPage();
+        List<FsStoreOrderScrm> list = storeOrderService.selectFsStoreOrderList(param);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:query')")
+    @GetMapping("/store/storeOrder/{orderId}")
+    public AjaxResult storeOrderInfo(@PathVariable("orderId") Long orderId) {
+        return AjaxResult.success(storeOrderService.selectFsStoreOrderById(orderId));
+    }
+
+    // ========== 支付管理 /store/storePayment ==========
+    @PreAuthorize("@ss.hasPermi('store:storePayment:list')")
+    @GetMapping("/store/storePayment/list")
+    public TableDataInfo storePaymentList(FsStorePaymentScrm param) {
+        startPage();
+        List<FsStorePaymentScrm> list = storePaymentService.selectFsStorePaymentList(param);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storePayment:query')")
+    @GetMapping("/store/storePayment/{paymentId}")
+    public AjaxResult storePaymentInfo(@PathVariable("paymentId") Long paymentId) {
+        return AjaxResult.success(storePaymentService.selectFsStorePaymentById(paymentId));
+    }
+
+    // ========== 售后管理 /store/storeAfterSales ==========
+    @PreAuthorize("@ss.hasPermi('store:storeAfterSales:list')")
+    @GetMapping("/store/storeAfterSales/list")
+    public TableDataInfo storeAfterSalesList(FsStoreAfterSalesScrm param) {
+        startPage();
+        List<FsStoreAfterSalesScrm> list = storeAfterSalesService.selectFsStoreAfterSalesList(param);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeAfterSales:query')")
+    @GetMapping("/store/storeAfterSales/{afterSalesId}")
+    public AjaxResult storeAfterSalesInfo(@PathVariable("afterSalesId") Long afterSalesId) {
+        return AjaxResult.success(storeAfterSalesService.selectFsStoreAfterSalesById(afterSalesId));
+    }
+
+    // ========== 售后明细 /store/storeAfterSalesItem ==========
+    @PreAuthorize("@ss.hasPermi('store:storeAfterSalesItem:list')")
+    @GetMapping("/store/storeAfterSalesItem/list")
+    public TableDataInfo storeAfterSalesItemList(FsStoreAfterSalesItemScrm param) {
+        startPage();
+        List<FsStoreAfterSalesItemScrm> list = storeAfterSalesItemService.selectFsStoreAfterSalesItemList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 售后状态 /store/storeAfterSalesStatus ==========
+    @PreAuthorize("@ss.hasPermi('store:storeAfterSalesStatus:list')")
+    @GetMapping("/store/storeAfterSalesStatus/list")
+    public TableDataInfo storeAfterSalesStatusList(FsStoreAfterSalesStatusScrm param) {
+        startPage();
+        List<FsStoreAfterSalesStatusScrm> list = storeAfterSalesStatusService.selectFsStoreAfterSalesStatusList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 订单审核 /store/storeOrderAudit ==========
+    @PreAuthorize("@ss.hasPermi('store:storeOrderAudit:list')")
+    @GetMapping("/store/storeOrderAudit/list")
+    public TableDataInfo storeOrderAuditList(FsStoreOrderAuditParam param) {
+        startPage();
+        List<FsStoreOrderAuditVO> list = storeOrderAuditService.selectStoreOrderAuditVOList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 订单明细 /store/storeOrderItem ==========
+    @PreAuthorize("@ss.hasPermi('store:storeOrderItem:list')")
+    @GetMapping("/store/storeOrderItem/list")
+    public TableDataInfo storeOrderItemList(FsStoreOrderItemScrm param) {
+        startPage();
+        List<FsStoreOrderItemScrm> list = storeOrderItemService.selectFsStoreOrderItemList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 订单通知 /store/storeOrderNotice ==========
+    @PreAuthorize("@ss.hasPermi('store:storeOrderNotice:list')")
+    @GetMapping("/store/storeOrderNotice/list")
+    public TableDataInfo storeOrderNoticeList(FsStoreOrderNoticeScrm param) {
+        startPage();
+        List<FsStoreOrderNoticeScrm> list = storeOrderNoticeService.selectFsStoreOrderNoticeList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 线下订单 /store/storeOrderOffline ==========
+    @PreAuthorize("@ss.hasPermi('store:storeOrderOffline:list')")
+    @GetMapping("/store/storeOrderOffline/list")
+    public TableDataInfo storeOrderOfflineList(FsStoreOrderOfflineScrm param) {
+        startPage();
+        List<FsStoreOrderOfflineScrm> list = storeOrderOfflineService.selectFsStoreOrderOfflineList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 线下订单明细 /storeOrderOfflineItem/store ==========
+    @PreAuthorize("@ss.hasPermi('storeOrderOfflineItem:store:list')")
+    @GetMapping("/storeOrderOfflineItem/store/list")
+    public TableDataInfo storeOrderOfflineItemList(FsStoreOrderOfflineItemScrm param) {
+        startPage();
+        List<FsStoreOrderOfflineItemScrm> list = storeOrderOfflineItemService.selectFsStoreOrderOfflineItemList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 订单状态 /store/storeOrderStatus ==========
+    @PreAuthorize("@ss.hasPermi('store:storeOrderStatus:list')")
+    @GetMapping("/store/storeOrderStatus/list")
+    public TableDataInfo storeOrderStatusList(FsStoreOrderStatusScrm param) {
+        startPage();
+        List<FsStoreOrderStatusScrm> list = storeOrderStatusService.selectFsStoreOrderStatusList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 健康商城订单 /store/healthStoreOrder (别名) ==========
+    @PreAuthorize("@ss.hasPermi('store:healthStoreOrder:list')")
+    @GetMapping("/store/healthStoreOrder/list")
+    public TableDataInfo healthStoreOrderList(FsStoreOrderScrm param) {
+        startPage();
+        List<FsStoreOrderScrm> list = storeOrderService.selectFsStoreOrderList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 推广订单 /store/PromotionOrder ==========
+    @PreAuthorize("@ss.hasPermi('store:PromotionOrder:list')")
+    @GetMapping("/store/PromotionOrder/list")
+    public TableDataInfo promotionOrderList(FsStoreOrderScrm param) {
+        startPage();
+        List<FsStoreOrderScrm> list = storeOrderService.selectFsStoreOrderList(param);
+        return getDataTable(list);
+    }
+}

+ 251 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminStoreProductController.java

@@ -0,0 +1,251 @@
+package com.fs.admin.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.hisStore.domain.*;
+import com.fs.hisStore.service.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 商城商品管理 - adminui端Controller(fs-admin 8004)
+ * 桥接 hisStore 服务层,提供 /store/* 路径的完整CRUD
+ */
+@RestController
+public class AdminStoreProductController extends BaseController {
+
+    @Autowired
+    private IFsStoreProductScrmService storeProductService;
+    @Autowired
+    private IFsStoreProductCategoryScrmService storeProductCategoryService;
+    @Autowired
+    private IFsStoreProductRuleScrmService storeProductRuleService;
+    @Autowired
+    private IFsStoreProductPackageScrmService storeProductPackageService;
+    @Autowired
+    private IFsStoreProductAttrScrmService storeProductAttrService;
+    @Autowired
+    private IFsStoreProductAttrValueScrmService storeProductAttrValueService;
+    @Autowired
+    private IFsStoreProductDetailsScrmService storeProductDetailsService;
+    @Autowired
+    private IFsStoreProductGroupScrmService storeProductGroupService;
+    @Autowired
+    private IFsStoreProductRelationScrmService storeProductRelationService;
+    @Autowired
+    private IFsStoreProductReplyScrmService storeProductReplyService;
+    @Autowired
+    private IFsStoreProductTemplateScrmService storeProductTemplateService;
+
+    // ========== 商品管理 /store/storeProduct ==========
+    @PreAuthorize("@ss.hasPermi('store:storeProduct:list')")
+    @GetMapping("/store/storeProduct/list")
+    public TableDataInfo storeProductList(FsStoreProductScrm param) {
+        startPage();
+        List<FsStoreProductScrm> list = storeProductService.selectFsStoreProductList(param);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProduct:query')")
+    @GetMapping("/store/storeProduct/{productId}")
+    public AjaxResult storeProductInfo(@PathVariable("productId") Long productId) {
+        return AjaxResult.success(storeProductService.selectFsStoreProductById(productId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProduct:add')")
+    @Log(title = "商品管理", businessType = BusinessType.INSERT)
+    @PostMapping("/store/storeProduct")
+    public AjaxResult storeProductAdd(@RequestBody FsStoreProductScrm param) {
+        return toAjax(storeProductService.insertFsStoreProduct(param));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProduct:edit')")
+    @Log(title = "商品管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/store/storeProduct")
+    public AjaxResult storeProductEdit(@RequestBody FsStoreProductScrm param) {
+        return toAjax(storeProductService.updateFsStoreProduct(param));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProduct:remove')")
+    @Log(title = "商品管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/store/storeProduct/{productIds}")
+    public AjaxResult storeProductRemove(@PathVariable Long[] productIds) {
+        return toAjax(storeProductService.deleteFsStoreProductByIds(productIds));
+    }
+
+    // ========== 商品分类 /store/storeProductCategory ==========
+    @PreAuthorize("@ss.hasPermi('store:storeProductCategory:list')")
+    @GetMapping("/store/storeProductCategory/list")
+    public AjaxResult storeProductCategoryList(FsStoreProductCategoryScrm param) {
+        List<FsStoreProductCategoryScrm> list = storeProductCategoryService.selectFsStoreProductCategoryList(param);
+        return AjaxResult.success(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductCategory:query')")
+    @GetMapping("/store/storeProductCategory/{cateId}")
+    public AjaxResult storeProductCategoryInfo(@PathVariable("cateId") Long cateId) {
+        return AjaxResult.success(storeProductCategoryService.selectFsStoreProductCategoryById(cateId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductCategory:add')")
+    @Log(title = "商品分类", businessType = BusinessType.INSERT)
+    @PostMapping("/store/storeProductCategory")
+    public AjaxResult storeProductCategoryAdd(@RequestBody FsStoreProductCategoryScrm param) {
+        return toAjax(storeProductCategoryService.insertFsStoreProductCategory(param));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductCategory:edit')")
+    @Log(title = "商品分类", businessType = BusinessType.UPDATE)
+    @PutMapping("/store/storeProductCategory")
+    public AjaxResult storeProductCategoryEdit(@RequestBody FsStoreProductCategoryScrm param) {
+        return toAjax(storeProductCategoryService.updateFsStoreProductCategory(param));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductCategory:remove')")
+    @Log(title = "商品分类", businessType = BusinessType.DELETE)
+    @DeleteMapping("/store/storeProductCategory/{cateIds}")
+    public AjaxResult storeProductCategoryRemove(@PathVariable Long[] cateIds) {
+        return toAjax(storeProductCategoryService.deleteFsStoreProductCategoryByIds(cateIds));
+    }
+
+    // ========== 商品规格 /store/storeProductRule ==========
+    @PreAuthorize("@ss.hasPermi('store:storeProductRule:list')")
+    @GetMapping("/store/storeProductRule/list")
+    public TableDataInfo storeProductRuleList(FsStoreProductRuleScrm param) {
+        startPage();
+        List<FsStoreProductRuleScrm> list = storeProductRuleService.selectFsStoreProductRuleList(param);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductRule:query')")
+    @GetMapping("/store/storeProductRule/{ruleId}")
+    public AjaxResult storeProductRuleInfo(@PathVariable("ruleId") Integer ruleId) {
+        return AjaxResult.success(storeProductRuleService.selectFsStoreProductRuleById(ruleId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductRule:add')")
+    @Log(title = "商品规格", businessType = BusinessType.INSERT)
+    @PostMapping("/store/storeProductRule")
+    public AjaxResult storeProductRuleAdd(@RequestBody FsStoreProductRuleScrm param) {
+        return toAjax(storeProductRuleService.insertFsStoreProductRule(param));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductRule:edit')")
+    @Log(title = "商品规格", businessType = BusinessType.UPDATE)
+    @PutMapping("/store/storeProductRule")
+    public AjaxResult storeProductRuleEdit(@RequestBody FsStoreProductRuleScrm param) {
+        return toAjax(storeProductRuleService.updateFsStoreProductRule(param));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductRule:remove')")
+    @Log(title = "商品规格", businessType = BusinessType.DELETE)
+    @DeleteMapping("/store/storeProductRule/{ruleIds}")
+    public AjaxResult storeProductRuleRemove(@PathVariable Integer[] ruleIds) {
+        return toAjax(storeProductRuleService.deleteFsStoreProductRuleByIds(ruleIds));
+    }
+
+    // ========== 商品套餐 /store/storeProductPackage ==========
+    @PreAuthorize("@ss.hasPermi('store:storeProductPackage:list')")
+    @GetMapping("/store/storeProductPackage/list")
+    public TableDataInfo storeProductPackageList(FsStoreProductPackageScrm param) {
+        startPage();
+        List<FsStoreProductPackageScrm> list = storeProductPackageService.selectFsStoreProductPackageList(param);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductPackage:query')")
+    @GetMapping("/store/storeProductPackage/{packageId}")
+    public AjaxResult storeProductPackageInfo(@PathVariable("packageId") Long packageId) {
+        return AjaxResult.success(storeProductPackageService.selectFsStoreProductPackageById(packageId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductPackage:add')")
+    @Log(title = "商品套餐", businessType = BusinessType.INSERT)
+    @PostMapping("/store/storeProductPackage")
+    public AjaxResult storeProductPackageAdd(@RequestBody FsStoreProductPackageScrm param) {
+        return toAjax(storeProductPackageService.insertFsStoreProductPackage(param));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductPackage:edit')")
+    @Log(title = "商品套餐", businessType = BusinessType.UPDATE)
+    @PutMapping("/store/storeProductPackage")
+    public AjaxResult storeProductPackageEdit(@RequestBody FsStoreProductPackageScrm param) {
+        return toAjax(storeProductPackageService.updateFsStoreProductPackage(param));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductPackage:remove')")
+    @Log(title = "商品套餐", businessType = BusinessType.DELETE)
+    @DeleteMapping("/store/storeProductPackage/{packageIds}")
+    public AjaxResult storeProductPackageRemove(@PathVariable Long[] packageIds) {
+        return toAjax(storeProductPackageService.deleteFsStoreProductPackageByIds(packageIds));
+    }
+
+    // ========== 商品属性 /store/storeProductAttr ==========
+    @PreAuthorize("@ss.hasPermi('store:storeProductAttr:list')")
+    @GetMapping("/store/storeProductAttr/list")
+    public TableDataInfo storeProductAttrList(FsStoreProductAttrScrm param) {
+        startPage();
+        List<FsStoreProductAttrScrm> list = storeProductAttrService.selectFsStoreProductAttrList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 属性值 /store/storeProductAttrValue ==========
+    @PreAuthorize("@ss.hasPermi('store:storeProductAttrValue:list')")
+    @GetMapping("/store/storeProductAttrValue/list")
+    public TableDataInfo storeProductAttrValueList(FsStoreProductAttrValueScrm param) {
+        startPage();
+        List<FsStoreProductAttrValueScrm> list = storeProductAttrValueService.selectFsStoreProductAttrValueList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 商品详情 /store/storeProductDetails ==========
+    @PreAuthorize("@ss.hasPermi('store:storeProductDetails:list')")
+    @GetMapping("/store/storeProductDetails/list")
+    public TableDataInfo storeProductDetailsList(FsStoreProductDetailsScrm param) {
+        startPage();
+        List<FsStoreProductDetailsScrm> list = storeProductDetailsService.selectFsStoreProductDetailsList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 商品分组 /store/storeProductGroup ==========
+    @PreAuthorize("@ss.hasPermi('store:storeProductGroup:list')")
+    @GetMapping("/store/storeProductGroup/list")
+    public TableDataInfo storeProductGroupList(FsStoreProductGroupScrm param) {
+        startPage();
+        List<FsStoreProductGroupScrm> list = storeProductGroupService.selectFsStoreProductGroupList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 商品关联 /store/storeProductRelation ==========
+    @PreAuthorize("@ss.hasPermi('store:storeProductRelation:list')")
+    @GetMapping("/store/storeProductRelation/list")
+    public TableDataInfo storeProductRelationList(FsStoreProductRelationScrm param) {
+        startPage();
+        List<FsStoreProductRelationScrm> list = storeProductRelationService.selectFsStoreProductRelationList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 商品评价 /store/storeProductReply ==========
+    @PreAuthorize("@ss.hasPermi('store:storeProductReply:list')")
+    @GetMapping("/store/storeProductReply/list")
+    public TableDataInfo storeProductReplyList(FsStoreProductReplyScrm param) {
+        startPage();
+        List<FsStoreProductReplyScrm> list = storeProductReplyService.selectFsStoreProductReplyList(param);
+        return getDataTable(list);
+    }
+
+    // ========== 商品模板 /store/storeProductTemplate ==========
+    @PreAuthorize("@ss.hasPermi('store:storeProductTemplate:list')")
+    @GetMapping("/store/storeProductTemplate/list")
+    public TableDataInfo storeProductTemplateList(FsStoreProductTemplateScrm param) {
+        startPage();
+        List<FsStoreProductTemplateScrm> list = storeProductTemplateService.selectFsStoreProductTemplateList(param);
+        return getDataTable(list);
+    }
+}

+ 43 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminVideoResourceController.java

@@ -0,0 +1,43 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+
+import com.fs.admin.helper.AdminCrossTenantHelper;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台视频资源审计控制器
+ * 遍历所有租户库查询 fs_user_course_video 数据(视频资源部分)
+ */
+@RestController
+@RequestMapping("/admin/videoResource")
+public class AdminVideoResourceController extends BaseController {
+
+    @Autowired
+    private AdminCrossTenantHelper crossTenantHelper;
+
+    /**
+     * 查询所有租户的视频资源列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:videoResource:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) Long companyId,
+                              @RequestParam(required = false) String companyName,
+                              @RequestParam(required = false) String videoName) {
+        String sql = "SELECT id, title as videoName, video_type as videoType, " +
+                "play_url as playUrl, duration, status, create_time as createTime " +
+                "FROM fs_user_course_video WHERE 1=1";
+        StringBuilder sb = new StringBuilder(sql);
+        if (videoName != null && !videoName.isEmpty()) {
+            sb.append(" AND title LIKE '%").append(videoName.replace("'", "''")).append("%'");
+        }
+        sb.append(" ORDER BY create_time DESC");
+
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(companyId, companyName, sb.toString());
+        return getDataTable(allList);
+    }
+}

+ 96 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminWithdrawalController.java

@@ -0,0 +1,96 @@
+package com.fs.admin.controller;
+
+import java.util.List;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.annotation.ProxyLog;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.proxy.domain.ProxyWithdraw;
+import com.fs.proxy.service.ProxyWithdrawService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台-代理提现管理控制器
+ */
+@RestController
+@RequestMapping("/admin/withdrawal")
+public class AdminWithdrawalController extends BaseController {
+
+    @Autowired
+    private ProxyWithdrawService proxyWithdrawService;
+
+    /**
+     * 查询提现申请列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:withdrawal:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(ProxyWithdraw proxyWithdraw) {
+        startPage();
+        List<ProxyWithdraw> list = proxyWithdrawService.selectProxyWithdrawList(proxyWithdraw);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出提现申请列表
+     */
+    @Log(title = "导出提现申请", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:withdrawal:list')")
+    @GetMapping("/export")
+    public AjaxResult export(ProxyWithdraw proxyWithdraw) {
+        List<ProxyWithdraw> list = proxyWithdrawService.selectProxyWithdrawList(proxyWithdraw);
+        ExcelUtil<ProxyWithdraw> util = new ExcelUtil<>(ProxyWithdraw.class);
+        return util.exportExcel(list, "提现申请数据");
+    }
+
+    /**
+     * 获取提现申请详情
+     */
+    @PreAuthorize("@ss.hasPermi('admin:withdrawal:list')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        ProxyWithdraw record = proxyWithdrawService.selectProxyWithdrawById(id);
+        return record != null ? AjaxResult.success(record) : AjaxResult.error("记录不存在");
+    }
+
+    /**
+     * 审核通过(status 0→1)
+     */
+    @PreAuthorize("@ss.hasPermi('admin:withdrawal:approve')")
+    @ProxyLog(title = "提现管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/approve")
+    public AjaxResult approve(@PathVariable Long id) {
+        Long auditorId = SecurityUtils.getUserId();
+        int rows = proxyWithdrawService.auditWithdraw(id, 1, auditorId, null);
+        return rows > 0 ? AjaxResult.success("审核通过") : AjaxResult.error("操作失败,请检查提现状态");
+    }
+
+    /**
+     * 审核拒绝(status 0→2)
+     */
+    @PreAuthorize("@ss.hasPermi('admin:withdrawal:approve')")
+    @ProxyLog(title = "提现管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/reject")
+    public AjaxResult reject(@PathVariable Long id, @RequestParam String reason) {
+        Long auditorId = SecurityUtils.getUserId();
+        int rows = proxyWithdrawService.auditWithdraw(id, 2, auditorId, reason);
+        return rows > 0 ? AjaxResult.success("已拒绝") : AjaxResult.error("操作失败,请检查提现状态");
+    }
+
+    /**
+     * 确认打款(status 1→3)
+     */
+    @PreAuthorize("@ss.hasPermi('admin:withdrawal:approve')")
+    @ProxyLog(title = "提现管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/paid")
+    public AjaxResult confirmPaid(@PathVariable Long id) {
+        int rows = proxyWithdrawService.payWithdraw(id);
+        return rows > 0 ? AjaxResult.success("已确认打款") : AjaxResult.error("操作失败,请检查提现状态");
+    }
+}

+ 107 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AiChatQualityController.java

@@ -0,0 +1,107 @@
+package com.fs.admin.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.fastGpt.domain.FastGptChatMsg;
+import com.fs.fastGpt.domain.FastGptChatSession;
+import com.fs.fastGpt.service.IFastGptChatMsgService;
+import com.fs.fastGpt.service.IFastGptChatSessionService;
+import com.fs.proxy.domain.AiChatQualityRecord;
+import com.fs.proxy.service.AiChatQualityService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/admin/aiChatQuality")
+public class AiChatQualityController extends BaseController {
+
+    @Autowired
+    private IFastGptChatSessionService fastGptChatSessionService;
+
+    @Autowired
+    private IFastGptChatMsgService fastGptChatMsgService;
+
+    @Autowired
+    private AiChatQualityService aiChatQualityService;
+
+    @Log(title = "导出AI对话会话", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:aiChatQuality:list')")
+    @GetMapping("/export")
+    public AjaxResult export(FastGptChatSession session) {
+        List<FastGptChatSession> list = fastGptChatSessionService.selectFastGptChatSessionList(session);
+        ExcelUtil<FastGptChatSession> util = new ExcelUtil<>(FastGptChatSession.class);
+        return util.exportExcel(list, "AI对话质检数据");
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiChatQuality:list')")
+    @GetMapping({"/list", "/sessions"})
+    public TableDataInfo listSessions(FastGptChatSession session) {
+        startPage();
+        List<FastGptChatSession> list = fastGptChatSessionService.selectFastGptChatSessionList(session);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiChatQuality:query')")
+    @GetMapping("/session/{sessionId}")
+    public AjaxResult getSessionDetail(@PathVariable Long sessionId) {
+        return AjaxResult.success(fastGptChatSessionService.selectFastGptChatSessionBySessionId(sessionId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiChatQuality:query')")
+    @GetMapping("/messages/{sessionId}")
+    public AjaxResult getMessages(@PathVariable Long sessionId) {
+        FastGptChatMsg query = new FastGptChatMsg();
+        query.setSessionId(sessionId);
+        List<FastGptChatMsg> messages = fastGptChatMsgService.selectFastGptChatMsgList(query);
+        return AjaxResult.success(messages);
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiChatQuality:list')")
+    @GetMapping("/records")
+    public TableDataInfo listRecords(AiChatQualityRecord record) {
+        startPage();
+        List<AiChatQualityRecord> list = aiChatQualityService.selectAiChatQualityRecordList(record);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiChatQuality:query')")
+    @GetMapping("/record/{id}")
+    public AjaxResult getRecordInfo(@PathVariable Long id) {
+        AiChatQualityRecord record = aiChatQualityService.selectAiChatQualityRecordById(id);
+        return AjaxResult.success(record);
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiChatQuality:add')")
+    @PostMapping("/record")
+    public AjaxResult addRecord(@RequestBody AiChatQualityRecord record) {
+        return toAjax(aiChatQualityService.insertAiChatQualityRecord(record));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiChatQuality:edit')")
+    @PutMapping("/record")
+    public AjaxResult editRecord(@RequestBody AiChatQualityRecord record) {
+        return toAjax(aiChatQualityService.updateAiChatQualityRecord(record));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiChatQuality:remove')")
+    @DeleteMapping("/record/{ids}")
+    public AjaxResult removeRecords(@PathVariable Long[] ids) {
+        return toAjax(aiChatQualityService.deleteAiChatQualityRecordByIds(ids));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiChatQuality:batch')")
+    @PostMapping("/batchQuality")
+    public AjaxResult batchQuality(@RequestBody List<Long> sessionIds,
+                                   @RequestParam String result,
+                                   @RequestParam(required = false) String remark) {
+        int count = aiChatQualityService.batchQuality(sessionIds, result, remark);
+        return AjaxResult.success("批量质检完成,共处理 " + count + " 条记录");
+    }
+}

+ 102 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/AiProviderAdminController.java

@@ -0,0 +1,102 @@
+package com.fs.admin.controller;
+
+import java.util.List;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.company.domain.CompanyAiProvider;
+import com.fs.company.mapper.CompanyAiProviderMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping({"/admin/aiProvider", "/knowledge/ai-provider"})
+public class AiProviderAdminController extends BaseController
+{
+    @Autowired
+    private CompanyAiProviderMapper aiProviderMapper;
+
+    @PreAuthorize("@ss.hasPermi('admin:aiProvider:list')")
+    @GetMapping("/list")
+    public AjaxResult list()
+    {
+        List<CompanyAiProvider> list = aiProviderMapper.selectList();
+        list.forEach(this::maskApiKey);
+        return AjaxResult.success(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiProvider:list')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id)
+    {
+        CompanyAiProvider provider = aiProviderMapper.selectById(id);
+        if (provider != null) {
+            maskApiKey(provider);
+        }
+        return AjaxResult.success(provider);
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiProvider:add')")
+    @Log(title = "新增大模型供应商", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CompanyAiProvider provider)
+    {
+        provider.setIsDefault(0);
+        provider.setDelFlag(0);
+        return toAjax(aiProviderMapper.insert(provider));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiProvider:edit')")
+    @Log(title = "修改大模型供应商", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable Long id, @RequestBody CompanyAiProvider provider)
+    {
+        provider.setId(id);
+        if (provider.getApiKey() != null && provider.getApiKey().contains("*")) {
+            CompanyAiProvider existing = aiProviderMapper.selectById(id);
+            if (existing != null) {
+                provider.setApiKey(existing.getApiKey());
+            }
+        }
+        return toAjax(aiProviderMapper.updateById(provider));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiProvider:remove')")
+    @Log(title = "删除大模型供应商", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}")
+    public AjaxResult remove(@PathVariable Long id)
+    {
+        return toAjax(aiProviderMapper.deleteById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiProvider:edit')")
+    @Log(title = "设置默认大模型供应商", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/set-default")
+    public AjaxResult setDefault(@PathVariable Long id)
+    {
+        aiProviderMapper.clearAllDefault();
+        CompanyAiProvider provider = new CompanyAiProvider();
+        provider.setId(id);
+        provider.setIsDefault(1);
+        return toAjax(aiProviderMapper.updateById(provider));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:aiProvider:list')")
+    @PostMapping("/test")
+    public AjaxResult test(@RequestBody CompanyAiProvider provider)
+    {
+        return AjaxResult.success("连接测试功能待实现");
+    }
+
+    private void maskApiKey(CompanyAiProvider provider) {
+        String apiKey = provider.getApiKey();
+        if (apiKey != null && apiKey.length() > 8) {
+            provider.setApiKey(apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length() - 4));
+        } else if (apiKey != null && apiKey.length() > 0) {
+            provider.setApiKey("****");
+        }
+    }
+}

+ 138 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/ArticleAdminController.java

@@ -0,0 +1,138 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+
+import com.fs.admin.helper.AdminCrossTenantHelper;
+import com.fs.common.annotation.Excel;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台文章内容审计控制器
+ * 遍历所有租户库查询 fs_article 数据
+ */
+@RestController
+@RequestMapping("/admin/article")
+public class ArticleAdminController extends BaseController {
+
+    @Autowired
+    private AdminCrossTenantHelper crossTenantHelper;
+
+    private String str(Object o) { return o == null ? "" : o.toString(); }
+
+    @Data
+    public static class ArticleExportVO {
+        @Excel(name = "租户名称") private String companyName;
+        @Excel(name = "文章标题") private String articleTitle;
+        @Excel(name = "分类") private String categoryName;
+        @Excel(name = "作者") private String author;
+        @Excel(name = "状态") private String status;
+        @Excel(name = "创建时间") private String createTime;
+    }
+
+    @Log(title = "导出文章", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:article:list')")
+    @GetMapping("/export")
+    public AjaxResult export(@RequestParam(required = false) String articleTitle,
+                             @RequestParam(required = false) Long companyId,
+                             @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder(
+                            "SELECT a.article_id, a.article_title, a.category_name, a.author, " +
+                            "a.create_time, a.status " +
+                            "FROM fs_article a WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (articleTitle != null && !articleTitle.isEmpty()) {
+                        sql.append(" AND a.article_title LIKE ?");
+                        params.add("%" + articleTitle + "%");
+                    }
+                    sql.append(" ORDER BY a.create_time DESC");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        List<ArticleExportVO> voList = new ArrayList<>();
+        for (Map<String, Object> m : allList) {
+            ArticleExportVO vo = new ArticleExportVO();
+            vo.setCompanyName(str(m.get("company_name")));
+            vo.setArticleTitle(str(m.get("article_title")));
+            vo.setCategoryName(str(m.get("category_name")));
+            vo.setAuthor(str(m.get("author")));
+            vo.setStatus(str(m.get("status")));
+            vo.setCreateTime(str(m.get("create_time")));
+            voList.add(vo);
+        }
+        ExcelUtil<ArticleExportVO> util = new ExcelUtil<>(ArticleExportVO.class);
+        return util.exportExcel(voList, "文章数据");
+    }
+
+    /**
+     * 查询所有租户的文章列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:article:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String articleTitle,
+                              @RequestParam(required = false) Long companyId,
+                              @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder(
+                            "SELECT a.article_id, a.article_title, a.category_name, a.author, " +
+                            "a.create_time, a.status " +
+                            "FROM fs_article a WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (articleTitle != null && !articleTitle.isEmpty()) {
+                        sql.append(" AND a.article_title LIKE ?");
+                        params.add("%" + articleTitle + "%");
+                    }
+                    sql.append(" ORDER BY a.create_time DESC LIMIT 200");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        return getDataTable(allList);
+    }
+
+    /**
+     * 查询待审计文章列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:article:list')")
+    @GetMapping("/pending")
+    public TableDataInfo pendingList(@RequestParam(required = false) Long companyId,
+                                     @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) ->
+                        jdbc.queryForList(
+                                "SELECT a.article_id, a.article_title, a.category_name, a.author, " +
+                                "a.create_time, a.status " +
+                                "FROM fs_article a WHERE a.status = 0 ORDER BY a.create_time DESC LIMIT 200"));
+        return getDataTable(allList);
+    }
+
+    /**
+     * 文章统计信息
+     */
+    @PreAuthorize("@ss.hasPermi('admin:article:query')")
+    @GetMapping("/statistics")
+    public AjaxResult statistics(@RequestParam(required = false) Long companyId,
+                                 @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) ->
+                        jdbc.queryForList("SELECT article_id, status FROM fs_article"));
+        Map<String, Object> result = new HashMap<>();
+        result.put("totalCount", allList.size());
+        result.put("pendingCount", allList.stream().filter(r -> "0".equals(String.valueOf(r.get("status")))).count());
+        result.put("approvedCount", allList.stream().filter(r -> "1".equals(String.valueOf(r.get("status")))).count());
+        result.put("rejectedCount", allList.stream().filter(r -> "2".equals(String.valueOf(r.get("status")))).count());
+        return AjaxResult.success(result);
+    }
+}

+ 156 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/CallRecordAdminController.java

@@ -0,0 +1,156 @@
+package com.fs.admin.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
+import com.fs.proxy.domain.CallQualityRecord;
+import com.fs.proxy.service.CallQualityService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 总后台外呼记录和质检控制器
+ * 查看所有租户的外呼通话记录、录音和质检
+ */
+@RestController
+@RequestMapping("/admin/callRecord")
+public class CallRecordAdminController extends BaseController {
+
+    @Autowired
+    private ICompanyVoiceRoboticCallLogCallphoneService callLogService;
+
+    @Autowired
+    private CallQualityService callQualityService;
+
+    /**
+     * 查询所有租户的外呼通话记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:callRecord:list')")
+    @GetMapping("/list")
+    public TableDataInfo list() {
+        startPage();
+        List<CompanyVoiceRoboticCallLogCallphone> list = callLogService.list(new QueryWrapper<CompanyVoiceRoboticCallLogCallphone>().orderByDesc("run_time"));
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取外呼通话记录详情
+     */
+    @PreAuthorize("@ss.hasPermi('admin:callRecord:query')")
+    @GetMapping(value = "/{logId}")
+    public AjaxResult getInfo(@PathVariable("logId") Long logId) {
+        return AjaxResult.success(callLogService.getById(logId));
+    }
+
+    /**
+     * 导出通话记录
+     */
+    @Log(title = "导出通话记录", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:callRecord:list')")
+    @GetMapping("/export")
+    public AjaxResult export(CompanyVoiceRoboticCallLogCallphone param) {
+        List<CompanyVoiceRoboticCallLogCallphone> list = callLogService.list(
+            new QueryWrapper<CompanyVoiceRoboticCallLogCallphone>().orderByDesc("run_time"));
+        ExcelUtil<CompanyVoiceRoboticCallLogCallphone> util = new ExcelUtil<>(CompanyVoiceRoboticCallLogCallphone.class);
+        return util.exportExcel(list, "通话记录数据");
+    }
+
+    /**
+     * 根据租户ID查询外呼记录
+     */
+    @PreAuthorize("@ss.hasPermi('admin:callRecord:query')")
+    @GetMapping("/byCompany/{companyId}")
+    public TableDataInfo getByCompanyId(@PathVariable Long companyId) {
+        startPage();
+        List<CompanyVoiceRoboticCallLogCallphone> list = callLogService.list(new QueryWrapper<CompanyVoiceRoboticCallLogCallphone>().orderByDesc("run_time"));
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取通话录音路径
+     */
+    @PreAuthorize("@ss.hasPermi('admin:callRecord:query')")
+    @GetMapping("/audio/{logId}")
+    public AjaxResult getAudio(@PathVariable Long logId) {
+        CompanyVoiceRoboticCallLogCallphone callLog = callLogService.getById(logId);
+        if (callLog != null && callLog.getRecordPath() != null) {
+            return AjaxResult.success(callLog.getRecordPath());
+        }
+        return AjaxResult.error("录音不存在");
+    }
+
+    /**
+     * 查询质检记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:callRecord:qualityList')")
+    @GetMapping("/quality/list")
+    public TableDataInfo qualityList(CallQualityRecord record) {
+        startPage();
+        List<CallQualityRecord> list = callQualityService.selectCallQualityRecordList(record);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取质检记录详情
+     */
+    @PreAuthorize("@ss.hasPermi('admin:callRecord:query')")
+    @GetMapping("/quality/{id}")
+    public AjaxResult getQualityInfo(@PathVariable Long id) {
+        return AjaxResult.success(callQualityService.selectCallQualityRecordById(id));
+    }
+
+    /**
+     * 新增质检记录
+     */
+    @PreAuthorize("@ss.hasPermi('admin:callRecord:addQuality')")
+    @PostMapping("/quality")
+    public AjaxResult addQuality(@RequestBody CallQualityRecord record) {
+        return toAjax(callQualityService.insertCallQualityRecord(record));
+    }
+
+    /**
+     * 修改质检记录
+     */
+    @PreAuthorize("@ss.hasPermi('admin:callRecord:editQuality')")
+    @PutMapping("/quality")
+    public AjaxResult editQuality(@RequestBody CallQualityRecord record) {
+        return toAjax(callQualityService.updateCallQualityRecord(record));
+    }
+
+    /**
+     * 删除质检记录
+     */
+    @PreAuthorize("@ss.hasPermi('admin:callRecord:removeQuality')")
+    @DeleteMapping("/quality/{ids}")
+    public AjaxResult removeQuality(@PathVariable Long[] ids) {
+        return toAjax(callQualityService.deleteCallQualityRecordByIds(ids));
+    }
+
+    /**
+     * 通话记录统计信息
+     */
+    @PreAuthorize("@ss.hasPermi('admin:callRecord:query')")
+    @GetMapping("/statistics")
+    public AjaxResult statistics() {
+        return AjaxResult.success(callLogService.count());
+    }
+
+    /**
+     * 报备质检
+     */
+    @PreAuthorize("@ss.hasPermi('admin:callRecord:report')")
+    @PostMapping("/report")
+    public AjaxResult report(@RequestBody List<Long> logIds) {
+        int count = callQualityService.batchReport(logIds);
+        return AjaxResult.success("报备成功,共报备 " + count + " 条通话记录");
+    }
+}

+ 148 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/CompanyAdminController.java

@@ -0,0 +1,148 @@
+package com.fs.admin.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 总后台租户管理控制器(SaaS 多租户版)
+ * 查询 tenant_info 表,不再查询 company 表
+ */
+@RestController
+@RequestMapping("/admin/company")
+public class CompanyAdminController extends BaseController {
+
+    @Autowired
+    private TenantInfoService tenantInfoService;
+
+    /**
+     * 查询所有租户列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:company:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(TenantInfo tenantInfo) {
+        startPage();
+        List<TenantInfo> list = tenantInfoService.selectTenantInfoList(tenantInfo);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出租户列表
+     */
+    @Log(title = "导出租户列表", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:company:list')")
+    @GetMapping("/export")
+    public void export(HttpServletResponse response, TenantInfo tenantInfo) throws IOException {
+        List<TenantInfo> list = tenantInfoService.selectTenantInfoList(tenantInfo);
+        ExcelUtil<TenantInfo> util = new ExcelUtil<>(TenantInfo.class);
+        util.exportExcel(response, list, "租户列表数据");
+    }
+
+    /**
+     * 获取租户详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('admin:company:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") String id) {
+        return AjaxResult.success(tenantInfoService.selectTenantInfoById(id));
+    }
+
+    /**
+     * 新增租户
+     */
+    @PreAuthorize("@ss.hasPermi('admin:company:add')")
+    @Log(title = "租户管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public R add(@RequestBody TenantInfo tenantInfo) {
+        int i = tenantInfoService.insertTenantInfo(tenantInfo);
+        return i > 0 ? R.ok("租户数据库初始化中,请稍后") : R.error("租户创建失败");
+    }
+
+    /**
+     * 修改租户
+     */
+    @PreAuthorize("@ss.hasPermi('admin:company:edit')")
+    @Log(title = "租户管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody TenantInfo tenantInfo) {
+        return toAjax(tenantInfoService.updateTenantInfo(tenantInfo));
+    }
+
+    /**
+     * 删除租户
+     */
+    @PreAuthorize("@ss.hasPermi('admin:company:remove')")
+    @Log(title = "租户管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable String[] ids) {
+        return toAjax(tenantInfoService.deleteTenantInfoByIds(ids));
+    }
+
+    /**
+     * 禁用/启用租户
+     */
+    @PreAuthorize("@ss.hasPermi('admin:company:edit')")
+    @Log(title = "租户管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/status/{id}")
+    public AjaxResult changeStatus(@PathVariable String id, @RequestParam Integer status) {
+        TenantInfo tenantInfo = new TenantInfo();
+        tenantInfo.setId(Long.valueOf(id));
+        tenantInfo.setStatus(status);
+        return toAjax(tenantInfoService.updateTenantInfo(tenantInfo));
+    }
+
+    /**
+     * 获取租户下拉列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:company:query')")
+    @GetMapping("/options")
+    public AjaxResult getOptions() {
+        TenantInfo tenantInfo = new TenantInfo();
+        tenantInfo.setStatus(1);
+        List<TenantInfo> list = tenantInfoService.selectTenantInfoList(tenantInfo);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 租户统计信息
+     */
+    @PreAuthorize("@ss.hasPermi('admin:company:query')")
+    @GetMapping("/statistics")
+    public AjaxResult statistics() {
+        TenantInfo tenantInfo = new TenantInfo();
+        List<TenantInfo> all = tenantInfoService.selectTenantInfoList(tenantInfo);
+        long activeCount = all.stream().filter(t -> t.getStatus() != null && t.getStatus() == 1).count();
+        Map<String, Object> result = new HashMap<>();
+        result.put("totalCount", all.size());
+        result.put("activeCount", activeCount);
+        result.put("disabledCount", all.size() - activeCount);
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 租户充值/扣款
+     * TODO 后续优化:需要切到租户库查询 company 表余额并操作充值/扣款
+     */
+    @PreAuthorize("@ss.hasPermi('admin:company:edit')")
+    @Log(title = "租户管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/recharge")
+    public AjaxResult rechargeCompany(@PathVariable String id, @RequestBody Map<String, Object> params) {
+        // TODO 后续优化:切到租户库执行充值/扣款逻辑
+        return AjaxResult.error("充值/扣款功能暂未对接多租户,后续优化");
+    }
+}

+ 139 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/CompanyUserAdminController.java

@@ -0,0 +1,139 @@
+package com.fs.admin.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.annotation.Log;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.Company;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.domain.CompanyUserChangeApply;
+import com.fs.company.service.ICompanyService;
+import com.fs.company.service.ICompanyUserChangeApplyService;
+import com.fs.company.service.ICompanyUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 总后台员工账户管理控制器
+ * 查看所有租户的员工账户、禁用启用员工账户、查看账户变化记录
+ */
+@RestController
+@RequestMapping("/admin/companyUser")
+public class CompanyUserAdminController extends BaseController {
+
+    @Autowired
+    private ICompanyUserService companyUserService;
+
+    @Autowired
+    private ICompanyUserChangeApplyService companyUserChangeApplyService;
+
+    @Autowired
+    private ICompanyService companyService;
+
+    /**
+     * 查询所有租户的员工列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:companyUser:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyUser companyUser) {
+        startPage();
+        List<CompanyUser> list = companyUserService.selectCompanyUserList(companyUser);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出员工列表
+     */
+    @Log(title = "导出员工列表", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:companyUser:list')")
+    @GetMapping("/export")
+    public AjaxResult export(CompanyUser companyUser) {
+        List<CompanyUser> list = companyUserService.selectCompanyUserList(companyUser);
+        ExcelUtil<CompanyUser> util = new ExcelUtil<>(CompanyUser.class);
+        return util.exportExcel(list, "员工账户数据");
+    }
+
+    /**
+     * 获取员工详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('admin:companyUser:query')")
+    @GetMapping(value = "/{userId}")
+    public AjaxResult getInfo(@PathVariable("userId") Long userId) {
+        return AjaxResult.success(companyUserService.selectCompanyUserById(userId));
+    }
+
+    /**
+     * 根据租户ID查询员工列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:companyUser:query')")
+    @GetMapping("/byCompany/{companyId}")
+    public TableDataInfo getByCompanyId(@PathVariable Long companyId) {
+        startPage();
+        CompanyUser companyUser = new CompanyUser();
+        companyUser.setCompanyId(companyId);
+        List<CompanyUser> list = companyUserService.selectCompanyUserList(companyUser);
+        return getDataTable(list);
+    }
+
+    /**
+     * 禁用/启用员工账户
+     */
+    @PreAuthorize("@ss.hasPermi('admin:companyUser:edit')")
+    @PutMapping("/status/{userId}")
+    public AjaxResult changeStatus(@PathVariable Long userId, @RequestParam Integer status) {
+        CompanyUser companyUser = new CompanyUser();
+        companyUser.setUserId(userId);
+        companyUser.setStatus(status != null ? String.valueOf(status) : "1");
+        return toAjax(companyUserService.updateCompanyUser(companyUser));
+    }
+
+    /**
+     * 查询员工账户变化记录
+     */
+    @PreAuthorize("@ss.hasPermi('admin:companyUser:changeList')")
+    @GetMapping("/changeList")
+    public TableDataInfo changeList() {
+        startPage();
+        List<CompanyUserChangeApply> list = companyUserChangeApplyService.list();
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取员工账户变化记录详情
+     */
+    @PreAuthorize("@ss.hasPermi('admin:companyUser:query')")
+    @GetMapping("/change/{id}")
+    public AjaxResult getChangeInfo(@PathVariable Long id) {
+        return AjaxResult.success(companyUserChangeApplyService.getById(id));
+    }
+
+    /**
+     * 获取租户列表(用于筛选)
+     */
+    @PreAuthorize("@ss.hasPermi('admin:companyUser:query')")
+    @GetMapping("/companies")
+    public AjaxResult getCompanies() {
+        Company company = new Company();
+        company.setStatus(1);
+        List<Company> list = companyService.selectCompanyList(company);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 统计各租户员工数量
+     */
+    @PreAuthorize("@ss.hasPermi('admin:companyUser:query')")
+    @GetMapping("/statistics")
+    public AjaxResult statistics() {
+        CompanyUser query = new CompanyUser();
+        List<CompanyUser> all = companyUserService.selectCompanyUserList(query);
+        java.util.Map<String, Object> result = new java.util.HashMap<>();
+        result.put("totalCount", all.size());
+        return AjaxResult.success(result);
+    }
+}

+ 122 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/CourseAdminController.java

@@ -0,0 +1,122 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+
+import com.fs.admin.helper.AdminCrossTenantHelper;
+import com.fs.common.annotation.Excel;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台课程内容管理控制器
+ * 遍历所有租户库查询课程数据
+ */
+@RestController
+@RequestMapping("/admin/course")
+public class CourseAdminController extends BaseController {
+
+    @Autowired
+    private AdminCrossTenantHelper crossTenantHelper;
+
+    private String str(Object o) { return o == null ? "" : o.toString(); }
+
+    @Data
+    public static class CourseExportVO {
+        @Excel(name = "租户名称") private String companyName;
+        @Excel(name = "课程名称") private String courseName;
+        @Excel(name = "播放次数") private String playCount;
+        @Excel(name = "状态") private String status;
+        @Excel(name = "创建时间") private String createTime;
+    }
+
+    @Log(title = "导出课程", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:course:list')")
+    @GetMapping("/export")
+    public AjaxResult export(@RequestParam(required = false) String courseName,
+                             @RequestParam(required = false) Long companyId,
+                             @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder(
+                            "SELECT v.video_id, v.course_id, v.course_name, v.cover_url, " +
+                            "v.duration, v.play_count, v.create_time, v.status " +
+                            "FROM fs_user_course_video v WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (courseName != null && !courseName.isEmpty()) {
+                        sql.append(" AND v.course_name LIKE ?");
+                        params.add("%" + courseName + "%");
+                    }
+                    sql.append(" ORDER BY v.create_time DESC");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        List<CourseExportVO> voList = new ArrayList<>();
+        for (Map<String, Object> m : allList) {
+            CourseExportVO vo = new CourseExportVO();
+            vo.setCompanyName(str(m.get("company_name")));
+            vo.setCourseName(str(m.get("course_name")));
+            vo.setPlayCount(str(m.get("play_count")));
+            vo.setStatus(str(m.get("status")));
+            vo.setCreateTime(str(m.get("create_time")));
+            voList.add(vo);
+        }
+        ExcelUtil<CourseExportVO> util = new ExcelUtil<>(CourseExportVO.class);
+        return util.exportExcel(voList, "课程数据");
+    }
+
+    /**
+     * 查询所有租户的课程列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:course:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String courseName,
+                              @RequestParam(required = false) Long companyId,
+                              @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder(
+                            "SELECT v.video_id, v.course_id, v.course_name, v.cover_url, " +
+                            "v.duration, v.play_count, v.create_time, v.status " +
+                            "FROM fs_user_course_video v WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (courseName != null && !courseName.isEmpty()) {
+                        sql.append(" AND v.course_name LIKE ?");
+                        params.add("%" + courseName + "%");
+                    }
+                    sql.append(" ORDER BY v.create_time DESC LIMIT 200");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        return getDataTable(allList);
+    }
+
+    /**
+     * 课程统计信息
+     */
+    @PreAuthorize("@ss.hasPermi('admin:course:query')")
+    @GetMapping("/statistics")
+    public AjaxResult statistics(@RequestParam(required = false) Long companyId,
+                                 @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) ->
+                        jdbc.queryForList("SELECT video_id, status FROM fs_user_course_video"));
+        Map<String, Object> result = new HashMap<>();
+        result.put("totalCount", allList.size());
+        result.put("onlineCount", allList.stream().filter(r -> "0".equals(String.valueOf(r.get("status")))).count());
+        result.put("offlineCount", allList.stream().filter(r -> "1".equals(String.valueOf(r.get("status")))).count());
+        result.put("totalPlayCount", allList.stream()
+                .mapToLong(r -> r.get("play_count") != null ? ((Number) r.get("play_count")).longValue() : 0L)
+                .sum());
+        return AjaxResult.success(result);
+    }
+}

+ 59 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/DbConfigController.java

@@ -0,0 +1,59 @@
+package com.fs.admin.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.tenant.domain.DbConfig;
+import com.fs.tenant.service.DbConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/admin/dbConfig")
+public class DbConfigController extends BaseController {
+
+    @Autowired
+    private DbConfigService dbConfigService;
+
+    @PreAuthorize("@ss.hasPermi('admin:dbConfig:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(DbConfig dbConfig) {
+        startPage();
+        List<DbConfig> list = dbConfigService.selectDbConfigList(dbConfig);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:dbConfig:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(dbConfigService.selectDbConfigById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:dbConfig:add')")
+    @PostMapping
+    public AjaxResult add(@RequestBody DbConfig dbConfig) {
+        return toAjax(dbConfigService.insertDbConfig(dbConfig));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:dbConfig:edit')")
+    @PutMapping
+    public AjaxResult edit(@RequestBody DbConfig dbConfig) {
+        return toAjax(dbConfigService.updateDbConfig(dbConfig));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:dbConfig:remove')")
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(dbConfigService.deleteDbConfigByIds(ids));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:dbConfig:query')")
+    @GetMapping("/available")
+    public AjaxResult getAvailable() {
+        List<DbConfig> list = dbConfigService.selectDbConfigList(new DbConfig());
+        return AjaxResult.success(list);
+    }
+}

+ 119 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/LiveAdminController.java

@@ -0,0 +1,119 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+
+import com.fs.admin.helper.AdminCrossTenantHelper;
+import com.fs.common.annotation.Excel;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台直播内容管理控制器
+ * 遍历所有租户库查询 live_video 数据
+ */
+@RestController
+@RequestMapping("/admin/live")
+public class LiveAdminController extends BaseController {
+
+    @Autowired
+    private AdminCrossTenantHelper crossTenantHelper;
+
+    private String str(Object o) { return o == null ? "" : o.toString(); }
+
+    @Data
+    public static class LiveExportVO {
+        @Excel(name = "租户名称") private String companyName;
+        @Excel(name = "直播标题") private String title;
+        @Excel(name = "状态") private String liveStatus;
+        @Excel(name = "开始时间") private String startTime;
+        @Excel(name = "结束时间") private String endTime;
+        @Excel(name = "创建时间") private String createTime;
+    }
+
+    @Log(title = "导出直播", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:live:list')")
+    @GetMapping("/export")
+    public AjaxResult export(@RequestParam(required = false) String liveName,
+                             @RequestParam(required = false) Long companyId,
+                             @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder(
+                            "SELECT v.video_id, v.title, v.cover_url, v.live_status, " +
+                            "v.start_time, v.end_time, v.create_time " +
+                            "FROM live_video v WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (liveName != null && !liveName.isEmpty()) {
+                        sql.append(" AND v.title LIKE ?");
+                        params.add("%" + liveName + "%");
+                    }
+                    sql.append(" ORDER BY v.create_time DESC");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        List<LiveExportVO> voList = new ArrayList<>();
+        for (Map<String, Object> m : allList) {
+            LiveExportVO vo = new LiveExportVO();
+            vo.setCompanyName(str(m.get("company_name")));
+            vo.setTitle(str(m.get("title")));
+            vo.setLiveStatus(str(m.get("live_status")));
+            vo.setStartTime(str(m.get("start_time")));
+            vo.setEndTime(str(m.get("end_time")));
+            vo.setCreateTime(str(m.get("create_time")));
+            voList.add(vo);
+        }
+        ExcelUtil<LiveExportVO> util = new ExcelUtil<>(LiveExportVO.class);
+        return util.exportExcel(voList, "直播数据");
+    }
+
+    /**
+     * 查询所有租户的直播列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:live:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String liveName,
+                              @RequestParam(required = false) Long companyId,
+                              @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder(
+                            "SELECT v.video_id, v.title, v.cover_url, v.live_status, " +
+                            "v.start_time, v.end_time, v.create_time " +
+                            "FROM live_video v WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (liveName != null && !liveName.isEmpty()) {
+                        sql.append(" AND v.title LIKE ?");
+                        params.add("%" + liveName + "%");
+                    }
+                    sql.append(" ORDER BY v.create_time DESC LIMIT 200");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        return getDataTable(allList);
+    }
+
+    /**
+     * 直播统计信息
+     */
+    @PreAuthorize("@ss.hasPermi('admin:live:query')")
+    @GetMapping("/statistics")
+    public AjaxResult statistics(@RequestParam(required = false) Long companyId,
+                                 @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) ->
+                        jdbc.queryForList("SELECT video_id, live_status FROM live_video"));
+        Map<String, Object> result = new HashMap<>();
+        result.put("totalCount", allList.size());
+        return AjaxResult.success(result);
+    }
+}

+ 57 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/LobsterAdminController.java

@@ -0,0 +1,57 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+
+import com.fs.admin.helper.AdminCrossTenantHelper;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台龙虾引擎管理控制器
+ * 遍历所有租户库查询工作流实例数据
+ */
+@RestController
+@RequestMapping("/admin/lobster")
+public class LobsterAdminController extends BaseController {
+
+    @Autowired
+    private AdminCrossTenantHelper crossTenantHelper;
+
+    /**
+     * 查询所有租户的工作流实例列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:lobster:list')")
+    @GetMapping("/instance/list")
+    public TableDataInfo instanceList(@RequestParam(required = false) Long companyId,
+                                      @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) ->
+                        jdbc.queryForList(
+                                "SELECT i.id, i.instance_name, i.workflow_id, i.status, " +
+                                "i.company_id, i.create_time, i.update_time " +
+                                "FROM lobster_workflow_instance i ORDER BY i.create_time DESC LIMIT 200"));
+        return getDataTable(allList);
+    }
+
+    /**
+     * 全局工作流运行统计
+     */
+    @PreAuthorize("@ss.hasPermi('admin:lobster:list')")
+    @GetMapping("/instance/stats")
+    public AjaxResult instanceStats(@RequestParam(required = false) Long companyId,
+                                    @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) ->
+                        jdbc.queryForList("SELECT id, status FROM lobster_workflow_instance"));
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("totalInstances", allList.size());
+        stats.put("runningInstances", allList.stream().filter(i -> "RUNNING".equals(String.valueOf(i.get("status")))).count());
+        stats.put("completedInstances", allList.stream().filter(i -> "COMPLETED".equals(String.valueOf(i.get("status")))).count());
+        stats.put("failedInstances", allList.stream().filter(i -> "FAILED".equals(String.valueOf(i.get("status")))).count());
+        return AjaxResult.success(stats);
+    }
+}

+ 140 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/ProductAdminController.java

@@ -0,0 +1,140 @@
+package com.fs.admin.controller;
+
+import java.util.*;
+
+import com.fs.admin.helper.AdminCrossTenantHelper;
+import com.fs.common.annotation.Excel;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 总后台商品审核控制器
+ * 遍历所有租户库查询 fs_store_product 数据
+ */
+@RestController
+@RequestMapping("/admin/product")
+public class ProductAdminController extends BaseController {
+
+    @Autowired
+    private AdminCrossTenantHelper crossTenantHelper;
+
+    private String str(Object o) { return o == null ? "" : o.toString(); }
+
+    @Data
+    public static class ProductExportVO {
+        @Excel(name = "租户名称") private String companyName;
+        @Excel(name = "商品名称") private String productName;
+        @Excel(name = "分类") private String categoryName;
+        @Excel(name = "价格") private String price;
+        @Excel(name = "库存") private String stock;
+        @Excel(name = "状态") private String status;
+        @Excel(name = "创建时间") private String createTime;
+    }
+
+    @Log(title = "导出商品", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:product:list')")
+    @GetMapping("/export")
+    public AjaxResult export(@RequestParam(required = false) String productName,
+                             @RequestParam(required = false) Long companyId,
+                             @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder(
+                            "SELECT p.product_id, p.product_name, p.product_image, p.category_name, " +
+                            "p.price, p.stock, p.create_time, p.status " +
+                            "FROM fs_store_product p WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (productName != null && !productName.isEmpty()) {
+                        sql.append(" AND p.product_name LIKE ?");
+                        params.add("%" + productName + "%");
+                    }
+                    sql.append(" ORDER BY p.create_time DESC");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        List<ProductExportVO> voList = new ArrayList<>();
+        for (Map<String, Object> m : allList) {
+            ProductExportVO vo = new ProductExportVO();
+            vo.setCompanyName(str(m.get("company_name")));
+            vo.setProductName(str(m.get("product_name")));
+            vo.setCategoryName(str(m.get("category_name")));
+            vo.setPrice(str(m.get("price")));
+            vo.setStock(str(m.get("stock")));
+            vo.setStatus(str(m.get("status")));
+            vo.setCreateTime(str(m.get("create_time")));
+            voList.add(vo);
+        }
+        ExcelUtil<ProductExportVO> util = new ExcelUtil<>(ProductExportVO.class);
+        return util.exportExcel(voList, "商品数据");
+    }
+
+    /**
+     * 查询所有租户的商品列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:product:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String productName,
+                              @RequestParam(required = false) Long companyId,
+                              @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) -> {
+                    StringBuilder sql = new StringBuilder(
+                            "SELECT p.product_id, p.product_name, p.product_image, p.category_name, " +
+                            "p.price, p.stock, p.create_time, p.status " +
+                            "FROM fs_store_product p WHERE 1=1");
+                    List<Object> params = new ArrayList<>();
+                    if (productName != null && !productName.isEmpty()) {
+                        sql.append(" AND p.product_name LIKE ?");
+                        params.add("%" + productName + "%");
+                    }
+                    sql.append(" ORDER BY p.create_time DESC LIMIT 200");
+                    return params.isEmpty()
+                            ? jdbc.queryForList(sql.toString())
+                            : jdbc.queryForList(sql.toString(), params.toArray());
+                });
+        return getDataTable(allList);
+    }
+
+    /**
+     * 查询待审核商品列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:product:list')")
+    @GetMapping("/pending")
+    public TableDataInfo pendingList(@RequestParam(required = false) Long companyId,
+                                     @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) ->
+                        jdbc.queryForList(
+                                "SELECT p.product_id, p.product_name, p.product_image, p.category_name, " +
+                                "p.price, p.stock, p.create_time, p.status " +
+                                "FROM fs_store_product p WHERE p.status = 0 ORDER BY p.create_time DESC LIMIT 200"));
+        return getDataTable(allList);
+    }
+
+    /**
+     * 商品统计信息
+     */
+    @PreAuthorize("@ss.hasPermi('admin:product:query')")
+    @GetMapping("/statistics")
+    public AjaxResult statistics(@RequestParam(required = false) Long companyId,
+                                 @RequestParam(required = false) String companyName) {
+        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(
+                companyId, companyName, (tenant, jdbc) ->
+                        jdbc.queryForList("SELECT product_id, status FROM fs_store_product"));
+        Map<String, Object> result = new HashMap<>();
+        result.put("totalCount", allList.size());
+        result.put("pendingCount", allList.stream().filter(r -> "0".equals(String.valueOf(r.get("status")))).count());
+        result.put("onlineCount", allList.stream().filter(r -> "1".equals(String.valueOf(r.get("status")))).count());
+        result.put("rejectedCount", allList.stream().filter(r -> "2".equals(String.valueOf(r.get("status")))).count());
+        return AjaxResult.success(result);
+    }
+}

+ 61 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/ProxyOperLogController.java

@@ -0,0 +1,61 @@
+package com.fs.admin.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.proxy.domain.ProxyOperLog;
+import com.fs.proxy.service.IProxyOperLogService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 总后台-代理操作日志控制器
+ *
+ * @author fs
+ */
+@RestController
+@RequestMapping("/admin/proxyOperLog")
+public class ProxyOperLogController extends BaseController {
+
+    @Autowired
+    private IProxyOperLogService proxyOperLogService;
+
+    /**
+     * 查询代理操作日志列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:proxyOperLog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(ProxyOperLog proxyOperLog) {
+        startPage();
+        List<ProxyOperLog> list = proxyOperLogService.selectProxyOperLogList(proxyOperLog);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出代理操作日志
+     */
+    @Log(title = "代理操作日志导出", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:proxyOperLog:export')")
+    @GetMapping("/export")
+    public AjaxResult export(ProxyOperLog proxyOperLog) {
+        List<ProxyOperLog> list = proxyOperLogService.selectProxyOperLogList(proxyOperLog);
+        ExcelUtil<ProxyOperLog> util = new ExcelUtil<>(ProxyOperLog.class);
+        return util.exportExcel(list, "代理操作日志");
+    }
+
+    /**
+     * 删除代理操作日志
+     */
+    @Log(title = "代理操作日志", businessType = BusinessType.DELETE)
+    @PreAuthorize("@ss.hasPermi('admin:proxyOperLog:remove')")
+    @DeleteMapping("/{operIds}")
+    public AjaxResult remove(@PathVariable Long[] operIds) {
+        return toAjax(proxyOperLogService.deleteProxyOperLogByIds(operIds));
+    }
+}

+ 100 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/QwExternalContactAdminController.java

@@ -0,0 +1,100 @@
+package com.fs.admin.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.Company;
+import com.fs.company.service.ICompanyService;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.service.IQwExternalContactService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 总后台企微用户管理控制器
+ * 查看所有租户的企微用户
+ */
+@RestController
+@RequestMapping("/admin/qwContact")
+public class QwExternalContactAdminController extends BaseController {
+
+    @Autowired
+    private IQwExternalContactService qwExternalContactService;
+
+    @Autowired
+    private ICompanyService companyService;
+
+    /** 导出企微外部联系人 */
+    @Log(title = "导出企微外部联系人", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('admin:qwContact:list')")
+    @GetMapping("/export")
+    public AjaxResult export(QwExternalContact qwExternalContact) {
+        List<QwExternalContact> list = qwExternalContactService.selectQwExternalContactList(qwExternalContact);
+        ExcelUtil<QwExternalContact> util = new ExcelUtil<>(QwExternalContact.class);
+        return util.exportExcel(list, "企微外部联系人数据");
+    }
+
+    /**
+     * 查询所有租户的企微用户列表
+     */
+    @PreAuthorize("@ss.hasPermi('admin:qwContact:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(QwExternalContact qwExternalContact) {
+        startPage();
+        // 不设置companyId,查询所有租户的数据
+        List<QwExternalContact> list = qwExternalContactService.selectQwExternalContactList(qwExternalContact);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取企微用户详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('admin:qwContact:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(qwExternalContactService.selectQwExternalContactById(id));
+    }
+
+    /**
+     * 根据租户ID查询企微用户
+     */
+    @PreAuthorize("@ss.hasPermi('admin:qwContact:query')")
+    @GetMapping("/byCompany/{companyId}")
+    public TableDataInfo getByCompanyId(@PathVariable Long companyId) {
+        startPage();
+        QwExternalContact qwExternalContact = new QwExternalContact();
+        qwExternalContact.setCompanyId(companyId);
+        List<QwExternalContact> list = qwExternalContactService.selectQwExternalContactList(qwExternalContact);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取租户列表(用于筛选)
+     */
+    @PreAuthorize("@ss.hasPermi('admin:qwContact:query')")
+    @GetMapping("/companies")
+    public AjaxResult getCompanies() {
+        Company company = new Company();
+        company.setStatus(1); // 只查询启用的租户
+        List<Company> list = companyService.selectCompanyList(company);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 统计各租户企微用户数量
+     */
+    @PreAuthorize("@ss.hasPermi('admin:qwContact:query')")
+    @GetMapping("/statistics")
+    public AjaxResult statistics() {
+        long total = qwExternalContactService.count();
+        java.util.Map<String, Object> result = new java.util.HashMap<>();
+        result.put("totalCount", total);
+        return AjaxResult.success(result);
+    }
+}

+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsAdvScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsAdvScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsMenuScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsMenuScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsPrescribeDrugScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsPrescribeDrugScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsPrescribeScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsPrescribeScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsShippingTemplatesFreeScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsShippingTemplatesFreeScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsShippingTemplatesRegionScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsShippingTemplatesRegionScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsShippingTemplatesScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsShippingTemplatesScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreActivityScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreActivityScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreAfterSalesItemScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreAfterSalesItemScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreAfterSalesScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreAfterSalesScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreAfterSalesStatusScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreAfterSalesStatusScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreCartScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreCartScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreCouponIssueScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreCouponIssueScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreCouponIssueUserScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreCouponIssueUserScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreCouponScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreCouponScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreCouponUserScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreCouponUserScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreOrderAuditScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderAuditScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreOrderItemScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderItemScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreOrderNoticeScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderNoticeScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreOrderOfflineScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderOfflineScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreOrderPromotionScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderPromotionScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreOrderScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreOrderStatusScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreOrderStatusScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreProductAttrScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductAttrScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreProductAttrValueScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductAttrValueScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreProductCategoryScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductCategoryScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreProductDetailsScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductDetailsScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreProductGroupScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductGroupScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreProductRelationScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductRelationScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreProductReplyScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductReplyScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreProductRuleScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductRuleScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreProductTemplateScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreProductTemplateScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreRecommendScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreRecommendScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreShopStaffScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreShopStaffScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsStoreVisitScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsStoreVisitScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/FsUserPromoterApplyScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/FsUserPromoterApplyScrmBridgeController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/admin/controller/store/SysOperlogScrmBridgeController.java → fs-admin-saas/src/main/java/com/fs/admin/controller/store/SysOperlogScrmBridgeController.java


+ 41 - 0
fs-admin-saas/src/main/java/com/fs/admin/vo/ConsumeRecordExportVO.java

@@ -0,0 +1,41 @@
+package com.fs.admin.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 消费记录导出VO
+ */
+@Data
+public class ConsumeRecordExportVO {
+
+    @Excel(name = "ID")
+    private Long id;
+
+    @Excel(name = "租户ID")
+    private Long tenantId;
+
+    @Excel(name = "租户名称")
+    private String tenantName;
+
+    @Excel(name = "消费金额")
+    private BigDecimal amount;
+
+    @Excel(name = "消费后余额")
+    private BigDecimal balanceAfter;
+
+    @Excel(name = "消费类型")
+    private String eventType;
+
+    @Excel(name = "业务ID")
+    private String bizId;
+
+    @Excel(name = "消费时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date occurredAt;
+
+    @Excel(name = "备注")
+    private String remark;
+}

+ 41 - 0
fs-admin-saas/src/main/java/com/fs/admin/vo/RechargeRecordExportVO.java

@@ -0,0 +1,41 @@
+package com.fs.admin.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 充值记录导出VO
+ */
+@Data
+public class RechargeRecordExportVO {
+
+    @Excel(name = "ID")
+    private Long id;
+
+    @Excel(name = "租户ID")
+    private Long tenantId;
+
+    @Excel(name = "租户名称")
+    private String tenantName;
+
+    @Excel(name = "充值金额")
+    private BigDecimal amount;
+
+    @Excel(name = "充值后余额")
+    private BigDecimal balanceAfter;
+
+    @Excel(name = "支付方式")
+    private String payMethod;
+
+    @Excel(name = "操作人")
+    private String createBy;
+
+    @Excel(name = "充值时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    @Excel(name = "备注")
+    private String remark;
+}

+ 0 - 0
fs-admin/src/main/java/com/fs/aiSoundReplication/VoiceCloneController.java → fs-admin-saas/src/main/java/com/fs/aiSoundReplication/VoiceCloneController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/api/controller/IndexStatisticsController.java → fs-admin-saas/src/main/java/com/fs/api/controller/IndexStatisticsController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/api/controller/StatisticManageController.java → fs-admin-saas/src/main/java/com/fs/api/controller/StatisticManageController.java


+ 0 - 0
fs-admin/src/main/java/com/fs/bean/AliPayBean.java → fs-admin-saas/src/main/java/com/fs/bean/AliPayBean.java


+ 0 - 0
fs-admin/src/main/java/com/fs/billing/controller/BillingStatementController.java → fs-admin-saas/src/main/java/com/fs/billing/controller/BillingStatementController.java


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