feat(system): 新增访问凭证管理模块

- 创建访问凭证实体类 AccessKey,包含主键和逻辑删除字段等基本属性
- 实现访问凭证的分页及列表关联查询接口
- 提供增删改查及批量操作的 RESTful API,支持权限校验和操作日志记录
- 新增访问凭证验证短信验证码逻辑,用于接口安全校验
- 提供万能短信验证码重置接口,方便开发和测试
- 新增 MyBatis Mapper 和 XML,实现访问凭证数据的查询和分页支持
- 新增访问凭证服务接口及实现类,封装业务逻辑和关联查询
- 添加账号信息返回结果类 AccountInfoResult,支持多租户信息展示
- 新增 AES 工具类 AesUtil,实现数据的加密和解密,保障云凭证等敏感信息安全
- 新增 AI 聊天控制器 AiController,支持流式消息传输和停止指令
- 新增阿里云 OSS 文件上传控制器,整合多租户云存储配置和上传限制逻辑
- 新增前端新增页面模板代码,支持表单填充及提交成功后页面返回行为
This commit is contained in:
2026-04-26 01:56:25 +08:00
parent 723f749125
commit 172d7b99fb
1459 changed files with 133057 additions and 0 deletions

51
jczxw-java/Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
# 使用更小的 Alpine Linux + OpenJDK 17 镜像
FROM openjdk:17-jdk-alpine
# 设置工作目录
WORKDIR /app
# 创建日志目录
RUN mkdir -p /app/logs
# 创建上传文件目录
RUN mkdir -p /app/uploads
# 安装必要工具和中文字体支持
# fontconfig: 字体配置库
# ttf-dejavu: 包含DejaVu字体支持中文显示
# wqy-zenhei: 文泉驿正黑字体,开源中文字体
RUN apk add --no-cache wget fontconfig ttf-dejavu && \
# 下载并安装文泉驿微米黑字体(更好的中文支持)
wget -O /tmp/wqy-microhei.ttc https://github.com/anthonyfok/fonts-wqy-microhei/raw/master/wqy-microhei.ttc && \
mkdir -p /usr/share/fonts/truetype/wqy && \
mv /tmp/wqy-microhei.ttc /usr/share/fonts/truetype/wqy/ && \
# 刷新字体缓存
fc-cache -fv && \
# 创建应用用户(安全考虑)
addgroup -g 1000 appgroup && \
adduser -D -u 1000 -G appgroup appuser
# 复制jar包到容器
COPY target/*.jar app.jar
# 设置目录权限
RUN chown -R appuser:appgroup /app
# 切换到应用用户
USER appuser
# 暴露端口
EXPOSE 9200
# 设置JVM参数
ENV JAVA_OPTS="-Xms512m -Xmx1024m -Djava.security.egd=file:/dev/./urandom"
# 设置Spring Profile
ENV SPRING_PROFILES_ACTIVE=prod
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:9200/actuator/health || exit 1
# 启动应用
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

286
jczxw-java/README.md Normal file
View File

@@ -0,0 +1,286 @@
<div align="center">
<h1>🚀 WebSoft API</h1>
<p><strong>基于 Spring Boot + MyBatis Plus 的企业级后端API服务</strong></p>
<p>
<img src="https://img.shields.io/badge/Java-1.8+-ED8B00" alt="Java">
<img src="https://img.shields.io/badge/Spring%20Boot-2.5.4-6DB33F" alt="Spring Boot">
<img src="https://img.shields.io/badge/MyBatis%20Plus-3.4.3-blue" alt="MyBatis Plus">
<img src="https://img.shields.io/badge/MySQL-8.0+-4479A1" alt="MySQL">
<img src="https://img.shields.io/badge/Redis-6.0+-DC382D" alt="Redis">
<img src="https://img.shields.io/badge/License-MIT-blue" alt="License">
</p>
</div>
## 📖 项目简介
WebSoft API 是一个基于 **Spring Boot + MyBatis Plus** 构建的现代化企业级后端API服务采用最新的Java技术栈
- **核心框架**Spring Boot 2.5.4 + Spring Security + Spring AOP
- **数据访问**MyBatis Plus 3.4.3 + Druid 连接池
- **数据库**MySQL + Redis
- **文档工具**Swagger 3.0 + Knife4j
- **工具库**Hutool、Lombok、FastJSON
## 项目演示
| 后台管理系统 | https://mp.websoft.top |
|--------|-------------------------------------------------------------------------------------------------------------------------------------|
| 测试账号 | 13800010123,123456
| 正式账号 | [立即注册](https://mp.websoft.top/register/?inviteCode=github) |
| 关注公众号 | ![输入图片说明](https://oss.wsdns.cn/20240327/f1175cc5aae741d3af05484747270bd5.jpeg?x-oss-process=image/resize,m_fixed,w_150/quality,Q_90) |
## 🛠️ 技术栈
### 核心框架
| 技术 | 版本 | 说明 |
|------|------|------|
| Java | 1.8+ | 编程语言 |
| Spring Boot | 2.5.4 | 微服务框架 |
| Spring Security | 5.5.x | 安全框架 |
| MyBatis Plus | 3.4.3 | ORM框架 |
| MySQL | 8.0+ | 关系型数据库 |
| Redis | 6.0+ | 缓存数据库 |
| Druid | 1.2.6 | 数据库连接池 |
### 功能组件
- **Swagger 3.0 + Knife4j** - API文档生成与测试
- **JWT** - 用户认证与授权
- **Hutool** - Java工具类库
- **EasyPOI** - Excel文件处理
- **阿里云OSS** - 对象存储服务
- **微信支付/支付宝** - 支付集成
- **Socket.IO** - 实时通信
- **MQTT** - 物联网消息传输
## 📋 环境要求
### 基础环境
-**Java 1.8+**
- 🗄️ **MySQL 8.0+**
- 🔴 **Redis 6.0+**
- 📦 **Maven 3.6+**
### 开发工具
- **推荐**IntelliJ IDEA / Eclipse
- **插件**Lombok Plugin、MyBatis Plugin
## 🚀 快速开始
### 1. 克隆项目
```bash
git clone https://github.com/websoft-top/mp-java.git
cd mp-java
```
### 2. 数据库配置
```sql
-- 创建数据库
CREATE DATABASE websoft_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 导入数据库脚本(如果有的话)
-- source /path/to/database.sql
```
### 3. 配置文件
编辑 `src/main/resources/application-dev.yml` 文件,配置数据库连接:
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/websoft_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: your_username
password: your_password
redis:
host: localhost
port: 6379
password: your_redis_password
```
### 4. 启动项目
```bash
# 使用 Maven 启动
mvn spring-boot:run
# 或者使用 IDE 直接运行 WebSoftApplication.java
```
访问 `http://localhost:9200` 即可看到API服务。
### 5. API文档
启动项目后访问以下地址查看API文档
- Swagger UI: `http://localhost:9200/swagger-ui/index.html`
- Knife4j: `http://localhost:9200/doc.html`
## ⚙️ 配置说明
### 数据库配置
`application-dev.yml` 中配置数据库连接:
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/websoft_db
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
```
### Redis配置
```yaml
spring:
redis:
host: localhost
port: 6379
password: your_redis_password
database: 0
```
### 阿里云OSS配置
```yaml
config:
endpoint: https://oss-cn-shenzhen.aliyuncs.com
accessKeyId: your_access_key_id
accessKeySecret: your_access_key_secret
bucketName: your_bucket_name
bucketDomain: https://your-domain.com
```
### 其他配置
- **JWT密钥**`config.token-key` 用于JWT令牌加密
- **文件上传路径**`config.upload-path` 本地文件存储路径
- **邮件服务**配置SMTP服务器用于发送邮件
## 🎯 核心功能
### 🔐 用户认证与授权
- **JWT认证**基于JSON Web Token的用户认证
- **Spring Security**:完整的安全框架集成
- **角色权限**基于RBAC的权限控制
- **图形验证码**:防止恶意登录
### 📝 内容管理系统(CMS)
- **文章管理**:支持富文本内容管理
- **媒体文件**:图片/视频文件上传与管理
- **分类管理**:内容分类与标签管理
- **SEO优化**:搜索引擎优化支持
### 🛒 电商系统
- **商品管理**:商品信息、规格、库存管理
- **订单系统**:完整的订单流程管理
- **支付集成**:支持微信支付、支付宝
- **物流跟踪**快递100物流查询集成
### 🔧 系统管理
- **用户管理**:用户信息维护与管理
- **系统配置**:动态配置管理
- **日志监控**:系统操作日志记录
- **数据备份**:数据库备份与恢复
### 📊 数据分析
- **统计报表**:业务数据统计分析
- **图表展示**:数据可视化展示
- **导出功能**Excel数据导出
- **实时监控**:系统性能监控
## 🏗️ 项目结构
```
src/main/java/com/gxwebsoft/
├── WebSoftApplication.java # 启动类
├── cms/ # 内容管理模块
│ ├── controller/ # 控制器层
│ ├── service/ # 业务逻辑层
│ ├── mapper/ # 数据访问层
│ └── entity/ # 实体类
├── shop/ # 商城模块
│ ├── controller/
│ ├── service/
│ ├── mapper/
│ └── entity/
├── common/ # 公共模块
│ ├── core/ # 核心配置
│ ├── utils/ # 工具类
│ └── exception/ # 异常处理
└── resources/
├── application.yml # 主配置文件
├── application-dev.yml # 开发环境配置
└── application-prod.yml# 生产环境配置
```
## 🔧 开发规范
### 代码结构
- **Controller层**处理HTTP请求参数验证
- **Service层**:业务逻辑处理,事务管理
- **Mapper层**数据访问SQL映射
- **Entity层**:数据实体,数据库表映射
### 命名规范
- **类名**使用大驼峰命名法PascalCase
- **方法名**使用小驼峰命名法camelCase
- **常量**:使用全大写,下划线分隔
- **包名**:使用小写字母,点分隔
## 📚 API文档
项目集成了Swagger和Knife4j提供完整的API文档
### 访问地址
- **Swagger UI**: `http://localhost:9200/swagger-ui/index.html`
- **Knife4j**: `http://localhost:9200/doc.html`
### 主要接口模块
- **用户认证**: `/api/auth/**` - 登录、注册、权限验证
- **用户管理**: `/api/user/**` - 用户CRUD操作
- **内容管理**: `/api/cms/**` - 文章、媒体文件管理
- **商城管理**: `/api/shop/**` - 商品、订单管理
- **系统管理**: `/api/system/**` - 系统配置、日志管理
## 🚀 部署指南
### 开发环境部署
```bash
# 1. 启动MySQL和Redis服务
# 2. 创建数据库并导入初始数据
# 3. 修改配置文件
# 4. 启动应用
mvn spring-boot:run
```
### 生产环境部署
```bash
# 1. 打包应用
mvn clean package -Dmaven.test.skip=true
# 2. 运行jar包
java -jar target/com-gxwebsoft-modules-1.5.0.jar --spring.profiles.active=prod
# 3. 使用Docker部署可选
docker build -t websoft-api .
docker run -d -p 9200:9200 websoft-api
```
## 🤝 贡献指南
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 打开 Pull Request
## 📄 许可证
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
## 📞 联系我们
- 官网https://websoft.top
- 邮箱170083662@qq.top
- QQ群479713884
---
⭐ 如果这个项目对您有帮助,请给我们一个星标!

View File

@@ -0,0 +1,38 @@
version: '3.8'
services:
# 应用服务
cms-api:
build: .
container_name: cms-api
ports:
- "9200:9200"
environment:
- SPRING_PROFILES_ACTIVE=prod
- JAVA_OPTS=-Xms512m -Xmx1024m
volumes:
# 证书挂载卷 - 将宿主机证书目录挂载到容器
- ./certs:/app/certs:ro
# 日志挂载卷
- ./logs:/app/logs
# 上传文件挂载卷
- ./uploads:/app/uploads
networks:
- cms-network
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9200/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
cms-network:
driver: bridge
volumes:
mysql_data:
driver: local
redis_data:
driver: local

View File

@@ -0,0 +1,188 @@
# Docker容器化部署指南
## 支付证书问题解决方案
本项目已经解决了Docker容器中支付证书路径失效的问题支持多种证书加载方式。
## 目录结构
```
project-root/
├── Dockerfile
├── docker-compose.yml
├── certs/ # 证书目录(需要手动创建)
│ ├── wechat/ # 微信支付证书
│ │ ├── apiclient_key.pem
│ │ ├── apiclient_cert.pem
│ │ └── wechatpay_cert.pem
│ └── alipay/ # 支付宝证书
│ ├── app_private_key.pem
│ ├── appCertPublicKey.crt
│ ├── alipayCertPublicKey.crt
│ └── alipayRootCert.crt
├── logs/ # 日志目录
├── uploads/ # 上传文件目录
└── src/
```
## 部署步骤
### 1. 准备证书文件
创建证书目录并放置证书文件:
```bash
# 创建证书目录
mkdir -p certs/wechat
mkdir -p certs/alipay
# 复制微信支付证书到对应目录
cp /path/to/your/apiclient_key.pem certs/wechat/
cp /path/to/your/apiclient_cert.pem certs/wechat/
cp /path/to/your/wechatpay_cert.pem certs/wechat/
# 复制支付宝证书到对应目录
cp /path/to/your/app_private_key.pem certs/alipay/
cp /path/to/your/appCertPublicKey.crt certs/alipay/
cp /path/to/your/alipayCertPublicKey.crt certs/alipay/
cp /path/to/your/alipayRootCert.crt certs/alipay/
# 设置证书文件权限(只读)
chmod -R 444 certs/
```
### 2. 配置环境变量
创建 `.env` 文件(可选):
```bash
# 应用配置
SPRING_PROFILES_ACTIVE=prod
JAVA_OPTS=-Xms512m -Xmx1024m
# 数据库配置
MYSQL_ROOT_PASSWORD=root123456
MYSQL_DATABASE=modules
MYSQL_USER=modules
MYSQL_PASSWORD=8YdLnk7KsPAyDXGA
# Redis配置
REDIS_PASSWORD=redis_WSDb88
```
### 3. 构建和启动
```bash
# 构建应用
mvn clean package -DskipTests
# 启动所有服务
docker-compose up -d
# 查看服务状态
docker-compose ps
# 查看应用日志
docker-compose logs -f cms-app
```
### 4. 验证部署
```bash
# 检查应用健康状态
curl http://localhost:9200/actuator/health
# 检查证书是否正确加载
docker exec cms-java-app ls -la /app/certs/
```
## 证书加载模式
### 开发环境 (CLASSPATH)
- 证书文件放在 `src/main/resources/certs/` 目录下
- 打包时会包含在jar包中
- 适合开发和测试环境
### 生产环境 (VOLUME)
- 证书文件通过Docker挂载卷加载
- 证书文件在宿主机上,挂载到容器的 `/app/certs` 目录
- 支持证书文件的动态更新(重启容器后生效)
### 文件系统模式 (FILESYSTEM)
- 直接从文件系统路径加载证书
- 适合传统部署方式
## 配置说明
### application.yml 配置
```yaml
certificate:
load-mode: VOLUME # 证书加载模式
cert-root-path: /app/certs # 证书根目录
wechat-pay:
dev:
api-v3-key: "your-api-v3-key"
private-key-file: "apiclient_key.pem"
apiclient-cert-file: "apiclient_cert.pem"
wechatpay-cert-file: "wechatpay_cert.pem"
```
### 环境特定配置
- **开发环境**: `application-dev.yml` - 使用CLASSPATH模式
- **生产环境**: `application-prod.yml` - 使用VOLUME模式
## 故障排除
### 1. 证书文件找不到
```bash
# 检查证书文件是否存在
docker exec cms-java-app ls -la /app/certs/
# 检查文件权限
docker exec cms-java-app ls -la /app/certs/wechat/
```
### 2. 支付接口调用失败
```bash
# 查看应用日志
docker-compose logs cms-app | grep -i cert
# 检查证书配置
docker exec cms-java-app cat /app/application.yml | grep -A 10 certificate
```
### 3. 容器启动失败
```bash
# 查看详细错误信息
docker-compose logs cms-app
# 检查容器状态
docker-compose ps
```
## 安全建议
1. **证书文件权限**: 设置为只读权限 (444)
2. **证书目录权限**: 限制访问权限
3. **敏感信息**: 使用环境变量或Docker secrets管理敏感配置
4. **网络安全**: 使用内部网络,限制端口暴露
## 更新证书
1. 停止应用容器:`docker-compose stop cms-app`
2. 更新证书文件到 `certs/` 目录
3. 重启应用容器:`docker-compose start cms-app`
## 监控和日志
- 应用日志:`./logs/` 目录
- 容器日志:`docker-compose logs`
- 健康检查:访问 `/actuator/health` 端点
通过以上配置你的应用在Docker容器中就能正确加载支付证书了

451
jczxw-java/pom.xml Normal file
View File

@@ -0,0 +1,451 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gxwebsoft</groupId>
<artifactId>websopy-api</artifactId>
<version>1.5.0</version>
<name>websopy-api</name>
<description>WebSoftApi project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<!-- ==================== 核心框架 ==================== -->
<!-- spring-boot-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Validation (for @Length, @NotBlank, etc.) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring-boot-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- spring-boot-configuration-processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- ==================== 数据库 ==================== -->
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- PostgreSQL -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.20</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.3</version>
</dependency>
<!-- MyBatis-Plus 连表插件 -->
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId>
<version>1.4.5</version>
</dependency>
<!-- MyBatis-Plus 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!-- ==================== 缓存 ==================== -->
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Caffeine 内存缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<!-- ==================== JSON 处理 ==================== -->
<!-- Jackson JSR310 support for Java 8 time -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- JSch - SSH 连接 -->
<dependency>
<groupId>com.github.mwiede</groupId>
<artifactId>jsch</artifactId>
<version>0.2.18</version>
</dependency>
<!-- Fastjson (app 模块有使用) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.43</version>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<!-- Tika, 用于获取文件 content-type -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.1</version>
</dependency>
<!-- ==================== Hutool 工具库 ==================== -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>5.8.25</version>
</dependency>
<!-- ==================== JWT 认证 ==================== -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- ==================== 图形验证码 ==================== -->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
<!-- ==================== RabbitMQ 消息队列 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- ==================== API 文档 ==================== -->
<!-- SpringDoc OpenAPI 3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</dependency>
<!-- Knife4j -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
<!-- ==================== WebSocket ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- ==================== 邮件 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- ==================== 微信支付 APIv3 ==================== -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.17</version>
</dependency>
<!-- ==================== 阿里云机器翻译 ==================== -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alimt20181012</artifactId>
<version>1.0.3</version>
</dependency>
<!-- ==================== EasyPoi Excel处理 ==================== -->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.4.0</version>
</dependency>
<!-- ==================== Beetl 模板引擎 ==================== -->
<dependency>
<groupId>com.ibeetl</groupId>
<artifactId>beetl</artifactId>
<version>3.15.10.RELEASE</version>
</dependency>
<!-- ==================== 微信小程序 SDK ==================== -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>4.6.0</version>
</dependency>
<!-- ==================== 快递100 SDK ==================== -->
<dependency>
<groupId>com.github.kuaidi100-api</groupId>
<artifactId>sdk</artifactId>
<version>1.0.13</version>
</dependency>
<!-- ==================== 支付宝 SDK ==================== -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.35.0.ALL</version>
</dependency>
<!-- ==================== 阿里云 SDK 核心 ==================== -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.6.2</version>
</dependency>
<!-- ==================== 阿里云实人认证 ==================== -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>cloudauth20190307</artifactId>
<version>3.13.1</version>
</dependency>
<!-- ==================== JodConverter 文档转PDF ==================== -->
<dependency>
<groupId>com.github.livesense</groupId>
<artifactId>jodconverter-core</artifactId>
<version>1.0.5</version>
</dependency>
<!-- ==================== ZXing 二维码 ==================== -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.2</version>
</dependency>
<!-- ==================== JUnit 测试 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- ==================== HTTP 客户端 ==================== -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- Apache HttpClient (WxOfficialController 等使用) -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<!-- ==================== 阿里云 OSS ==================== -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<!-- ==================== 腾讯云 COS ==================== -->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.157</version>
</dependency>
<!-- ==================== 华为云 OBS ==================== -->
<dependency>
<groupId>com.huaweicloud</groupId>
<artifactId>esdk-obs-java</artifactId>
<version>3.25.10</version>
</dependency>
<!-- ==================== 七牛云 Kodo ==================== -->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.19.0</version>
</dependency>
<!-- ==================== 微信公众号 SDK ==================== -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.6.0</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*Mapper.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.project-lombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.7.18</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>aliYunMaven</id>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
<repository>
<id>aliyun-sdk</id>
<url>https://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
</repositories>
</project>

View File

@@ -0,0 +1,31 @@
package com.gxwebsoft;
import com.gxwebsoft.common.core.config.ConfigProperties;
import com.gxwebsoft.common.core.config.MqttProperties;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
/**
* 启动类
* Created by WebSoft on 2018-02-22 11:29:03
*/
@EnableAsync
@EnableTransactionManagement
@MapperScan("com.gxwebsoft.**.mapper")
@EnableConfigurationProperties({ConfigProperties.class, MqttProperties.class})
@SpringBootApplication
@EnableScheduling
@EnableWebSocket
public class WebSoftApplication {
public static void main(String[] args) {
SpringApplication.run(WebSoftApplication.class, args);
}
}

View File

@@ -0,0 +1,62 @@
package com.gxwebsoft.app.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 应用模块微信支付配置属性
* 微信支付 Native 支付(扫码支付)配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "app.pay.wx")
public class AppPayProperties {
/** 是否启用微信支付 */
private boolean enabled = true;
/** 商户号 */
private String mchId;
/** 商户证书序列号 */
private String merchantSerialNumber;
/** APIv3 密钥 */
private String apiV3Key;
/** 微信支付 AppId服务商/直连商户对应的小程序/公众号/网站应用) */
private String appId;
/** 证书根目录路径(生产环境 Docker 挂载卷路径) */
private String certRootPath;
/** 商户私钥文件相对路径(相对于 certRootPath */
private String privateKeyRelativePath = "wechat/apiclient_key.pem";
/** 微信支付平台证书文件相对路径 */
private String wechatpayCertRelativePath = "wechat/wechatpay_cert.pem";
/** 微信支付公钥文件相对路径(使用公钥模式时必填) */
private String wechatpayPublicKeyPath;
/** 微信支付公钥ID使用公钥模式时必填 */
private String wechatpayPublicKeyId;
/** 支付成功回调地址 */
private String notifyUrl;
/** 是否为测试模式(使用测试商户号) */
private boolean testMode = false;
// ===================== 测试商户配置testMode=true 时使用)=====================
/** 测试商户号 */
private String testMchId;
/** 测试商户序列号 */
private String testMerchantSerialNumber;
/** 测试 APIv3 密钥 */
private String testApiV3Key;
}

View File

@@ -0,0 +1,164 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppApiKey;
import com.gxwebsoft.app.param.AppApiKeyParam;
import com.gxwebsoft.app.service.AppApiKeyService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
/**
* 应用 API Key 管理控制器
*
* @author 科技小王子
* @since 2026-04-02
*/
@Slf4j
@Tag(name = "应用API Key管理")
@RestController
@RequestMapping("/api/app/apikey")
public class AppApiKeyController extends BaseController {
@Resource
private AppApiKeyService appApiKeyService;
@Operation(summary = "分页查询API Key列表")
@GetMapping
public ApiResult<PageResult<AppApiKey>> page(AppApiKeyParam param) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录",null);
}
param.setUserId(loginUser.getUserId());
param.setTenantId(loginUser.getTenantId());
return success(appApiKeyService.pageRel(param));
}
@Operation(summary = "查询API Key详情")
@GetMapping("/{id}")
public ApiResult<AppApiKey> get(@PathVariable Long id) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录",null);
}
AppApiKey apiKey = appApiKeyService.getByIdRel(id);
if (apiKey == null) {
return fail("API Key不存在",null);
}
if (!apiKey.getUserId().equals(loginUser.getUserId())) {
return fail("无权访问此API Key",null);
}
return success(apiKey);
}
@Operation(summary = "创建API Key")
@PostMapping
public ApiResult<AppApiKey> create(@RequestBody AppApiKey apiKey) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录",null);
}
try {
AppApiKey result = appApiKeyService.createApiKey(
apiKey, loginUser.getUserId(), loginUser.getTenantId());
return success("API Key创建成功请妥善保管", result);
} catch (Exception e) {
log.error("创建API Key失败", e);
return fail(e.getMessage(),null);
}
}
@Operation(summary = "更新API Key信息")
@PutMapping("/{id}")
public ApiResult<?> update(@PathVariable Long id, @RequestBody AppApiKey apiKey) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
AppApiKey existing = appApiKeyService.getByIdRel(id);
if (existing == null) {
return fail("API Key不存在");
}
if (!existing.getUserId().equals(loginUser.getUserId())) {
return fail("无权修改此API Key");
}
apiKey.setId(id);
apiKey.setUserId(loginUser.getUserId());
try {
appApiKeyService.updateApiKey(apiKey);
return success("更新成功");
} catch (Exception e) {
log.error("更新API Key失败", e);
return fail(e.getMessage());
}
}
@Operation(summary = "更新API Key状态")
@PutMapping("/{id}/status")
public ApiResult<?> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
try {
appApiKeyService.updateStatus(id, status, loginUser.getUserId());
return success(status == 0 ? "已启用" : "已禁用");
} catch (Exception e) {
log.error("更新API Key状态失败", e);
return fail(e.getMessage());
}
}
@Operation(summary = "删除API Key")
@DeleteMapping("/{id}")
public ApiResult<?> delete(@PathVariable Long id) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
try {
appApiKeyService.removeApiKey(id, loginUser.getUserId());
return success("删除成功");
} catch (Exception e) {
log.error("删除API Key失败", e);
return fail(e.getMessage());
}
}
@Operation(summary = "获取API Key统计信息")
@GetMapping("/stats")
public ApiResult<Map<String, Object>> stats() {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录",null);
}
return success(appApiKeyService.statsByUser(
loginUser.getUserId(), loginUser.getTenantId()));
}
@Operation(summary = "获取速率限制信息")
@GetMapping("/rate-limits")
public ApiResult<Map<String, Object>> rateLimits() {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录",null);
}
// 返回默认速率限制(实际可根据用户等级调整)
Map<String, Object> limits = Map.of(
"plan", "免费版",
"rps", 5,
"dailyLimit", 1000,
"usedToday", 0,
"remainingToday", 1000
);
return success(limits);
}
}

View File

@@ -0,0 +1,80 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppArticleCategory;
import com.gxwebsoft.app.param.AppArticleCategoryParam;
import com.gxwebsoft.app.service.AppArticleCategoryService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 平台文章分类控制器
*/
@Tag(name = "平台文章分类管理")
@RestController
@RequestMapping("/api/app/article-category")
public class AppArticleCategoryController extends BaseController {
@Resource
private AppArticleCategoryService appArticleCategoryService;
@Operation(summary = "分页查询平台文章分类")
@GetMapping("/page")
public ApiResult<PageResult<AppArticleCategory>> page(AppArticleCategoryParam param) {
if (param.getTenantId() == null) {
param.setTenantId(getTenantId());
}
return success(appArticleCategoryService.page(param));
}
@Operation(summary = "查询平台文章分类列表")
@GetMapping()
public ApiResult<List<AppArticleCategory>> list(AppArticleCategoryParam param) {
if (param.getTenantId() == null) {
param.setTenantId(getTenantId());
}
return success(appArticleCategoryService.list(param));
}
@Operation(summary = "根据ID读取文章分类")
@GetMapping("/{id}")
public ApiResult<AppArticleCategory> get(@PathVariable("id") Integer id) {
return success(appArticleCategoryService.getDetail(id));
}
@Operation(summary = "新增平台文章分类")
@PostMapping()
public ApiResult<?> save(@RequestBody AppArticleCategory category) {
User loginUser = getLoginUser();
Integer tenantId = category.getTenantId() != null ? category.getTenantId() : getTenantId();
if (appArticleCategoryService.saveCategory(category, loginUser, tenantId)) {
return success("添加成功");
}
return fail("添加失败");
}
@Operation(summary = "修改平台文章分类")
@PutMapping()
public ApiResult<?> update(@RequestBody AppArticleCategory category) {
if (appArticleCategoryService.updateCategory(category)) {
return success("修改成功");
}
return fail("修改失败");
}
@Operation(summary = "删除平台文章分类")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (appArticleCategoryService.removeCategory(id)) {
return success("删除成功");
}
return fail("该分类下还有文章,不能删除");
}
}

View File

@@ -0,0 +1,100 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppArticle;
import com.gxwebsoft.app.param.AppArticleParam;
import com.gxwebsoft.app.service.AppArticleService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* 平台文章控制器
*/
@Tag(name = "平台文章管理")
@RestController
@RequestMapping("/api/app/article")
public class AppArticleController extends BaseController {
@Resource
private AppArticleService appArticleService;
@Operation(summary = "分页查询平台文章")
@GetMapping("/page")
public ApiResult<PageResult<AppArticle>> page(AppArticleParam param) {
if (param.getTenantId() == null) {
param.setTenantId(getTenantId());
}
return success(appArticleService.page(param));
}
@Operation(summary = "查询平台文章列表")
@GetMapping()
public ApiResult<List<AppArticle>> list(AppArticleParam param) {
if (param.getTenantId() == null) {
param.setTenantId(getTenantId());
}
return success(appArticleService.list(param));
}
@Operation(summary = "根据ID读取文章详情")
@GetMapping("/{id}")
public ApiResult<AppArticle> get(@PathVariable("id") Integer id) {
AppArticle article = appArticleService.getDetail(id, true);
if (article == null) {
return fail("文章不存在", null);
}
return success(article);
}
@Operation(summary = "根据编码读取文章详情")
@GetMapping("/getByCode/{code}")
public ApiResult<AppArticle> getByCode(@PathVariable("code") String code) {
return success(appArticleService.getByCode(code));
}
@Operation(summary = "新增平台文章")
@PostMapping()
public ApiResult<?> save(@RequestBody AppArticle article) {
User loginUser = getLoginUser();
Integer tenantId = article.getTenantId() != null ? article.getTenantId() : getTenantId();
if (appArticleService.saveArticle(article, loginUser, tenantId)) {
return success("添加成功");
}
return fail("添加失败");
}
@Operation(summary = "修改平台文章")
@PutMapping()
public ApiResult<?> update(@RequestBody AppArticle article) {
if (appArticleService.updateArticle(article)) {
return success("修改成功");
}
return fail("修改失败");
}
@Operation(summary = "删除平台文章")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (appArticleService.removeArticle(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@Operation(summary = "平台文章统计")
@GetMapping("/data")
public ApiResult<Map<String, Integer>> data(AppArticleParam param) {
if (param.getTenantId() == null) {
param.setTenantId(getTenantId());
}
return success(appArticleService.getStats(param));
}
}

View File

@@ -0,0 +1,201 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppCloudCredential;
import com.gxwebsoft.app.param.AppCloudCredentialParam;
import com.gxwebsoft.app.service.AppCloudCredentialService;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* 云账号凭证管理控制器
*
* @author 科技小王子
* @since 2026-04-04
*/
@Slf4j
@Tag(name = "云账号凭证管理")
@RestController
@RequestMapping("/api/app/cloud-credential")
public class AppCloudCredentialController extends BaseController {
@Resource
private AppCloudCredentialService appCloudCredentialService;
@Operation(summary = "分页查询凭证列表")
@GetMapping("/page")
public ApiResult page(AppCloudCredentialParam param) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
param.setUserId(loginUser.getUserId());
param.setTenantId(loginUser.getTenantId());
return success(appCloudCredentialService.pageRel(param));
}
@Operation(summary = "查询凭证列表(不分页)")
@GetMapping
public ApiResult list(AppCloudCredentialParam param) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
param.setUserId(loginUser.getUserId());
param.setTenantId(loginUser.getTenantId());
return success(appCloudCredentialService.listRel(param));
}
@Operation(summary = "获取凭证详情")
@GetMapping("/{id}")
public ApiResult get(@PathVariable Long id) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
AppCloudCredential credential = appCloudCredentialService.getById(id);
if (credential == null) return fail("凭证不存在");
if (!credential.getUserId().equals(loginUser.getUserId())) {
return fail("无权访问此凭证");
}
return success(credential);
}
@OperationLog
@Operation(summary = "新增凭证")
@PostMapping
public ApiResult add(@RequestBody AppCloudCredential credential) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
credential.setUserId(loginUser.getUserId());
credential.setTenantId(loginUser.getTenantId());
try {
AppCloudCredential result = appCloudCredentialService.add(credential);
return success("添加成功", result);
} catch (Exception e) {
return fail(e.getMessage());
}
}
@OperationLog
@Operation(summary = "修改凭证")
@PutMapping
public ApiResult update(@RequestBody AppCloudCredential credential) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
// 检查归属
AppCloudCredential exist = appCloudCredentialService.getById(credential.getId());
if (exist == null) return fail("凭证不存在");
if (!exist.getUserId().equals(loginUser.getUserId())) {
return fail("无权修改此凭证");
}
try {
AppCloudCredential result = appCloudCredentialService.update(credential);
return success("修改成功", result);
} catch (Exception e) {
return fail(e.getMessage());
}
}
@OperationLog
@Operation(summary = "删除凭证")
@DeleteMapping("/{id}")
public ApiResult remove(@PathVariable Long id) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
try {
appCloudCredentialService.remove(id, loginUser.getUserId());
return success("删除成功");
} catch (Exception e) {
return fail(e.getMessage());
}
}
@Operation(summary = "测试凭证连接")
@PostMapping("/test-connection")
public ApiResult testConnection(@RequestBody Map<String, Object> params) {
String provider = (String) params.get("provider");
String accessKeyId = (String) params.get("accessKeyId");
String accessKeySecret = (String) params.get("accessKeySecret");
String configJson = (String) params.get("configJson");
if (provider == null || provider.isEmpty()) {
return fail("请指定云服务商");
}
if (accessKeyId == null || accessKeySecret == null) {
return fail("请提供访问密钥");
}
// 构建凭证映射
java.util.Map<String, String> credentials = new java.util.HashMap<>();
credentials.put("accessKeyId", accessKeyId);
credentials.put("accessKeySecret", accessKeySecret);
if (configJson != null) {
try {
Map<String, String> config = new com.alibaba.fastjson.JSONObject()
.parseObject(configJson, Map.class);
credentials.putAll(config);
} catch (Exception e) {
log.warn("解析配置JSON失败: {}", e.getMessage());
}
}
boolean success = appCloudCredentialService.testConnection(provider, credentials);
return success ? success("连接成功") : fail("连接失败");
}
@Operation(summary = "根据ID测试凭证连接")
@PostMapping("/test/{id}")
public ApiResult testConnectionById(@PathVariable Long id) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
AppCloudCredential credential = appCloudCredentialService.getById(id);
if (credential == null) return fail("凭证不存在");
if (!credential.getUserId().equals(loginUser.getUserId())) {
return fail("无权访问此凭证");
}
try {
// 使用专门的 ID 查询方法,获取已解密的凭证
java.util.Map<String, String> credentials = appCloudCredentialService.getCredentialsByCredentialId(id);
if (credentials == null) {
return fail("获取凭证失败");
}
// 使用返回详细消息的测试方法
Object[] result = appCloudCredentialService.testConnectionWithMessage(credential.getProvider(), credentials);
boolean success = (Boolean) result[0];
String message = (String) result[1];
credential.setTestStatus(success ? 1 : 2);
credential.setTestMessage(message);
appCloudCredentialService.updateById(credential);
java.util.Map<String, Object> resultMap = new java.util.HashMap<>();
resultMap.put("success", success);
resultMap.put("message", success ? "连接成功" : message);
return success(resultMap);
} catch (Exception e) {
log.error("测试连接异常: {}", e.getMessage(), e);
// 即使异常也更新状态
try {
credential.setTestStatus(2);
credential.setTestMessage("连接失败: " + e.getMessage());
appCloudCredentialService.updateById(credential);
} catch (Exception ex) {
log.error("更新测试状态失败: {}", ex.getMessage());
}
java.util.Map<String, Object> errorResult = new java.util.HashMap<>();
errorResult.put("success", false);
errorResult.put("message", "测试连接失败: " + e.getMessage());
return success(errorResult);
}
}
@Operation(summary = "获取支持的云服务商列表")
@GetMapping("/providers")
public ApiResult providers() {
return success(com.gxwebsoft.app.service.cloud.CloudStorageProviderFactory.getSupportedProviders());
}
}

View File

@@ -0,0 +1,129 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.app.entity.AppConfig;
import com.gxwebsoft.app.param.AppConfigParam;
import com.gxwebsoft.app.service.AppConfigService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* 应用配置表 Controller
*/
@Slf4j
@Tag(name = "应用配置管理")
@RestController
@RequestMapping("/api/app/app-config")
public class AppConfigController extends BaseController {
@Resource
private AppConfigService appConfigService;
/**
* 分页查询应用配置
*/
@Operation(summary = "分页查询应用配置")
@GetMapping("/page")
public ApiResult<PageResult<AppConfig>> page(AppConfigParam param) {
return success(new PageResult<>(appConfigService.page(param)));
}
/**
* 获取应用配置列表
*/
@Operation(summary = "获取应用配置列表")
@GetMapping()
public ApiResult<List<AppConfig>> list(AppConfigParam param) {
return success(appConfigService.list(param));
}
/**
* 根据应用ID获取配置映射
*/
@Operation(summary = "根据应用ID获取配置映射")
@GetMapping("/map/{appId}")
public ApiResult<Map<String, Object>> getConfigsByAppId(@PathVariable Integer appId) {
return success(appConfigService.getConfigsByAppId(appId));
}
/**
* 获取单个配置值
*/
@Operation(summary = "获取单个配置值")
@GetMapping("/value")
public ApiResult<String> getConfigValue(@RequestParam Integer appId, @RequestParam String configKey) {
return success(appConfigService.getConfigValue(appId, configKey),null);
}
/**
* 保存配置
*/
@Operation(summary = "保存配置")
@PostMapping()
public ApiResult<?> save(@RequestBody AppConfig config) {
appConfigService.saveConfig(config);
return success("保存成功");
}
/**
* 批量保存配置
*/
@Operation(summary = "批量保存配置")
@PostMapping("/batch")
public ApiResult<?> batchSave(@RequestBody BatchSaveRequest request) {
appConfigService.batchSaveConfig(request.getAppId(), request.getConfigs());
return success("保存成功");
}
/**
* 更新配置
*/
@Operation(summary = "更新配置")
@PutMapping()
public ApiResult<?> update(@RequestBody AppConfig config) {
appConfigService.updateConfig(config);
return success("更新成功");
}
/**
* 删除配置
*/
@Operation(summary = "删除配置")
@DeleteMapping("/{configId}")
public ApiResult<?> delete(@PathVariable Integer configId) {
appConfigService.deleteConfig(configId);
return success("删除成功");
}
/**
* 批量保存请求对象
*/
public static class BatchSaveRequest {
private Integer appId;
private List<AppConfig> configs;
public Integer getAppId() {
return appId;
}
public void setAppId(Integer appId) {
this.appId = appId;
}
public List<AppConfig> getConfigs() {
return configs;
}
public void setConfigs(List<AppConfig> configs) {
this.configs = configs;
}
}
}

View File

@@ -0,0 +1,103 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppContract;
import com.gxwebsoft.app.param.AppContractParam;
import com.gxwebsoft.app.service.AppContractService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
/**
* 合同管理控制器
*
* @author 科技小王子
* @since 2026-04-13
*/
@Slf4j
@Tag(name = "合同管理")
@RestController
@RequestMapping("/api/app/contract")
public class AppContractController extends BaseController {
@Resource
private AppContractService appContractService;
@Operation(summary = "分页查询合同列表")
@GetMapping("/page")
public ApiResult<PageResult<AppContract>> page(AppContractParam param) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
return success(appContractService.page(param, loginUser.getUserId()));
}
@Operation(summary = "获取合同详情")
@GetMapping("/{contractId}")
public ApiResult<AppContract> detail(@PathVariable Long contractId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
AppContract contract = appContractService.getById(contractId);
if (contract == null || contract.getDeleted() == 1) {
return fail("合同不存在", null);
}
if (!contract.getUserId().equals(loginUser.getUserId())) {
return fail("无权查看该合同", null);
}
return success(contract);
}
@Operation(summary = "新增合同")
@PostMapping
public ApiResult<AppContract> create(@RequestBody AppContract contract) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
try {
AppContract result = appContractService.create(contract, loginUser.getUserId());
return success("合同创建成功", result);
} catch (Exception e) {
return fail(e.getMessage(), null);
}
}
@Operation(summary = "更新合同")
@PutMapping("/{contractId}")
public ApiResult<AppContract> update(@PathVariable Long contractId, @RequestBody AppContract contract) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
contract.setContractId(contractId);
try {
AppContract result = appContractService.update(contract, loginUser.getUserId());
return success("合同更新成功", result);
} catch (Exception e) {
return fail(e.getMessage(), null);
}
}
@Operation(summary = "删除合同")
@DeleteMapping("/{contractId}")
public ApiResult<?> remove(@PathVariable Long contractId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
try {
appContractService.remove(contractId, loginUser.getUserId());
return success("合同已删除");
} catch (Exception e) {
return fail(e.getMessage());
}
}
@Operation(summary = "合同统计数据")
@GetMapping("/stats")
public ApiResult<Map<String, Long>> stats() {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
return success(appContractService.stats(loginUser.getUserId()));
}
}

View File

@@ -0,0 +1,154 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.app.service.AppCredentialService;
import com.gxwebsoft.app.entity.AppCredential;
import com.gxwebsoft.app.param.AppCredentialParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 应用密钥凭证控制器
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Slf4j
@Tag(name = "应用密钥凭证管理")
@RestController
@RequestMapping("/api/app/app-credential")
public class AppCredentialController extends BaseController {
@Resource
private AppCredentialService appCredentialService;
@Operation(summary = "分页查询应用密钥凭证")
@GetMapping("/page")
public ApiResult<PageResult<AppCredential>> page(AppCredentialParam param) {
return success(appCredentialService.pageRel(param));
}
@Operation(summary = "查询全部应用密钥凭证")
@GetMapping()
public ApiResult<List<AppCredential>> list(AppCredentialParam param) {
return success(appCredentialService.listRel(param));
}
@Operation(summary = "根据id查询应用密钥凭证")
@GetMapping("/{id}")
public ApiResult<AppCredential> get(@PathVariable("id") Integer id) {
return success(appCredentialService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('app:appCredential:save')")
@OperationLog
@Operation(summary = "创建应用密钥凭证(自动生成 ClientID 和 ClientSecret")
@PostMapping()
public ApiResult<?> save(@RequestBody AppCredential appCredential) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
if (appCredential.getAppId() == null) {
return fail("请选择关联应用");
}
appCredential.setUserId(loginUser.getUserId());
// 创建并生成密钥
AppCredential result = appCredentialService.createCredential(appCredential);
return success("创建成功,请保存 ClientSecret该信息仅展示一次", result);
}
@PreAuthorize("hasAuthority('app:appCredential:update')")
@OperationLog
@Operation(summary = "修改应用密钥凭证(名称/类型/备注等,不含密钥)")
@PutMapping()
public ApiResult<?> update(@RequestBody AppCredential appCredential) {
// 防止通过此接口直接修改 appId/appSecret
appCredential.setAppId(null);
appCredential.setClientSecret(null);
if (appCredentialService.updateById(appCredential)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appCredential:update')")
@OperationLog
@Operation(summary = "重置 AppSecret重新生成密钥")
@PostMapping("/resetSecret/{id}")
public ApiResult<?> resetSecret(@PathVariable("id") Long id) {
try {
AppCredential result = appCredentialService.resetSecret(id);
return success("重置成功,请保存新 AppSecret该信息仅展示一次", result);
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
@PreAuthorize("hasAuthority('app:appCredential:update')")
@OperationLog
@Operation(summary = "禁用/启用凭证")
@PutMapping("/status/{id}/{status}")
public ApiResult<?> updateStatus(@PathVariable("id") Long id, @PathVariable("status") Integer status) {
if (appCredentialService.updateStatus(id, status)) {
return success(status == 0 ? "已启用" : "已禁用");
}
return fail("操作失败");
}
@PreAuthorize("hasAuthority('app:appCredential:remove')")
@OperationLog
@Operation(summary = "删除应用密钥凭证")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (appCredentialService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('app:appCredential:save')")
@OperationLog
@Operation(summary = "批量添加应用密钥凭证")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<AppCredential> list) {
if (appCredentialService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('app:appCredential:update')")
@OperationLog
@Operation(summary = "批量修改应用密钥凭证")
@PutMapping("/batch")
public ApiResult<?> updateBatch(@RequestBody BatchParam<AppCredential> batchParam) {
if (batchParam.update(appCredentialService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appCredential:remove')")
@OperationLog
@Operation(summary = "批量删除应用密钥凭证")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (appCredentialService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -0,0 +1,133 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.app.service.AppEventService;
import com.gxwebsoft.app.entity.AppEvent;
import com.gxwebsoft.app.param.AppEventParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 应用操作动态控制器
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Slf4j
@Tag(name = "应用操作动态管理")
@RestController
@RequestMapping("/api/app/app-event")
public class AppEventController extends BaseController {
@Resource
private AppEventService appEventService;
@Operation(summary = "分页查询操作动态")
@GetMapping("/page")
public ApiResult<PageResult<AppEvent>> page(AppEventParam param) {
return success(appEventService.pageRel(param));
}
@Operation(summary = "查询全部操作动态")
@GetMapping()
public ApiResult<List<AppEvent>> list(AppEventParam param) {
return success(appEventService.listRel(param));
}
@Operation(summary = "根据id查询操作动态")
@GetMapping("/{id}")
public ApiResult<AppEvent> get(@PathVariable("id") Integer id) {
return success(appEventService.getByIdRel(id));
}
@Operation(summary = "获取应用最新一条动态(用于卡片展示)")
@GetMapping("/latest/{appId}")
public ApiResult<AppEvent> getLatest(@PathVariable("appId") Long appId) {
return success(appEventService.getLatestEvent(appId));
}
@PreAuthorize("hasAuthority('app:appEvent:save')")
@OperationLog
@Operation(summary = "手动记录操作动态")
@PostMapping()
public ApiResult<?> save(@RequestBody AppEvent appEvent) {
User loginUser = getLoginUser();
if (loginUser != null) {
appEvent.setUserId(loginUser.getUserId());
appEvent.setTenantId(loginUser.getTenantId());
if (appEvent.getOperatorId() == null) {
appEvent.setOperatorId(loginUser.getUserId().longValue());
}
if (appEvent.getOperator() == null) {
appEvent.setOperator(loginUser.getNickname());
}
}
if (appEventService.save(appEvent)) {
return success("记录成功");
}
return fail("记录失败");
}
@PreAuthorize("hasAuthority('app:appEvent:update')")
@OperationLog
@Operation(summary = "修改操作动态")
@PutMapping()
public ApiResult<?> update(@RequestBody AppEvent appEvent) {
if (appEventService.updateById(appEvent)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appEvent:remove')")
@OperationLog
@Operation(summary = "删除操作动态")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (appEventService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('app:appEvent:remove')")
@OperationLog
@Operation(summary = "清空应用所有动态记录")
@DeleteMapping("/clear/{appId}")
public ApiResult<?> clearByAppId(@PathVariable("appId") Long appId) {
// 只清空当前租户下的数据
AppEventParam param = new AppEventParam();
param.setAppId(appId);
List<AppEvent> list = appEventService.listRel(param);
if (list.isEmpty()) {
return success("暂无动态记录");
}
List<Long> ids = list.stream().map(AppEvent::getId).collect(java.util.stream.Collectors.toList());
if (appEventService.removeByIds(ids)) {
return success("已清空 " + ids.size() + " 条动态记录");
}
return fail("清空失败");
}
@PreAuthorize("hasAuthority('app:appEvent:remove')")
@OperationLog
@Operation(summary = "批量删除操作动态")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (appEventService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -0,0 +1,96 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.service.AppInviteService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 应用成员邀请 Controller
*/
@Tag(name = "应用成员邀请")
@RestController
@RequestMapping("/api/app/developer/invite")
@RequiredArgsConstructor
public class AppInviteController extends BaseController {
private final AppInviteService appInviteService;
@Operation(summary = "生成二维码邀请")
@PreAuthorize("hasAuthority('app:appUser:save')")
@PostMapping("/qrcode")
public ApiResult<Map<String, String>> generateQrCode(@RequestBody Map<String, Object> params) {
Integer appId = (Integer) params.get("appId");
String role = (String) params.get("role");
Integer currentUserId = getLoginUserId();
Map<String, String> result = appInviteService.generateQrCodeInvite(appId, role, currentUserId);
return success(result);
}
@Operation(summary = "生成链接邀请")
@PreAuthorize("hasAuthority('app:appUser:save')")
@PostMapping("/link")
public ApiResult<Map<String, String>> generateLink(@RequestBody Map<String, Object> params) {
Integer appId = (Integer) params.get("appId");
String role = (String) params.get("role");
Integer currentUserId = getLoginUserId();
Map<String, String> result = appInviteService.generateLinkInvite(appId, role, currentUserId);
return success(result);
}
@Operation(summary = "验证邀请")
@PostMapping("/verify")
public ApiResult<Map<String, Object>> verifyInvite(@RequestBody Map<String, Object> params) {
String token = (String) params.get("token");
Integer appId = (Integer) params.get("appId");
Map<String, Object> result = appInviteService.verifyInvite(token, appId);
return success(result);
}
@Operation(summary = "通过Token获取邀请信息小程序专用")
@GetMapping("/info")
public ApiResult<Map<String, Object>> getInviteInfo(@RequestParam String token) {
Map<String, Object> result = appInviteService.getInviteInfoByToken(token);
return success(result);
}
@Operation(summary = "接受邀请(支持小程序手机号自动注册)")
@PostMapping("/accept")
public ApiResult<?> acceptInvite(@RequestBody Map<String, Object> params) {
String token = (String) params.get("token");
Integer currentUserId = getLoginUserId();
// 已登录用户直接加入
if (currentUserId != null) {
Integer appId = (Integer) params.get("appId");
appInviteService.acceptInvite(token, appId, currentUserId);
return success("加入成功");
}
// 小程序手机号登录 + 加入(需要 code
String code = (String) params.get("code");
if (code != null) {
// TODO: 调用微信手机号登录接口,自动注册用户并加入
// 这里暂时返回提示
return fail("手机号登录功能开发中,请先在网页端登录");
}
return fail("请先登录");
}
@Operation(summary = "查询邀请状态(用于轮询检测是否被使用)")
@GetMapping("/status")
public ApiResult<Map<String, Object>> getInviteStatus(@RequestParam String token) {
Map<String, Object> result = appInviteService.getInviteStatus(token);
return success(result);
}
}

View File

@@ -0,0 +1,431 @@
package com.gxwebsoft.app.controller;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.gxwebsoft.app.entity.AppInviteToken;
import com.gxwebsoft.app.entity.AppProduct;
import com.gxwebsoft.app.entity.AppUser;
import com.gxwebsoft.app.entity.AppUserCache;
import com.gxwebsoft.app.mapper.AppInviteTokenMapper;
import com.gxwebsoft.app.mapper.AppProductMapper;
import com.gxwebsoft.app.mapper.AppUserCacheMapper;
import com.gxwebsoft.app.mapper.AppUserMapper;
import com.gxwebsoft.app.service.AppUserCacheService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 小程序邀请接口(处理 /api/_app 路径)
* 用于小程序扫码加入应用场景
*/
@Slf4j
@Tag(name = "小程序邀请")
@RestController
@RequestMapping("/api/_app/developer/invite")
@RequiredArgsConstructor
public class AppMpInviteController extends BaseController {
private final AppInviteTokenMapper appInviteTokenMapper;
private final AppProductMapper appProductMapper;
private final AppUserMapper appUserMapper;
private final AppUserCacheMapper appUserCacheMapper;
private final AppUserCacheService appUserCacheService;
@Value("${app.invite.base-url:https://websopy.websoft.top}")
private String baseUrl;
@Value("${spring.redis.host:127.0.0.1}")
private String redisHost;
@Value("${spring.redis.port:6379}")
private int redisPort;
@Value("${spring.redis.password:}")
private String redisPassword;
@Operation(summary = "通过Token获取邀请信息小程序专用")
@GetMapping("/info")
public ApiResult<Map<String, Object>> getInviteInfo(@RequestParam String token) {
try {
Map<String, Object> result = getInviteInfoByToken(token);
return success(result);
} catch (Exception e) {
log.error("获取邀请信息失败", e);
return fail("获取邀请信息失败");
}
}
@Operation(summary = "验证邀请PC端和小程序通用")
@PostMapping("/verify")
public ApiResult<Map<String, Object>> verifyInvite(@RequestBody Map<String, Object> params) {
try {
String token = (String) params.get("token");
Integer appId = (Integer) params.get("appId");
AppInviteToken inviteToken = appInviteTokenMapper.selectByToken(token);
if (inviteToken == null) {
return fail("邀请不存在或已失效");
}
if (appId != null && !inviteToken.getAppId().equals(appId)) {
return fail("邀请与应用不匹配");
}
if (inviteToken.getExpireTime().isBefore(LocalDateTime.now())) {
return fail("邀请已过期");
}
if (inviteToken.getStatus() != 0) {
return fail("邀请已被使用");
}
AppProduct app = appProductMapper.selectById(inviteToken.getAppId());
if (app == null) {
return fail("应用不存在");
}
AppUserCache inviter = appUserCacheService.getByUserId(inviteToken.getInviterId());
Map<String, Object> result = new HashMap<>();
result.put("appId", inviteToken.getAppId());
result.put("appName", app.getProductName());
result.put("appIcon", app.getIcon());
result.put("inviterName", inviter != null ? inviter.getNickname() : "未知");
result.put("inviterAvatar", inviter != null ? inviter.getAvatar() : null);
result.put("role", inviteToken.getRole());
result.put("expireTime", inviteToken.getExpireTime().toString());
return success(result);
} catch (Exception e) {
log.error("验证邀请失败", e);
return fail("验证邀请失败");
}
}
@Operation(summary = "接受邀请(小程序专用,支持已登录和未注册两种模式)")
@PostMapping("/accept")
@Transactional(rollbackFor = Exception.class)
public ApiResult<?> acceptInvite(@RequestBody Map<String, Object> params) {
String token = (String) params.get("token");
String code = (String) params.get("code");
if (StrUtil.isBlank(token)) {
return fail("邀请 token 不能为空");
}
try {
// 1. 验证邀请 token
AppInviteToken inviteToken = appInviteTokenMapper.selectByToken(token);
if (inviteToken == null) {
return fail("邀请不存在或已失效");
}
if (inviteToken.getExpireTime().isBefore(LocalDateTime.now())) {
return fail("邀请已过期");
}
if (inviteToken.getStatus() != 0) {
return fail("邀请已被使用");
}
Integer userId;
// 2. 判断是已登录用户还是未注册用户
if (StrUtil.isBlank(code)) {
// ===== 模式一:已登录用户(通过 Authorization 头识别)=====
log.info("接受邀请 - 已登录用户模式通过token识别");
userId = getLoginUserId();
if (userId == null) {
return fail("用户未登录,请先登录");
}
log.info("已登录用户加入应用: userId={}", userId);
} else {
// ===== 模式二:未注册用户(通过微信授权码获取手机号)=====
log.info("接受邀请 - 未注册用户模式(通过手机号授权码)");
// 2.1 通过微信手机号 code 获取手机号
String phone = getPhoneByCode(code);
if (StrUtil.isBlank(phone)) {
return fail("获取手机号失败,请重试");
}
// 2.2 查询用户,不存在则创建
userId = getOrCreateUserByPhone(phone);
if (userId == null) {
return fail("用户创建失败");
}
log.info("未注册用户加入应用: phone={}, userId={}", phone, userId);
}
// 4. 检查是否已经是成员
AppUser existUser = appUserMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AppUser>()
.eq(AppUser::getAppId, inviteToken.getAppId().longValue())
.eq(AppUser::getUserId, userId)
);
if (existUser != null && existUser.getInviteStatus() == 0) {
return fail("你已经是该应用的成员");
}
// 5. 添加为应用成员(直接加入,不走待确认流程)
AppUser appUser = new AppUser();
appUser.setAppId(inviteToken.getAppId().longValue());
appUser.setUserId(userId);
appUser.setRole(inviteToken.getRole() != null ? inviteToken.getRole() : "developer");
appUser.setInviteBy(inviteToken.getInviterId().longValue());
appUser.setInviteTime(LocalDateTime.now());
appUser.setStatus(0);
appUser.setInviteStatus(0); // 直接确认
appUser.setSortNumber(0);
// 从 AppProduct 获取 tenantId
AppProduct app = appProductMapper.selectById(inviteToken.getAppId());
appUser.setTenantId(app != null ? app.getTenantId() : null);
// 补充用户信息
AppUserCache userCache = appUserCacheService.getByUserId(userId);
if (userCache != null) {
appUser.setUsername(userCache.getUsername());
appUser.setNickname(userCache.getNickname());
appUser.setAvatar(userCache.getAvatar());
appUser.setPhone(userCache.getPhone());
}
appUserMapper.insert(appUser);
// 6. 标记邀请已使用
inviteToken.setStatus(1);
appInviteTokenMapper.updateById(inviteToken);
log.info("小程序扫码加入应用成功: token={}, userId={}, appId={}", token, userId, inviteToken.getAppId());
// 7. 返回应用信息(复用上面查询的 app 对象)
String appName = (app != null) ? app.getProductName() : "应用";
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "加入成功");
result.put("appName", appName);
return success(result);
} catch (Exception e) {
log.error("接受邀请失败", e);
return fail("加入失败,请重试");
}
}
/**
* 通过 token 获取邀请信息
*/
private Map<String, Object> getInviteInfoByToken(String token) throws Exception {
AppInviteToken inviteToken = appInviteTokenMapper.selectByToken(token);
if (inviteToken == null) {
throw new Exception("邀请不存在或已失效");
}
if (inviteToken.getExpireTime().isBefore(LocalDateTime.now())) {
throw new Exception("邀请已过期");
}
if (inviteToken.getStatus() != 0) {
throw new Exception("邀请已被使用");
}
AppProduct app = appProductMapper.selectById(inviteToken.getAppId());
if (app == null) {
throw new Exception("应用不存在");
}
AppUserCache inviter = appUserCacheService.getByUserId(inviteToken.getInviterId());
String roleName = getRoleName(inviteToken.getRole());
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("appId", inviteToken.getAppId());
result.put("appName", app.getProductName());
result.put("appLogo", app.getIcon());
result.put("inviterName", inviter != null ? inviter.getNickname() : "某位用户");
result.put("roleName", roleName);
result.put("role", inviteToken.getRole());
result.put("expireTime", inviteToken.getExpireTime().toString());
return result;
}
/**
* 获取角色名称
*/
private String getRoleName(String role) {
if (StrUtil.isBlank(role)) return "成员";
switch (role) {
case "owner": return "所有者";
case "admin": return "管理员";
case "developer": return "开发者";
default: return "成员";
}
}
/**
* 通过微信手机号 code 获取手机号
*/
private String getPhoneByCode(String code) {
try {
// 获取 access_token从小程序配置读取
String accessToken = getMiniprogramAccessToken();
if (StrUtil.isBlank(accessToken)) {
log.warn("获取小程序access_token失败");
return null;
}
String apiUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken;
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("code", code);
String response = HttpUtil.post(apiUrl, JSON.toJSONString(paramMap));
JSONObject json = JSON.parseObject(response);
if (json.containsKey("errcode") && json.getInteger("errcode") != 0) {
log.error("微信获取手机号失败: {}", json.getString("errmsg"));
return null;
}
// 解析手机号
if (json.containsKey("phone_info")) {
JSONObject phoneInfo = json.getJSONObject("phone_info");
return phoneInfo.getString("phoneNumber");
}
return null;
} catch (Exception e) {
log.error("获取手机号异常", e);
return null;
}
}
/**
* 获取小程序 AccessToken
*/
private String getMiniprogramAccessToken() {
try {
// 使用 WxMiniprogramUtil 获取 access_token
String accessToken = com.gxwebsoft.common.core.utils.WxMiniprogramUtil.getAccessToken();
if (StrUtil.isBlank(accessToken)) {
log.warn("通过 WxMiniprogramUtil 获取小程序 access_token 失败");
}
return accessToken;
} catch (Exception e) {
log.error("获取AccessToken失败", e);
return null;
}
}
/**
* 调用微信 API 获取 AccessToken
*/
private String fetchAccessToken(String appId, String appSecret) {
String apiUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid="
+ appId + "&secret=" + appSecret;
String response = HttpUtil.get(apiUrl);
JSONObject json = JSON.parseObject(response);
return json.getString("access_token");
}
/**
* 获取 Spring Bean
*/
private Object getSpringBean(Class<?> clazz) {
try {
Class<?> springContextUtilClass = Class.forName("com.gxwebsoft.common.core.config.SpringContextUtil");
Object bean = springContextUtilClass.getMethod("getBean", Class.class)
.invoke(null, clazz);
return bean;
} catch (Exception e) {
log.warn("获取Spring Bean失败: {}", e.getMessage());
return null;
}
}
/**
* 通过手机号查询或创建用户
* 优先从 app_user_cache 查询,不存在则通过 HTTP 调用主服务器 API 查询/创建(跨库到 gxwebsoft_core.sys_user
*/
private Integer getOrCreateUserByPhone(String phone) {
try {
// 1. 优先从 app_user_cache 查询用户
AppUserCache userCache = appUserCacheMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AppUserCache>()
.eq(AppUserCache::getPhone, phone)
);
if (userCache != null) {
log.info("从 app_user_cache 找到用户: phone={}, userId={}", phone, userCache.getUserId());
return userCache.getUserId();
}
log.info("app_user_cache 中未找到用户,通过 AppUserCacheService 刷新缓存: phone={}", phone);
// 2. 通过 AppUserCacheService 从主服务器刷新缓存
// AppUserCacheService 会调用 server API 获取用户信息并保存到本地缓存
AppUserCache refreshedCache = appUserCacheService.refreshUserCacheByPhone(phone);
if (refreshedCache != null) {
log.info("从主服务器刷新缓存成功: phone={}, userId={}", phone, refreshedCache.getUserId());
return refreshedCache.getUserId();
}
log.info("主服务器中未找到用户,需要创建新用户: phone={}", phone);
// 3. 用户不存在,通过 HTTP 调用主服务器 API 创建新用户
log.info("主服务器中未找到用户,创建新用户: phone={}", phone);
String createPath = "/system/user/";
Map<String, Object> userMap = new HashMap<>();
userMap.put("phone", phone);
userMap.put("username", "wx_" + phone);
userMap.put("nickname", "微信用户");
userMap.put("status", 0);
userMap.put("platform", "MP-WEIXIN");
userMap.put("tenantId", 1); // 默认租户
// 3. 用户不存在,调用 AppUserCacheService 创建新用户
log.info("调用 AppUserCacheService 创建新用户: phone={}", phone);
// 使用 AppUserCacheService 创建用户(内部会调用 server API
AppUserCache newUserCache = appUserCacheService.createUserByPhone(phone);
if (newUserCache != null && newUserCache.getUserId() != null) {
log.info("新用户创建成功: phone={}, userId={}", phone, newUserCache.getUserId());
return newUserCache.getUserId();
}
log.error("保存用户失败: phone={}", phone);
return null;
} catch (Exception e) {
log.error("创建用户失败: phone={}", phone, e);
return null;
}
}
/**
* 同步用户到缓存表
*/
private void syncUserToCache(Integer userId) {
try {
if (userId == null) return;
// 简单实现:更新缓存
Object userCacheService = getSpringBean(Class.forName("com.gxwebsoft.app.service.AppUserCacheService"));
if (userCacheService != null) {
userCacheService.getClass().getMethod("refreshUserCache", Integer.class).invoke(userCacheService, userId);
}
} catch (Exception e) {
log.warn("同步用户缓存失败: {}", e.getMessage());
}
}
}

View File

@@ -0,0 +1,101 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppNotification;
import com.gxwebsoft.app.param.AppNotificationParam;
import com.gxwebsoft.app.service.AppNotificationService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* 站内消息通知控制器
*
* @author 科技小王子
* @since 2026-04-03
*/
@Slf4j
@Tag(name = "站内消息通知")
@RestController
@RequestMapping("/api/app/notification")
public class AppNotificationController extends BaseController {
@Resource
private AppNotificationService appNotificationService;
// ─── 查询接口 ────────────────────────────────────────────────
@Operation(summary = "分页查询通知列表")
@GetMapping("/page")
public ApiResult<PageResult<AppNotification>> page(AppNotificationParam param) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
return success(appNotificationService.pageByUser(param, loginUser.getUserId()));
}
@Operation(summary = "查询最近通知(铃铛下拉)")
@GetMapping("/recent")
public ApiResult<List<AppNotification>> recent(
@RequestParam(required = false) String type,
@RequestParam(required = false, defaultValue = "20") Integer limit) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
return success(appNotificationService.listRecent(loginUser.getUserId(), type, limit));
}
@Operation(summary = "获取未读数量统计")
@GetMapping("/unread-count")
public ApiResult<Map<String, Object>> unreadCount() {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
return success(appNotificationService.getUnreadCount(loginUser.getUserId()));
}
// ─── 操作接口 ────────────────────────────────────────────────
@Operation(summary = "标记单条通知为已读")
@PutMapping("/read/{id}")
public ApiResult<?> markRead(@PathVariable Long id) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
appNotificationService.markRead(id, loginUser.getUserId());
return success("已标记为已读");
}
@Operation(summary = "标记全部已读")
@PutMapping("/read-all")
public ApiResult<?> markAllRead(@RequestBody(required = false) Map<String, String> body) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
String type = (body != null) ? body.get("type") : null;
appNotificationService.markAllRead(loginUser.getUserId(), type);
return success("已全部标记为已读");
}
@Operation(summary = "删除单条通知")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable Long id) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
appNotificationService.removeNotification(id, loginUser.getUserId());
return success("已删除");
}
@Operation(summary = "清空已读通知")
@DeleteMapping("/clear-read")
public ApiResult<?> clearRead(@RequestBody(required = false) Map<String, String> body) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
String type = (body != null) ? body.get("type") : null;
appNotificationService.clearRead(loginUser.getUserId(), type);
return success("已清空已读通知");
}
}

View File

@@ -0,0 +1,157 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppPermissionRequest;
import com.gxwebsoft.app.param.AppPermissionRequestParam;
import com.gxwebsoft.app.service.AppPermissionRequestService;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 权限申请控制器
*
* @author 科技小王子
* @since 2026-04-03
*/
@Slf4j
@Tag(name = "权限申请")
@RestController
@RequestMapping("/api/app/developer/permission-requests")
public class AppPermissionRequestController extends BaseController {
@Resource
private AppPermissionRequestService appPermissionRequestService;
@Operation(summary = "获取权限申请列表")
@GetMapping("/page")
public ApiResult<PageResult<AppPermissionRequest>> page(AppPermissionRequestParam param) {
User loginUser = getLoginUser();
if (loginUser != null) {
param.setUserId(loginUser.getUserId());
}
return success(appPermissionRequestService.pageRel(param));
}
@Operation(summary = "查询全部权限申请")
@GetMapping
public ApiResult<List<AppPermissionRequest>> list(AppPermissionRequestParam param) {
User loginUser = getLoginUser();
if (loginUser != null) {
param.setUserId(loginUser.getUserId());
}
return success(appPermissionRequestService.listRel(param));
}
@Operation(summary = "根据id查询权限申请")
@GetMapping("/{id}")
public ApiResult<AppPermissionRequest> get(@PathVariable("id") Long id) {
return success(appPermissionRequestService.getByIdRel(id));
}
@Operation(summary = "获取权限申请统计")
@GetMapping("/stats")
public ApiResult<Map<String, Object>> stats() {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录", null);
}
return success(appPermissionRequestService.getPermissionRequestStats(loginUser.getUserId()));
}
@Operation(summary = "获取可申请的仓库列表")
@GetMapping("/available-repos")
public ApiResult<List<Map<String, Object>>> availableRepos() {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录", null);
}
return success(appPermissionRequestService.getAvailableRepositories(loginUser.getUserId()));
}
@OperationLog
@Operation(summary = "提交权限申请")
@PostMapping
public ApiResult<AppPermissionRequest> save(@RequestBody Map<String, String> params) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录", null);
}
String repo = params.get("repo");
String reason = params.get("reason");
String gitUsername = params.get("gitUsername");
if (repo == null || repo.trim().isEmpty()) {
return fail("请选择申请仓库", null);
}
if (reason == null || reason.trim().isEmpty()) {
return fail("请填写申请理由", null);
}
try {
// 从 repo 中提取仓库名称(假设格式为 owner/repo-name
String repoName = repo;
if (repo.contains("/")) {
String[] parts = repo.split("/");
repoName = parts[parts.length - 1];
}
AppPermissionRequest request = appPermissionRequestService.createPermissionRequest(
loginUser.getUserId(), gitUsername, repo, repoName, reason.trim(), loginUser.getTenantId()
);
return success("申请提交成功,请等待审核", request);
} catch (RuntimeException e) {
return fail(e.getMessage(), null);
}
}
@OperationLog
@Operation(summary = "审核权限申请-通过")
@PutMapping("/{id}/approve")
public ApiResult<?> approve(@PathVariable("id") Long id, @RequestBody(required = false) Map<String, String> params) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
try {
String note = params != null ? params.get("note") : null;
boolean result = appPermissionRequestService.approveRequest(id, loginUser.getUserId(), loginUser.getRealName(), note);
return result ? success("审核通过") : fail("审核失败");
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
@OperationLog
@Operation(summary = "审核权限申请-拒绝")
@PutMapping("/{id}/reject")
public ApiResult<?> reject(@PathVariable("id") Long id, @RequestBody Map<String, String> params) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
String reason = params != null ? params.get("reason") : null;
if (reason == null || reason.trim().isEmpty()) {
return fail("请填写拒绝原因");
}
try {
boolean result = appPermissionRequestService.rejectRequest(id, loginUser.getUserId(), loginUser.getRealName(), reason.trim());
return result ? success("已拒绝该申请") : fail("拒绝失败");
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
}

View File

@@ -0,0 +1,259 @@
package com.gxwebsoft.app.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gxwebsoft.app.entity.AppProduct;
import com.gxwebsoft.app.param.AppProductParam;
import com.gxwebsoft.app.service.AppProductService;
import com.gxwebsoft.app.service.AppUserService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;
/**
* 应用产品 前端控制器
*
* @author 科技小王子
*/
@RestController
@RequestMapping("/api/app/product")
@RequiredArgsConstructor
@Tag(name = "应用产品管理", description = "应用产品管理接口")
public class AppProductController extends BaseController {
private final AppProductService appProductService;
private final AppUserService appUserService;
@GetMapping("/page")
@Operation(summary = "分页查询应用列表")
public ApiResult<PageResult<AppProduct>> page(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long current,
@Parameter(description = "每页条数") @RequestParam(defaultValue = "10") Long size,
@Parameter(description = "应用名称") @RequestParam(required = false) String productName,
@Parameter(description = "应用标识") @RequestParam(required = false) String productCode,
@Parameter(description = "应用类型") @RequestParam(required = false) Integer appType,
@Parameter(description = "分类ID") @RequestParam(required = false) Integer categoryId,
@Parameter(description = "发布状态") @RequestParam(required = false) String publishStatus,
@Parameter(description = "状态") @RequestParam(required = false) Integer status,
@Parameter(description = "用户ID可选不传则自动用当前登录用户") @RequestParam(required = false) Integer userId) {
// 如果没传 userId自动用当前登录用户
if (userId == null) {
userId = getLoginUserId();
}
AppProduct product = new AppProduct();
product.setProductName(productName);
product.setProductCode(productCode);
product.setAppType(appType);
product.setCategoryId(categoryId);
product.setPublishStatus(publishStatus);
product.setStatus(status);
IPage<AppProduct> page = appProductService.selectPageList(new Page<>(current, size), product, userId);
return success(page);
}
@GetMapping("/list")
@Operation(summary = "获取我的应用列表")
public ApiResult<List<AppProduct>> list() {
Integer userId = getLoginUserId();
return success(appProductService.getByUserId(userId));
}
@GetMapping("/my/page")
@Operation(summary = "分页查询我的应用(创建者)")
public ApiResult<PageResult<AppProduct>> myApps(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long current,
@Parameter(description = "每页条数") @RequestParam(defaultValue = "10") Long size) {
Integer userId = getLoginUserId();
Page<AppProduct> page = new Page<>(current, size);
return success(appProductService.getMyApps(page, userId));
}
@GetMapping("/joined/page")
@Operation(summary = "分页查询我参与的应用")
public ApiResult<PageResult<AppProduct>> joinedApps(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long current,
@Parameter(description = "每页条数") @RequestParam(defaultValue = "10") Long size) {
Integer userId = getLoginUserId();
Page<AppProduct> page = new Page<>(current, size);
return success(appProductService.getJoinedApps(page, userId));
}
@GetMapping("/detail/{id}")
@Operation(summary = "获取应用详情")
public ApiResult<AppProduct> detail(
@Parameter(description = "应用ID") @PathVariable Integer id) {
// 记录用户访问(更新最近访问时间)
Integer userId = getLoginUserId();
if (userId != null) {
appUserService.recordVisit(id.longValue(), userId);
}
return success(appProductService.getDetail(id));
}
@PostMapping("/visit/{id}")
@Operation(summary = "记录应用访问")
public ApiResult<?> visit(
@Parameter(description = "应用ID") @PathVariable Integer id) {
Integer userId = getLoginUserId();
if (userId != null) {
appUserService.recordVisit(id.longValue(), userId);
}
return success();
}
@GetMapping("/info/{code}")
@Operation(summary = "根据标识获取应用")
public ApiResult<AppProduct> info(
@Parameter(description = "应用标识") @PathVariable String code) {
return success(appProductService.getByCode(code));
}
@PostMapping("/create")
@Operation(summary = "创建应用")
public ApiResult<?> create(@Valid @RequestBody AppProduct product) {
boolean success = appProductService.create(product);
return success ? success("创建成功") : fail("创建失败");
}
@PutMapping("/update")
@Operation(summary = "更新应用")
public ApiResult<?> update(@Valid @RequestBody AppProduct product) {
boolean success = appProductService.update(product);
return success ? success("更新成功") : fail("更新失败");
}
@DeleteMapping("/delete/{id}")
@Operation(summary = "删除应用")
public ApiResult<?> delete(
@Parameter(description = "应用ID") @PathVariable Integer id) {
boolean success = appProductService.delete(id);
return success ? success("删除成功") : fail("删除失败");
}
@PostMapping("/submit/{id}")
@Operation(summary = "提交审核")
public ApiResult<?> submit(
@Parameter(description = "应用ID") @PathVariable Integer id) {
boolean success = appProductService.submitReview(id);
return success ? success("提交成功") : fail("提交失败");
}
@PostMapping("/approve/{id}")
@Operation(summary = "审核通过")
public ApiResult<?> approve(
@Parameter(description = "应用ID") @PathVariable Integer id) {
boolean success = appProductService.approve(id);
return success ? success("审核通过") : fail("审核失败");
}
@PostMapping("/reject/{id}")
@Operation(summary = "审核拒绝")
public ApiResult<?> reject(
@Parameter(description = "应用ID") @PathVariable Integer id,
@RequestParam String reason) {
boolean success = appProductService.reject(id, reason);
return success ? success("操作成功") : fail("操作失败");
}
@PostMapping("/publish/{id}")
@Operation(summary = "上架应用")
public ApiResult<?> publish(
@Parameter(description = "应用ID") @PathVariable Integer id) {
boolean success = appProductService.publish(id);
return success ? success("上架成功") : fail("上架失败");
}
@PostMapping("/unpublish/{id}")
@Operation(summary = "下架应用")
public ApiResult<?> unpublish(
@Parameter(description = "应用ID") @PathVariable Integer id) {
boolean success = appProductService.unpublish(id);
return success ? success("下架成功") : fail("下架失败");
}
@PostMapping("/regenerateSecret/{id}")
@Operation(summary = "重新生成密钥")
public ApiResult<String> regenerateSecret(
@Parameter(description = "应用ID") @PathVariable Integer id) {
String newSecret = appProductService.regenerateSecret(id);
return success(newSecret,null);
}
@GetMapping("/market/page")
@Operation(summary = "应用市场分页")
public ApiResult<PageResult<AppProduct>> marketPage(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long current,
@Parameter(description = "每页条数") @RequestParam(defaultValue = "10") Long size,
@Parameter(description = "应用类型") @RequestParam(required = false) Integer appType,
@Parameter(description = "关键词") @RequestParam(required = false) String keyword) {
IPage<AppProduct> page = appProductService.getMarketList(new Page<>(current, size), appType, keyword);
return success(page);
}
@PostMapping("/view/{id}")
@Operation(summary = "浏览应用")
public ApiResult<?> view(
@Parameter(description = "应用ID") @PathVariable Integer id) {
appProductService.incrementClicks(id);
return success();
}
@PostMapping("/install/{id}")
@Operation(summary = "安装应用")
public ApiResult<?> install(
@Parameter(description = "应用ID") @PathVariable Integer id) {
appProductService.incrementInstalls(id);
return success();
}
@PostMapping("/download/{id}")
@Operation(summary = "下载应用")
public ApiResult<?> download(
@Parameter(description = "应用ID") @PathVariable Integer id) {
appProductService.incrementDownloads(id);
return success();
}
@PostMapping("/like/{id}")
@Operation(summary = "点赞应用")
public ApiResult<?> like(
@Parameter(description = "应用ID") @PathVariable Integer id) {
appProductService.incrementLikes(id);
return success();
}
@PostMapping("/user-stats")
@Operation(summary = "按用户ID列表批量统计应用数量")
public ApiResult<List<Map<String, Object>>> userStats(
@RequestBody List<Integer> userIds) {
return success(appProductService.getStatsByUserIds(userIds));
}
/**
* 获取当前用户可访问的应用列表(带角色信息)
* 用于权限过滤返回用户创建的应用owner和被邀请加入的应用成员角色
* 每个应用包含 myRole 字段,标识用户在该应用中的角色
*/
@GetMapping("/accessible")
@Operation(summary = "获取可访问的应用列表(带角色信息)")
public ApiResult<List<AppProduct>> accessibleApps() {
Integer userId = getLoginUserId();
if (userId == null) {
return success(List.of());
}
return success(appProductService.getAccessibleApps(userId));
}
}

View File

@@ -0,0 +1,368 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppResource;
import com.gxwebsoft.app.entity.ResourceAccessLevel;
import com.gxwebsoft.app.param.AppResourceParam;
import com.gxwebsoft.app.service.AppResourceService;
import com.gxwebsoft.app.service.AppCloudCredentialService;
import com.gxwebsoft.app.service.DatabaseOperatorFactory;
import com.gxwebsoft.app.service.DatabaseOperatorService;
import com.gxwebsoft.app.service.OnePanelService;
import com.gxwebsoft.app.service.SshService;
import com.gxwebsoft.app.service.cloud.CloudStorageProvider;
import com.gxwebsoft.app.service.cloud.CloudStorageProviderFactory;
import com.gxwebsoft.app.service.impl.AppResourceServiceImpl;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.utils.DbPasswordUtil;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 开发者资源管理控制器(服务器/数据库/云存储/域名/SSL
*
* @author 科技小王子
* @since 2026-03-31
*/
@Slf4j
@Tag(name = "开发者资源管理")
@RestController
@RequestMapping("/api/app/developer-resource")
public class AppResourceController extends BaseController {
@Resource
private AppResourceService appResourceService;
@Resource
private AppCloudCredentialService cloudCredentialService;
@Resource
private DatabaseOperatorFactory databaseOperatorFactory;
@Resource
private SshService sshService;
@Resource
private OnePanelService onePanelService;
@Resource
private RedisUtil redisUtil;
// ─── 查询接口 ─────────────────────────────────────────────────
@Operation(summary = "分页查询资源列表(支持协作权限)")
@GetMapping("/page")
public ApiResult<PageResult<AppResource>> page(AppResourceParam param) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
// **协作权限改进**:不再限制只能查自己的资源,改为查询用户有权访问的资源
// 通过修改SQL查询条件改为owner_user_id = 当前用户 OR app_id IN (用户有权限的应用)
param.setUserId(loginUser.getUserId());
param.setTenantId(loginUser.getTenantId());
// 调用新方法传入当前用户ID以便计算 accessLevel 和屏蔽敏感字段
return success(appResourceService.pageRel(param, loginUser.getUserId()));
}
@Operation(summary = "查询资源列表(不分页,支持协作权限)")
@GetMapping
public ApiResult<List<AppResource>> list(AppResourceParam param) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
param.setUserId(loginUser.getUserId());
param.setTenantId(loginUser.getTenantId());
// 调用新方法传入当前用户ID
return success(appResourceService.listRel(param, loginUser.getUserId()));
}
@Operation(summary = "获取资源详情(支持协作权限)")
@GetMapping("/{resourceId}")
public ApiResult<AppResource> get(@PathVariable Long resourceId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
AppResource resource = appResourceService.getByIdRel(resourceId, loginUser.getUserId());
if (resource == null) return fail("资源不存在", null);
// **协作权限改进**:不再直接禁止访问,由 Service 计算 accessLevel0 表示无权限
if (resource.getAccessLevel() == null || resource.getAccessLevel() == 0) {
return fail("无权访问此资源", null);
}
return success(resource);
}
@Operation(summary = "统计各类型资源数量")
@GetMapping("/stats")
public ApiResult<Map<String, Long>> stats() {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
return success(appResourceService.countByType(loginUser.getUserId(), loginUser.getTenantId()));
}
// ─── 新增/修改接口 ────────────────────────────────────────────
@OperationLog
@Operation(summary = "新增资源")
@PostMapping
public ApiResult<AppResource> save(@RequestBody AppResource resource) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
if (resource.getResourceType() == null || resource.getResourceType().isEmpty()) {
return fail("资源类型不能为空", null);
}
if (resource.getName() == null || resource.getName().isEmpty()) {
return fail("资源名称不能为空", null);
}
resource.setTenantId(loginUser.getTenantId());
try {
AppResource result = appResourceService.addResource(resource, loginUser.getUserId());
return success("添加成功", result);
} catch (Exception e) {
return fail(e.getMessage(), null);
}
}
@OperationLog
@Operation(summary = "修改资源(只有 Owner 可操作)")
@PutMapping
public ApiResult<AppResource> update(@RequestBody AppResource resource) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
try {
AppResource result = appResourceService.updateResource(resource, loginUser.getUserId());
return success("修改成功", result);
} catch (Exception e) {
return fail(e.getMessage(), null);
}
}
// ─── 删除接口 ─────────────────────────────────────────────────
@OperationLog
@Operation(summary = "删除资源(逻辑删除,需要短信验证码)")
@DeleteMapping("/{resourceId}")
public ApiResult<?> remove(@PathVariable Long resourceId, @RequestBody Map<String, String> body) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
// 验证短信验证码
String code = body.get("code");
if (code == null || code.isEmpty()) {
return fail("请输入短信验证码");
}
// 验证验证码key 为 code:手机号)
String phone = loginUser.getPhone();
if (phone == null || phone.isEmpty()) {
return fail("请先绑定手机号");
}
String cachedCode = redisUtil.get("code:" + phone);
if (cachedCode == null || !cachedCode.equals(code)) {
return fail("验证码错误或已过期");
}
// 验证通过,删除验证码
redisUtil.delete("code:" + phone);
try {
appResourceService.removeResource(resourceId, loginUser.getUserId());
return success("删除成功");
} catch (Exception e) {
return fail(e.getMessage());
}
}
@OperationLog
@Operation(summary = "批量删除资源")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Long> ids) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
try {
for (Long id : ids) {
appResourceService.removeResource(id, loginUser.getUserId());
}
return success("批量删除成功");
} catch (Exception e) {
return fail(e.getMessage());
}
}
// ─── 数据库操作接口 ──────────────────────────────────────────
@Operation(summary = "测试服务器数据库连接")
@PostMapping("/test-connection")
public ApiResult<DatabaseOperatorService.DatabaseOperationResult> testConnection(@RequestBody Map<String, Object> params) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
String host = (String) params.get("host");
String dbType = (String) params.get("dbType");
Integer port = params.get("port") != null ? ((Number) params.get("port")).intValue() : null;
String username = (String) params.get("username");
String password = (String) params.get("password");
String dbName = (String) params.get("dbName");
if (host == null || host.isEmpty()) return fail("服务器地址不能为空", null);
if (username == null || username.isEmpty()) return fail("用户名不能为空", null);
// 根据数据库类型获取默认端口和对应操作服务
if (dbType == null || dbType.isEmpty()) {
dbType = "MySQL";
}
if (port == null) {
port = "PostgreSQL".equals(dbType) ? 5432 : 3306;
}
DatabaseOperatorService operator = databaseOperatorFactory.getOperator(dbType);
DatabaseOperatorService.DatabaseOperationResult result =
operator.testConnection(host, port, username, password, dbName);
return success(result);
}
@Operation(summary = "测试服务器 SSH 连接")
@PostMapping("/test-ssh")
public ApiResult<SshService.ConnectionResult> testSshConnection(@RequestBody Map<String, Object> params) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
String host = (String) params.get("host");
Integer port = params.get("port") != null ? ((Number) params.get("port")).intValue() : 22;
String username = (String) params.get("username");
String password = (String) params.get("password");
SshService.ConnectionResult result = sshService.testConnection(host, port, username, password);
return success(result);
}
@Operation(summary = "获取1Panel服务器状态")
@GetMapping("/server-status/{resourceId}")
public ApiResult<OnePanelService.ServerStatus> getServerStatus(@PathVariable Long resourceId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
AppResource resource = appResourceService.getByIdRel(resourceId, loginUser.getUserId());
if (resource == null) return fail("资源不存在", null);
// 必须是服务器类型资源
if (!"server".equals(resource.getResourceType())) {
return fail("只有服务器资源才能获取状态", null);
}
// 必须有 panelPort 配置
if (resource.getPanelPort() == null) {
return fail("该服务器未配置1Panel端口", null);
}
// 必须有连接权限owner/admin/developer才能获取状态
int accessLevel = resource.getAccessLevel() != null ? resource.getAccessLevel() : 0;
if (accessLevel < ResourceAccessLevel.VIEW_CONNECTION.getValue()) {
return fail("没有权限获取服务器状态", null);
}
// 调用1Panel API获取状态
OnePanelService.ServerStatus status = onePanelService.getServerStatus(
resource.getIp(),
resource.getPanelPort(),
resource.getPanelPath(),
resource.getPanelUsername(),
resource.getPanelPassword()
);
return success(status);
}
@Operation(summary = "重试创建数据库(只有 Owner 可操作)")
@PostMapping("/retry-create-database/{resourceId}")
public ApiResult<?> retryCreateDatabase(@PathVariable Long resourceId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
AppResource resource = appResourceService.getByIdRel(resourceId, loginUser.getUserId());
if (resource == null) return fail("资源不存在");
if (!loginUser.getUserId().equals(resource.getOwnerUserId())) return fail("只有资源创建者才能操作");
if (!"failed".equals(resource.getStatus()) && !"pending".equals(resource.getStatus())) {
return fail("只有创建失败或创建中的资源才能重试");
}
try {
// 获取 ServiceImpl 实例调用异步方法
AppResourceServiceImpl serviceImpl = (AppResourceServiceImpl) appResourceService;
serviceImpl.asyncCreateDatabase(resourceId);
return success("已开始重新创建,请稍后查看状态");
} catch (Exception e) {
return fail(e.getMessage());
}
}
@Operation(summary = "重置数据库密码(只有 Owner 可操作)")
@PostMapping("/reset-password/{resourceId}")
public ApiResult<Map<String, String>> resetPassword(@PathVariable Long resourceId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
try {
String newPassword = appResourceService.resetDatabasePassword(resourceId, loginUser.getUserId());
Map<String, String> result = new HashMap<>();
result.put("password", newPassword);
return success(result);
} catch (Exception e) {
return fail(e.getMessage(), null);
}
}
@Operation(summary = "刷新存储桶信息(只有 Owner 可操作)")
@PostMapping("/refresh-storage/{resourceId}")
public ApiResult<Map<String, Object>> refreshStorage(@PathVariable Long resourceId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
AppResource resource = appResourceService.getByIdRel(resourceId, loginUser.getUserId());
if (resource == null) return fail("资源不存在",null);
if (!"storage".equals(resource.getResourceType())) {
return fail("只有存储桶资源才能刷新",null);
}
try {
// 获取云账号凭证
Map<String, String> credentials;
if (resource.getCredentialId() != null) {
credentials = cloudCredentialService.getCredentialsByCredentialId(resource.getCredentialId());
} else {
credentials = cloudCredentialService.getCredentials(resource.getProvider(), loginUser.getUserId());
}
if (credentials == null || credentials.isEmpty()) {
return fail("请先配置云账号凭证",null);
}
// 调用云厂商 API 获取存储桶信息
CloudStorageProvider cloudProvider = CloudStorageProviderFactory.getProvider(resource.getProvider());
Map<String, Object> bucketInfo = cloudProvider.getBucketInfo(resource, credentials);
// 更新到数据库
AppResource update = new AppResource();
update.setResourceId(resourceId);
if (bucketInfo.get("usedBytes") != null) {
update.setUsedBytes(((Number) bucketInfo.get("usedBytes")).longValue());
}
if (bucketInfo.get("objectCount") != null) {
update.setUsedCount(((Number) bucketInfo.get("objectCount")).intValue());
}
appResourceService.updateById(update);
return success(bucketInfo);
} catch (Exception e) {
log.error("刷新存储桶信息失败: resourceId={}, error={}", resourceId, e.getMessage(), e);
return fail("刷新失败: " + e.getMessage(), null);
}
}
}

View File

@@ -0,0 +1,160 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.app.entity.AppSetting;
import com.gxwebsoft.app.param.AppSettingParam;
import com.gxwebsoft.app.service.AppSettingService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* 平台设置表 Controller
*/
@Slf4j
@Tag(name = "平台设置管理")
@RestController
@RequestMapping("/api/app/setting")
public class AppSettingController extends BaseController {
@Resource
private AppSettingService appSettingService;
/**
* 分页查询设置
*/
@Operation(summary = "分页查询设置")
@GetMapping("/page")
public ApiResult<PageResult<AppSetting>> page(AppSettingParam param) {
return success(new PageResult<>(appSettingService.page(param)));
}
/**
* 获取设置列表
*/
@Operation(summary = "获取设置列表")
@GetMapping()
public ApiResult<List<AppSetting>> list(AppSettingParam param) {
return success(appSettingService.list(param));
}
/**
* 根据分类获取设置
*/
@Operation(summary = "根据分类获取设置")
@GetMapping("/category/{category}")
public ApiResult<List<AppSetting>> getByCategory(@PathVariable String category) {
return success(appSettingService.getByCategory(category));
}
/**
* 获取分类设置值Map格式
*/
@Operation(summary = "获取分类设置值")
@GetMapping("/category/{category}/values")
public ApiResult<Map<String, String>> getCategoryValues(@PathVariable String category) {
return success(appSettingService.getCategoryValues(category));
}
/**
* 根据key获取单个设置值
*/
@Operation(summary = "根据key获取设置值")
@GetMapping("/key/{key}")
public ApiResult<String> getValue(@PathVariable String key) {
return success("获取成功", appSettingService.getValue(key));
}
/**
* 根据key获取完整设置
*/
@Operation(summary = "根据key获取完整设置")
@GetMapping("/info/{key}")
public ApiResult<AppSetting> getByKey(@PathVariable String key) {
return success(appSettingService.getByKey(key));
}
/**
* 保存或更新设置
*/
@Operation(summary = "保存或更新设置")
@PostMapping()
public ApiResult<?> save(@RequestBody AppSetting setting) {
appSettingService.saveOrUpdateSetting(setting);
return success("保存成功");
}
/**
* 更新设置
*/
@Operation(summary = "更新设置")
@PutMapping()
public ApiResult<?> update(@RequestBody AppSetting setting) {
appSettingService.saveOrUpdateSetting(setting);
return success("更新成功");
}
/**
* 批量保存分类设置
*/
@Operation(summary = "批量保存分类设置")
@PostMapping("/batch/{category}")
public ApiResult<?> batchSave(@PathVariable String category, @RequestBody Map<String, Object> values) {
// 处理值类型转换
Map<String, Object> valuesMap = new java.util.HashMap<>(values.size());
for (Map.Entry<String, Object> entry : values.entrySet()) {
Object val = entry.getValue();
if (val == null) {
// 跳过 null 值
continue;
} else if (val instanceof Boolean || val instanceof Number) {
// 保持原始类型Boolean, Integer, Long, Double 等)
valuesMap.put(entry.getKey(), val);
} else if (val instanceof String) {
// 尝试将字符串转换为正确的类型
String strVal = (String) val;
if ("true".equalsIgnoreCase(strVal)) {
valuesMap.put(entry.getKey(), Boolean.TRUE);
} else if ("false".equalsIgnoreCase(strVal)) {
valuesMap.put(entry.getKey(), Boolean.FALSE);
} else {
// 尝试转换为数字
try {
if (strVal.contains(".")) {
valuesMap.put(entry.getKey(), Double.parseDouble(strVal));
} else {
valuesMap.put(entry.getKey(), Long.parseLong(strVal));
}
} catch (NumberFormatException e) {
// 保持字符串
valuesMap.put(entry.getKey(), strVal);
}
}
} else if (val instanceof java.util.List) {
// List 类型保持不变Jackson 已经正确处理了数组)
valuesMap.put(entry.getKey(), val);
} else {
valuesMap.put(entry.getKey(), val);
}
}
appSettingService.batchSave(category, valuesMap);
return success("保存成功");
}
/**
* 删除设置
*/
@Operation(summary = "删除设置")
@DeleteMapping("/{settingId}")
public ApiResult<?> delete(@PathVariable Integer settingId) {
appSettingService.removeById(settingId);
return success("删除成功");
}
}

View File

@@ -0,0 +1,593 @@
package com.gxwebsoft.app.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gxwebsoft.app.config.AppPayProperties;
import com.gxwebsoft.app.entity.AppProduct;
import com.gxwebsoft.app.entity.AppSubscription;
import com.gxwebsoft.app.mapper.AppSubscriptionMapper;
import com.gxwebsoft.app.mapper.AppProductMapper;
import com.gxwebsoft.app.mapper.SysUserCrossDbMapper;
import com.gxwebsoft.app.utils.WxNativePayUtil;
import com.gxwebsoft.common.core.constants.BalanceConstants;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.system.entity.User;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.payments.nativepay.model.Amount;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
/**
* 应用订阅控制器
* 路径前缀:/api/app/subscription
*/
@Slf4j
@RestController
@RequestMapping("/api/app/subscription")
@RequiredArgsConstructor
@Tag(name = "应用订阅管理")
public class AppSubscriptionController extends BaseController {
private final AppSubscriptionMapper subscriptionMapper;
private final AppProductMapper productMapper;
private final WxNativePayUtil wxNativePayUtil;
private final AppPayProperties appPayProperties;
private final SysUserCrossDbMapper sysUserCrossDbMapper;
@javax.annotation.Resource
private RedisUtil redisUtil;
@Value("${spring.profiles.active:dev}")
private String active;
// ============================================================
// 订阅操作
// ============================================================
/**
* 创建订阅
* 免费应用:直接激活
* 付费应用:创建待支付记录
*/
@PostMapping("/subscribe")
@Operation(summary = "创建订阅")
public Object subscribe(@RequestBody Map<String, Object> params) {
Object productIdObj = params.get("productId");
if (productIdObj == null) {
return fail("productId 不能为空");
}
String productIdStr = productIdObj.toString();
if ("NaN".equals(productIdStr) || "null".equals(productIdStr) || "undefined".equals(productIdStr)) {
return fail("无效的应用ID请刷新页面后重试");
}
Integer productId;
try {
productId = Integer.valueOf(productIdStr);
} catch (NumberFormatException e) {
return fail("应用ID格式错误" + productIdStr);
}
String subscriptionPeriod = (String) params.getOrDefault("subscriptionPeriod", "month");
Integer userId = getLoginUserId();
if (userId == null) {
return fail("请先登录");
}
// 1. 查询产品信息
AppProduct product = productMapper.selectById(productId);
if (product == null || !"published".equals(product.getPublishStatus())) {
return fail("应用不存在或未上架");
}
// 2. 检查是否已有 active 订阅(幂等)
LambdaQueryWrapper<AppSubscription> existQuery = new LambdaQueryWrapper<>();
existQuery.eq(AppSubscription::getUserId, userId)
.eq(AppSubscription::getProductId, productId)
.eq(AppSubscription::getStatus, "active");
if (subscriptionMapper.selectCount(existQuery) > 0) {
return fail("您已订阅该应用,无需重复购买");
}
// 3. 计算价格
// app_product.price 单位为分BigDecimal转为元存到 app_subscription.pay_price
BigDecimal priceInYuan = BigDecimal.ZERO;
String priceType = product.getPriceType() != null ? product.getPriceType() : "free";
if (!"free".equals(priceType) && product.getPrice() != null) {
// price 字段单位是分,除以 100 转为元
priceInYuan = product.getPrice().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
if ("subscription".equals(priceType) && "year".equals(subscriptionPeriod)) {
// 年付按10个月价格
priceInYuan = priceInYuan.multiply(BigDecimal.TEN);
}
}
// 4. 创建订阅记录
AppSubscription sub = new AppSubscription();
sub.setSubscriptionNo(generateSubscriptionNo());
sub.setUserId(userId);
sub.setProductId(productId);
sub.setTenantId(product.getTenantId());
sub.setStatus("pending");
sub.setPriceType(priceType);
sub.setOriginalPrice(priceInYuan);
sub.setPayPrice(priceInYuan);
sub.setPayStatus(0);
sub.setSubscriptionPeriod("subscription".equals(priceType) ? subscriptionPeriod : null);
// 5. 免费应用直接激活
boolean isFree = "free".equals(priceType) || priceInYuan.compareTo(BigDecimal.ZERO) == 0;
if (isFree) {
sub.setStatus("active");
sub.setPayStatus(1);
sub.setPayType(12); // 免费标记
sub.setPayTime(LocalDateTime.now());
sub.setStartTime(LocalDateTime.now());
if ("subscription".equals(priceType)) {
int months = "year".equals(subscriptionPeriod) ? 12 : 1;
sub.setExpireTime(LocalDateTime.now().plusMonths(months));
}
// 更新产品安装量
product.setInstalls((product.getInstalls() != null ? product.getInstalls() : 0) + 1);
productMapper.updateById(product);
}
subscriptionMapper.insert(sub);
// 6. 返回结果
Map<String, Object> result = new HashMap<>();
result.put("subscriptionId", sub.getId());
result.put("subscriptionNo", sub.getSubscriptionNo());
result.put("status", sub.getStatus());
result.put("message", isFree ? "订阅成功" : "请完成支付");
if (!isFree) {
result.put("payPrice", sub.getPayPrice());
// TODO: 对接现有支付系统,创建支付订单并返回 orderNo
// result.put("orderNo", payService.createOrder(sub));
}
return success(result);
}
/**
* 获取当前用户余额
*/
@GetMapping("/balance")
@Operation(summary = "获取用户余额")
public Object getBalance() {
Integer userId = getLoginUserId();
if (userId == null) {
return fail("请先登录");
}
// 跨库查询 gxwebsoft_core.sys_user
User freshUser = sysUserCrossDbMapper.selectByUserId(userId);
BigDecimal balance = freshUser != null && freshUser.getBalance() != null
? freshUser.getBalance() : BigDecimal.ZERO;
Map<String, Object> result = new HashMap<>();
result.put("balance", balance);
return success(result);
}
/**
* 余额支付
* POST /api/app/subscription/pay/{id}?method=balance
*/
@PostMapping("/pay/{id}")
@Operation(summary = "发起支付")
public Object pay(@PathVariable Long id, @RequestParam(defaultValue = "wechat") String method) {
AppSubscription sub = subscriptionMapper.selectById(id);
if (sub == null) {
return fail("订阅不存在");
}
Integer userId = getLoginUserId();
if (userId == null || !userId.equals(sub.getUserId())) {
return fail("无权操作");
}
if ("active".equals(sub.getStatus())) {
return fail("该订阅已激活");
}
// 免费应用走激活流程
if (sub.getPayPrice() == null || sub.getPayPrice().compareTo(BigDecimal.ZERO) == 0) {
return fail("该应用为免费应用,无需支付");
}
// 余额支付
if ("balance".equals(method)) {
return handleBalancePay(sub);
}
// 微信 Native 支付
return handleWechatPay(sub);
}
/**
* 余额支付处理
*/
private Object handleBalancePay(AppSubscription sub) {
Integer userId = getLoginUserId();
// 跨库查询用户最新余额gxwebsoft_core.sys_user
User user = sysUserCrossDbMapper.selectByUserId(userId);
if (user == null) {
return fail("用户不存在");
}
BigDecimal currentBalance = user.getBalance() != null ? user.getBalance() : BigDecimal.ZERO;
BigDecimal payPrice = sub.getPayPrice();
// 检查余额是否充足
if (currentBalance.compareTo(payPrice) < 0) {
return fail("余额不足,当前余额:" + currentBalance + " 元,需要:" + payPrice + "");
}
// 扣除余额(跨库更新 gxwebsoft_core.sys_user
BigDecimal newBalance = currentBalance.subtract(payPrice);
int updated = sysUserCrossDbMapper.updateBalance(userId, newBalance);
if (updated <= 0) {
return fail("余额扣除失败");
}
// 记录余额变动日志(跨库插入 gxwebsoft_core.sys_user_balance_log
sysUserCrossDbMapper.insertBalanceLog(
userId,
BalanceConstants.BALANCE_USE,
payPrice,
newBalance,
sub.getSubscriptionNo(),
"应用订阅:" + sub.getProductName(),
user.getTenantId()
);
// 激活订阅
sub.setStatus("active");
sub.setPayStatus(1);
sub.setPayType(0); // 0-余额支付
sub.setPayTime(LocalDateTime.now());
sub.setStartTime(LocalDateTime.now());
if ("subscription".equals(sub.getPriceType())) {
int months = "year".equals(sub.getSubscriptionPeriod()) ? 12 : 1;
sub.setExpireTime(LocalDateTime.now().plusMonths(months));
}
subscriptionMapper.updateById(sub);
// 更新产品安装量
AppProduct product = productMapper.selectById(sub.getProductId());
if (product != null) {
product.setInstalls((product.getInstalls() != null ? product.getInstalls() : 0) + 1);
productMapper.updateById(product);
}
// 写入 Redis 标记(兼容轮询)
redisUtil.set("wxpay:paid:" + sub.getSubscriptionNo(), "1", 30L, java.util.concurrent.TimeUnit.MINUTES);
log.info("余额支付成功 — subscriptionNo: {}, userId: {}, amount: {}, remainingBalance: {}",
sub.getSubscriptionNo(), userId, payPrice, newBalance);
Map<String, Object> result = new HashMap<>();
result.put("paid", true);
result.put("balance", newBalance);
result.put("subscriptionNo", sub.getSubscriptionNo());
return success(result);
}
/**
* 微信 Native 支付处理
*/
private Object handleWechatPay(AppSubscription sub) {
// 查询产品名称作为支付描述
AppProduct product = productMapper.selectById(sub.getProductId());
String productName = product != null ? product.getProductName() : "应用订阅";
// 金额:元转分
BigDecimal payPriceYuan = sub.getPayPrice();
int totalFee = payPriceYuan.multiply(new BigDecimal(100)).intValue();
if (totalFee < 1) {
totalFee = 1; // 微信最低1分
}
log.info("========== 微信 Native 支付请求 ==========");
log.info("商户号: {}", appPayProperties.getMchId());
log.info("AppID: {}", appPayProperties.getAppId());
log.info("订单号: {}", sub.getSubscriptionNo());
log.info("商品描述: {}", productName);
log.info("支付金额(分): {}", totalFee);
log.info("回调地址: {}", appPayProperties.getNotifyUrl());
log.info("==========================================");
try {
Config config = wxNativePayUtil.getConfig(appPayProperties);
NativePayService payService = new NativePayService.Builder().config(config).build();
PrepayRequest request = new PrepayRequest();
request.setAppid(appPayProperties.getAppId());
request.setMchid(appPayProperties.getMchId());
request.setOutTradeNo(sub.getSubscriptionNo());
request.setDescription(productName);
request.setNotifyUrl(appPayProperties.getNotifyUrl());
Amount amount = new Amount();
amount.setTotal(totalFee);
request.setAmount(amount);
PrepayResponse response = payService.prepay(request);
String codeUrl = response.getCodeUrl();
log.info("微信 Native 支付下单成功 — subscriptionNo: {}, codeUrl: {}", sub.getSubscriptionNo(), codeUrl);
Map<String, Object> result = new HashMap<>();
result.put("subscriptionId", sub.getId());
result.put("subscriptionNo", sub.getSubscriptionNo());
result.put("codeUrl", codeUrl);
result.put("payPrice", payPriceYuan);
return success(result);
} catch (Exception e) {
log.error("========== 微信支付下单失败 ==========");
log.error("商户号: {}", appPayProperties.getMchId());
log.error("AppID: {}", appPayProperties.getAppId());
log.error("订单号: {}", sub.getSubscriptionNo());
log.error("错误类型: {}", e.getClass().getName());
log.error("错误信息: {}", e.getMessage());
if (e instanceof com.wechat.pay.java.core.exception.ServiceException) {
com.wechat.pay.java.core.exception.ServiceException wechatEx =
(com.wechat.pay.java.core.exception.ServiceException) e;
log.error("微信错误码: {}", wechatEx.getErrorCode());
log.error("微信错误信息: {}", wechatEx.getErrorMessage());
}
log.error("==========================================");
// 测试模式:返回模拟二维码
if (appPayProperties.isEnabled() && !appPayProperties.isTestMode()) {
return fail("微信支付服务异常,请稍后重试");
}
// 测试模式返回可展示的 mock URL
String mockCodeUrl = "weixin://wxpay/bizpayurl?pr=TEST" + System.currentTimeMillis();
Map<String, Object> result = new HashMap<>();
result.put("subscriptionId", sub.getId());
result.put("subscriptionNo", sub.getSubscriptionNo());
result.put("codeUrl", mockCodeUrl);
result.put("payPrice", payPriceYuan);
result.put("_testMode", true);
return success(result);
}
}
// ============================================================
// 我的订阅
// ============================================================
/**
* 我的订阅列表(分页)
*/
@GetMapping("/my/page")
@Operation(summary = "我的订阅列表")
public Object myPage(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer limit,
@RequestParam(required = false) String status) {
Integer userId = getLoginUserId();
if (userId == null) {
return success(new Page<AppSubscription>());
}
Page<AppSubscription> pageParam = new Page<>(page, limit);
LambdaQueryWrapper<AppSubscription> query = new LambdaQueryWrapper<>();
query.eq(AppSubscription::getUserId, userId);
if (status != null && !status.isEmpty()) {
query.eq(AppSubscription::getStatus, status);
}
query.orderByDesc(AppSubscription::getCreateTime);
Page<AppSubscription> pageResult = subscriptionMapper.selectPage(pageParam, query);
// 填充产品信息TODO: 后续改为 LEFT JOIN 优化)
pageResult.getRecords().forEach(sub -> {
AppProduct product = productMapper.selectById(sub.getProductId());
if (product != null) {
sub.setProductName(product.getProductName());
sub.setProductLogo(product.getLogo());
sub.setProductIcon(product.getIcon());
sub.setProductAppType(product.getAppType());
sub.setProductDescription(product.getDescription());
}
});
return success(pageResult);
}
/**
* 订阅详情
*/
@GetMapping("/detail/{id}")
@Operation(summary = "订阅详情")
public Object detail(@PathVariable Long id) {
AppSubscription sub = subscriptionMapper.selectById(id);
if (sub == null) {
return fail("订阅不存在");
}
AppProduct product = productMapper.selectById(sub.getProductId());
if (product != null) {
sub.setProductName(product.getProductName());
sub.setProductLogo(product.getLogo());
sub.setProductIcon(product.getIcon());
sub.setProductAppType(product.getAppType());
}
return success(sub);
}
// ============================================================
// 订阅管理
// ============================================================
/**
* 续费
*/
@PostMapping("/renew/{id}")
@Operation(summary = "续费")
public Object renew(@PathVariable Long id,
@RequestParam(defaultValue = "month") String period) {
AppSubscription sub = subscriptionMapper.selectById(id);
if (sub == null) return fail("订阅不存在");
Integer userId = getLoginUserId();
if (userId == null || !userId.equals(sub.getUserId())) return fail("无权操作");
int months = "year".equals(period) ? 12 : 1;
if ("active".equals(sub.getStatus()) && sub.getExpireTime() != null) {
sub.setExpireTime(sub.getExpireTime().plusMonths(months));
} else {
sub.setExpireTime(LocalDateTime.now().plusMonths(months));
}
sub.setStatus("active");
subscriptionMapper.updateById(sub);
return success("续费成功");
}
/**
* 退订/取消
*/
@PostMapping("/cancel/{id}")
@Operation(summary = "退订")
public Object cancel(@PathVariable Long id) {
AppSubscription sub = subscriptionMapper.selectById(id);
if (sub == null) return fail("订阅不存在");
Integer userId = getLoginUserId();
if (userId == null || !userId.equals(sub.getUserId())) return fail("无权操作");
sub.setStatus("cancelled");
subscriptionMapper.updateById(sub);
return success("退订成功");
}
/**
* 启用/禁用
*/
@PostMapping("/toggle-enable/{id}")
@Operation(summary = "启用/禁用")
public Object toggleEnable(@PathVariable Long id, @RequestParam Boolean enabled) {
AppSubscription sub = subscriptionMapper.selectById(id);
if (sub == null) return fail("订阅不存在");
Integer userId = getLoginUserId();
if (userId == null || !userId.equals(sub.getUserId())) return fail("无权操作");
if (enabled) {
if ("expired".equals(sub.getStatus()) ||
(sub.getExpireTime() != null && sub.getExpireTime().isBefore(LocalDateTime.now()))) {
return fail("订阅已过期,请先续费");
}
sub.setStatus("active");
} else {
sub.setStatus("cancelled");
}
subscriptionMapper.updateById(sub);
return success(enabled ? "已启用" : "已禁用");
}
// ============================================================
// 查询
// ============================================================
/**
* 查询支付状态(前端轮询用)
* 优先查 Redis回调写入加速感知再查数据库兜底
*/
@GetMapping("/check-status/{subscriptionNo}")
@Operation(summary = "查询支付状态")
public Object checkStatus(@PathVariable String subscriptionNo) {
LambdaQueryWrapper<AppSubscription> query = new LambdaQueryWrapper<>();
query.eq(AppSubscription::getSubscriptionNo, subscriptionNo);
AppSubscription sub = subscriptionMapper.selectOne(query);
if (sub == null) {
return fail("订阅不存在");
}
// 优先检查 Redis 支付标记(微信回调写入,加速前端轮询感知)
boolean paidViaRedis = "1".equals(redisUtil.get("wxpay:paid:" + subscriptionNo));
boolean paidViaDb = sub.getPayStatus() != null && sub.getPayStatus() == 1;
boolean isPaid = paidViaRedis || paidViaDb;
// 补全产品信息
AppProduct product = productMapper.selectById(sub.getProductId());
if (product != null) {
sub.setProductName(product.getProductName());
sub.setProductLogo(product.getLogo());
sub.setProductIcon(product.getIcon());
}
Map<String, Object> result = new HashMap<>();
result.put("paid", isPaid);
result.put("payStatus", sub.getPayStatus());
result.put("status", sub.getStatus());
result.put("payTime", sub.getPayTime());
result.put("transactionId", sub.getTransactionId());
// 补全订单详情字段,前端支付页可直接使用
result.put("id", sub.getId());
result.put("subscriptionNo", sub.getSubscriptionNo());
result.put("productId", sub.getProductId());
result.put("productName", sub.getProductName());
result.put("productLogo", sub.getProductLogo());
result.put("priceType", sub.getPriceType());
result.put("payPrice", sub.getPayPrice());
result.put("subscriptionPeriod", sub.getSubscriptionPeriod());
return success(result);
}
/**
* 检查是否已购买某应用
*/
@GetMapping("/check-purchased/{productId}")
@Operation(summary = "检查是否已购买")
public Object checkPurchased(@PathVariable Integer productId) {
Integer userId = getLoginUserId();
if (userId == null) return success(false);
LambdaQueryWrapper<AppSubscription> query = new LambdaQueryWrapper<>();
query.eq(AppSubscription::getUserId, userId)
.eq(AppSubscription::getProductId, productId)
.in(AppSubscription::getStatus, "active", "pending");
return success(subscriptionMapper.selectCount(query) > 0);
}
// ============================================================
// 内部方法
// ============================================================
private String generateSubscriptionNo() {
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
int random = ThreadLocalRandom.current().nextInt(1000, 9999);
return "SUB" + date + random;
}
}

View File

@@ -0,0 +1,161 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppProduct;
import com.gxwebsoft.app.entity.AppTicket;
import com.gxwebsoft.app.entity.AppTicketReply;
import com.gxwebsoft.app.param.AppTicketParam;
import com.gxwebsoft.app.service.AppProductService;
import com.gxwebsoft.app.service.AppTicketService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 应用工单控制器
*
* @author 科技小王子
* @since 2026-03-30
*/
@Slf4j
@Tag(name = "应用工单管理")
@RestController
@RequestMapping("/api/app/ticket")
public class AppTicketController extends BaseController {
@Resource
private AppTicketService appTicketService;
@Resource
private AppProductService appProductService;
// ─── 客户端接口 ────────────────────────────────────────────────
@Operation(summary = "查询我的工单(分页)")
@GetMapping("/my")
public ApiResult<PageResult<AppTicket>> myTickets(AppTicketParam param) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录",null);
return success(appTicketService.myPage(param, loginUser.getUserId()));
}
@Operation(summary = "提交工单")
@PostMapping("/submit")
public ApiResult<AppTicket> submit(@RequestBody AppTicket ticket) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录",null);
try {
AppTicket result = appTicketService.submit(ticket, loginUser.getUserId());
return success("工单提交成功", result);
} catch (Exception e) {
return fail(e.getMessage(),null);
}
}
@Operation(summary = "关闭工单(提交人)")
@PutMapping("/{ticketId}/close")
public ApiResult<?> close(@PathVariable Long ticketId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
try {
appTicketService.closeByUser(ticketId, loginUser.getUserId());
return success("工单已关闭");
} catch (Exception e) {
return fail(e.getMessage());
}
}
// ─── 技术端接口 ────────────────────────────────────────────────
@Operation(summary = "查询所有工单(技术人员)")
@GetMapping("/list")
public ApiResult<PageResult<AppTicket>> allTickets(AppTicketParam param) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录",null);
// 按用户有权限的应用过滤工单(看到的工单限于自己参与的应用)
List<AppProduct> accessibleApps = appProductService.getAccessibleApps(loginUser.getUserId());
if (accessibleApps != null && !accessibleApps.isEmpty()) {
param.setAppIds(accessibleApps.stream().map(AppProduct::getProductId).collect(Collectors.toList()));
}
return success(appTicketService.allPage(param));
}
@Operation(summary = "获取工单详情")
@GetMapping("/{ticketId}")
public ApiResult<AppTicket> detail(@PathVariable Long ticketId) {
return success(appTicketService.getById(ticketId));
}
@Operation(summary = "更新工单状态(技术人员)")
@PutMapping("/status")
public ApiResult<?> updateStatus(@RequestBody Map<String, Object> body) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
Long ticketId = Long.valueOf(body.get("ticketId").toString());
String status = body.get("status").toString();
appTicketService.updateStatus(ticketId, status, loginUser.getUserId());
return success("状态已更新");
}
@Operation(summary = "分配处理人(管理员)")
@PutMapping("/assign")
public ApiResult<?> assign(@RequestBody Map<String, Object> body) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
Long ticketId = Long.valueOf(body.get("ticketId").toString());
Integer assigneeId = Integer.valueOf(body.get("assigneeId").toString());
appTicketService.assign(ticketId, assigneeId);
return success("分配成功");
}
// ─── 回复接口 ─────────────────────────────────────────────────
@Operation(summary = "获取工单回复列表")
@GetMapping("/{ticketId}/replies")
public ApiResult<List<AppTicketReply>> replies(@PathVariable Long ticketId) {
return success(appTicketService.getReplies(ticketId));
}
@Operation(summary = "提交工单回复")
@PostMapping("/reply")
public ApiResult<AppTicketReply> reply(@RequestBody AppTicketReply reply) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录",null);
if (reply.getContent() == null || reply.getContent().trim().isEmpty()) {
return fail("回复内容不能为空",null);
}
try {
AppTicketReply result = appTicketService.addReply(reply, loginUser.getUserId());
return success("回复成功", result);
} catch (Exception e) {
return fail(e.getMessage(),null);
}
}
// ─── 统计 & 辅助 ─────────────────────────────────────────────
@Operation(summary = "工单统计数据")
@GetMapping("/stats")
public ApiResult<Map<String, Long>> stats(
@RequestParam(required = false) Long appId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录",null);
// 技术端不限制用户维度;客户端通过路由区分
return success(appTicketService.stats(appId, null));
}
@Operation(summary = "获取技术人员列表(用于分配)")
@GetMapping("/staff-list")
public ApiResult<List<Map<String, Object>>> staffList() {
return success(appTicketService.getTechStaffList());
}
}

View File

@@ -0,0 +1,293 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.app.service.AppUserService;
import com.gxwebsoft.app.entity.AppUser;
import com.gxwebsoft.app.param.AppUserParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.app.mapper.SysUserCrossDbMapper;
import com.gxwebsoft.app.mapper.AppUserMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.*;
/**
* 应用成员控制器
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Slf4j
@Tag(name = "应用成员管理")
@RestController
@RequestMapping("/api/app/app-user")
public class AppUserController extends BaseController {
@Resource
private AppUserService appUserService;
@Resource
private SysUserCrossDbMapper sysUserCrossDbMapper;
@Resource
private AppUserMapper appUserMapper;
@Operation(summary = "分页查询应用成员")
@GetMapping("/page")
public ApiResult<PageResult<AppUser>> page(AppUserParam param) {
return success(appUserService.pageRel(param));
}
@Operation(summary = "查询全部应用成员")
@GetMapping()
public ApiResult<List<AppUser>> list(AppUserParam param) {
return success(appUserService.listRel(param));
}
@Operation(summary = "根据id查询应用成员")
@GetMapping("/{id}")
public ApiResult<AppUser> get(@PathVariable("id") Integer id) {
return success(appUserService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('app:appUser:save')")
@OperationLog
@Operation(summary = "添加应用成员(手动添加)")
@PostMapping()
public ApiResult<?> save(@RequestBody AppUser appUser) {
User loginUser = getLoginUser();
if (loginUser != null) {
appUser.setUserId(loginUser.getUserId());
appUser.setTenantId(loginUser.getTenantId());
}
if (appUserService.save(appUser)) {
return success("添加成功");
}
return fail("添加失败");
}
@Operation(summary = "邀请用户成为应用成员支持用户ID或手机号")
@PostMapping("/invite")
public ApiResult<?> invite(@RequestBody AppUser appUser) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
// 支持手机号邀请:若 userId 为空但传了 phone则先按手机号查出用户
if (appUser.getUserId() == null && appUser.getPhone() != null && !appUser.getPhone().isEmpty()) {
User targetUser = appUserService.findUserByPhone(appUser.getPhone());
if (targetUser == null) {
return fail("手机号未注册,请确认后再试");
}
appUser.setUserId(targetUser.getUserId());
}
if (appUser.getUserId() == null) {
return fail("请输入用户ID或手机号");
}
try {
AppUser result = appUserService.inviteUser(
appUser.getAppId(),
appUser.getUserId(),
appUser.getRole(),
loginUser.getUserId(),
loginUser.getTenantId()
);
return success("邀请成功", result);
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
@PreAuthorize("hasAuthority('app:appUser:update')")
@OperationLog
@Operation(summary = "修改应用成员信息")
@PutMapping()
public ApiResult<?> update(@RequestBody AppUser appUser) {
if (appUserService.updateById(appUser)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appUser:update')")
@OperationLog
@Operation(summary = "修改成员角色")
@PutMapping("/role/{id}/{role}")
public ApiResult<?> updateRole(@PathVariable("id") Long id, @PathVariable("role") String role) {
if (appUserService.updateRole(id, role)) {
return success("角色修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appUser:remove')")
@OperationLog
@Operation(summary = "移除应用成员")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (appUserService.removeById(id)) {
return success("已移除");
}
return fail("移除失败");
}
@PreAuthorize("hasAuthority('app:appUser:save')")
@OperationLog
@Operation(summary = "批量添加应用成员")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<AppUser> list) {
if (appUserService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('app:appUser:update')")
@OperationLog
@Operation(summary = "批量修改应用成员")
@PutMapping("/batch")
public ApiResult<?> updateBatch(@RequestBody BatchParam<AppUser> batchParam) {
if (batchParam.update(appUserService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appUser:remove')")
@OperationLog
@Operation(summary = "批量移除应用成员")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (appUserService.removeByIds(ids)) {
return success("移除成功");
}
return fail("移除失败");
}
@Operation(summary = "搜索用户(用于邀请成员,支持手机号/用户名/昵称模糊搜索)")
@GetMapping("/search")
public ApiResult<List<User>> searchUsers(@RequestParam("keyword") String keyword) {
if (keyword == null || keyword.trim().isEmpty()) {
return success(Collections.emptyList());
}
List<User> users = appUserService.searchUsers(keyword.trim());
return success(users);
}
// ============ 邀请确认相关接口 ============
@Operation(summary = "查询当前用户的待确认邀请列表")
@GetMapping("/invites/pending")
public ApiResult<List<AppUser>> listPendingInvites() {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录",null);
}
return success(appUserService.listPendingInvites(loginUser.getUserId()));
}
@Operation(summary = "统计当前用户的待确认邀请数量")
@GetMapping("/invites/pending/count")
public ApiResult<Map<String, Long>> countPendingInvites() {
User loginUser = getLoginUser();
if (loginUser == null) {
return success(Map.of("count", 0L));
}
long count = appUserService.countPendingInvites(loginUser.getUserId());
return success(Map.of("count", count));
}
@Operation(summary = "接受邀请加入应用")
@PostMapping("/invites/{inviteId}/accept")
public ApiResult<?> acceptInvite(@PathVariable("inviteId") Long inviteId) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
try {
if (appUserService.acceptInvite(inviteId, loginUser.getUserId())) {
return success("已接受邀请,加入应用成功");
}
return fail("接受邀请失败");
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
@Operation(summary = "拒绝邀请")
@PostMapping("/invites/{inviteId}/reject")
public ApiResult<?> rejectInvite(@PathVariable("inviteId") Long inviteId) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
try {
if (appUserService.rejectInvite(inviteId, loginUser.getUserId())) {
return success("已拒绝邀请");
}
return fail("拒绝邀请失败");
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
// ============ 权限检查接口 ============
/**
* 检查当前用户是否有开发者中心访问权限
* 判断逻辑:
* 1. sys_user.type === 2 → 平台开发者,直接放行
* 2. 在 app_user 表中有成员记录 → 协作成员,放行
* 3. 创建过应用app_product.user_id → 自动有权限
* 返回accessible, isPlatformDeveloper, apps可访问应用列表及角色
*/
@Operation(summary = "检查开发者中心访问权限")
@GetMapping("/check-access")
public ApiResult<Map<String, Object>> checkAccess() {
User loginUser = getLoginUser();
if (loginUser == null) {
return success(Map.of(
"accessible", false,
"isPlatformDeveloper", false,
"hasJoinedApps", false
));
}
Integer userId = loginUser.getUserId();
// 1. 判断是否平台级开发者type === 2
boolean isPlatformDeveloper = false;
try {
Integer userType = sysUserCrossDbMapper.selectUserType(userId);
isPlatformDeveloper = userType != null && userType == 2;
} catch (Exception e) {
log.warn("查询用户类型失败userId={}, error={}", userId, e.getMessage());
}
// 2. 查询用户参与的应用(创建的 + 被邀请的)
List<Map<String, Object>> apps = appUserMapper.selectUserAccessibleApps(userId);
boolean hasJoinedApps = apps != null && !apps.isEmpty();
// 3. 平台开发者 或 有参与的应用 → 可访问
boolean accessible = isPlatformDeveloper || hasJoinedApps;
// 4. 构建返回结果
Map<String, Object> result = new LinkedHashMap<>();
result.put("accessible", accessible);
result.put("isPlatformDeveloper", isPlatformDeveloper);
result.put("hasJoinedApps", hasJoinedApps);
result.put("apps", apps != null ? apps : Collections.emptyList());
return success(result);
}
}

View File

@@ -0,0 +1,181 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppUserCache;
import com.gxwebsoft.app.service.AppUserCacheService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
/**
* 用户同步控制器(供 server 端调用)
*
* @author WebSoft
* @since 2026-04-04
*/
@Slf4j
@Tag(name = "用户同步server 端调用)")
@RestController
@RequestMapping("/api/app/user-sync")
public class AppUserSyncController extends BaseController {
@Resource
private AppUserCacheService appUserCacheService;
/**
* 同步单个用户到缓存表
* 供 server 端用户注册成功后调用
*/
@Operation(summary = "同步单个用户server 端调用)")
@PostMapping("/single")
public ApiResult<?> syncSingleUser(@RequestBody AppUserCache userCache) {
log.info("收到用户同步请求: userId={}, username={}, nickname={}, phone={}, tenantId={}",
userCache.getUserId(), userCache.getUsername(), userCache.getNickname(), userCache.getPhone(), userCache.getTenantId());
if (userCache.getUserId() == null) {
return fail("userId 不能为空");
}
if (userCache.getTenantId() == null) {
return fail("tenantId 不能为空");
}
try {
// 设置更新时间(如果为空)
if (userCache.getUpdateTime() == null) {
userCache.setUpdateTime(LocalDateTime.now());
}
// 直接保存或更新缓存
boolean result = appUserCacheService.saveOrUpdate(userCache);
if (result) {
log.info("用户同步成功: userId={}, username={}, tenantId={}", userCache.getUserId(), userCache.getUsername(), userCache.getTenantId());
} else {
log.warn("用户同步返回失败: userId={}", userCache.getUserId());
}
return success("同步成功");
} catch (Exception e) {
log.error("用户同步失败: userId={}, error={}", userCache.getUserId(), e.getMessage(), e);
return fail("同步失败: " + e.getMessage());
}
}
/**
* 批量同步用户到缓存表
*/
@Operation(summary = "批量同步用户server 端调用)")
@PostMapping("/batch")
public ApiResult<?> syncBatchUsers(@RequestBody List<AppUserCache> userCaches) {
log.info("收到批量用户同步请求: count={}", userCaches.size());
if (userCaches.isEmpty()) {
return fail("用户列表不能为空");
}
// 校验每个用户数据
for (AppUserCache userCache : userCaches) {
if (userCache.getUserId() == null) {
return fail("用户列表中存在 userId 为空的记录");
}
if (userCache.getTenantId() == null) {
return fail("用户列表中存在 tenantId 为空的记录userId=" + userCache.getUserId());
}
}
try {
appUserCacheService.saveOrUpdateBatch(userCaches);
log.info("批量用户同步成功: count={}", userCaches.size());
return success("批量同步成功");
} catch (Exception e) {
log.error("批量用户同步失败: error={}", e.getMessage());
return fail("批量同步失败: " + e.getMessage());
}
}
/**
* 根据 userId 刷新用户缓存
* server 端可以只传 userIdwebsopy 端通过 API 回查 server 获取完整信息
*/
@Operation(summary = "刷新用户缓存server 端调用)")
@PostMapping("/refresh/{userId}")
public ApiResult<?> refreshUserCache(@PathVariable("userId") Integer userId) {
log.info("收到刷新用户缓存请求: userId={}", userId);
if (userId == null) {
return fail("userId 不能为空");
}
try {
appUserCacheService.refreshUserCache(userId);
log.info("用户缓存刷新成功: userId={}", userId);
return success("刷新成功");
} catch (Exception e) {
log.error("用户缓存刷新失败: userId={}, error={}", userId, e.getMessage());
return fail("刷新失败: " + e.getMessage());
}
}
/**
* 删除用户缓存
* 供 server 端删除用户时调用,同步删除本地缓存
*/
@Operation(summary = "删除用户缓存server 端调用)")
@PostMapping("/delete/{userId}")
public ApiResult<?> deleteUserCache(@PathVariable("userId") Integer userId) {
log.info("收到删除用户缓存请求: userId={}", userId);
if (userId == null) {
return fail("userId 不能为空");
}
try {
boolean result = appUserCacheService.removeById(userId);
if (result) {
log.info("用户缓存删除成功: userId={}", userId);
return success("删除成功");
} else {
log.warn("用户缓存删除失败或用户不存在: userId={}", userId);
return success("用户不存在或已删除");
}
} catch (Exception e) {
log.error("用户缓存删除失败: userId={}, error={}", userId, e.getMessage());
return fail("删除失败: " + e.getMessage());
}
}
/**
* 批量删除用户缓存
* 供 server 端批量删除用户时调用
*/
@Operation(summary = "批量删除用户缓存server 端调用)")
@PostMapping("/delete/batch")
public ApiResult<?> deleteBatchUserCache(@RequestBody List<Integer> userIds) {
log.info("收到批量删除用户缓存请求: count={}", userIds.size());
if (userIds == null || userIds.isEmpty()) {
return fail("userId 列表不能为空");
}
try {
boolean result = appUserCacheService.removeByIds(userIds);
if (result) {
log.info("批量用户缓存删除成功: count={}", userIds.size());
return success("批量删除成功");
} else {
log.warn("批量用户缓存删除失败: count={}", userIds.size());
return success("删除失败或用户不存在");
}
} catch (Exception e) {
log.error("批量用户缓存删除失败: error={}", e.getMessage());
return fail("批量删除失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,173 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.app.service.AppVersionService;
import com.gxwebsoft.app.entity.AppVersion;
import com.gxwebsoft.app.param.AppVersionParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 应用版本发布记录控制器
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Slf4j
@Tag(name = "应用版本发布管理")
@RestController
@RequestMapping("/api/app/app-version")
public class AppVersionController extends BaseController {
@Resource
private AppVersionService appVersionService;
@Operation(summary = "分页查询版本记录")
@GetMapping("/page")
public ApiResult<PageResult<AppVersion>> page(AppVersionParam param) {
return success(appVersionService.pageRel(param));
}
@Operation(summary = "查询全部版本记录")
@GetMapping()
public ApiResult<List<AppVersion>> list(AppVersionParam param) {
return success(appVersionService.listRel(param));
}
@Operation(summary = "根据id查询版本")
@GetMapping("/{id}")
public ApiResult<AppVersion> get(@PathVariable("id") Integer id) {
return success(appVersionService.getByIdRel(id));
}
@Operation(summary = "获取应用当前版本")
@GetMapping("/current/{appId}")
public ApiResult<AppVersion> getCurrentVersion(@PathVariable("appId") Long appId) {
return success(appVersionService.getCurrentVersion(appId));
}
@PreAuthorize("hasAuthority('app:appVersion:save')")
@OperationLog
@Operation(summary = "新增版本(构建中状态)")
@PostMapping()
public ApiResult<?> save(@RequestBody AppVersion appVersion) {
User loginUser = getLoginUser();
if (loginUser != null) {
appVersion.setUserId(loginUser.getUserId());
appVersion.setTenantId(loginUser.getTenantId());
}
// 默认为构建中状态
if (appVersion.getStatus() == null) {
appVersion.setStatus(0);
}
if (appVersion.getEnv() == null) {
appVersion.setEnv("production");
}
appVersion.setIsCurrent(false);
if (appVersionService.save(appVersion)) {
return success("创建成功");
}
return fail("创建失败");
}
@PreAuthorize("hasAuthority('app:appVersion:update')")
@OperationLog
@Operation(summary = "修改版本信息")
@PutMapping()
public ApiResult<?> update(@RequestBody AppVersion appVersion) {
if (appVersionService.updateById(appVersion)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appVersion:update')")
@OperationLog
@Operation(summary = "发布版本(将此版本设为当前运行版本)")
@PostMapping("/publish/{id}")
public ApiResult<?> publish(@PathVariable("id") Long id) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
try {
appVersionService.publish(id, loginUser.getUserId());
return success("发布成功");
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
@PreAuthorize("hasAuthority('app:appVersion:update')")
@OperationLog
@Operation(summary = "回滚到指定版本")
@PostMapping("/rollback/{id}")
public ApiResult<?> rollback(@PathVariable("id") Long id) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
try {
appVersionService.rollback(id, loginUser.getUserId());
return success("回滚成功");
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
@PreAuthorize("hasAuthority('app:appVersion:remove')")
@OperationLog
@Operation(summary = "删除版本记录")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (appVersionService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('app:appVersion:save')")
@OperationLog
@Operation(summary = "批量添加版本")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<AppVersion> list) {
if (appVersionService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('app:appVersion:update')")
@OperationLog
@Operation(summary = "批量修改版本")
@PutMapping("/batch")
public ApiResult<?> updateBatch(@RequestBody BatchParam<AppVersion> batchParam) {
if (batchParam.update(appVersionService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appVersion:remove')")
@OperationLog
@Operation(summary = "批量删除版本记录")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (appVersionService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -0,0 +1,205 @@
package com.gxwebsoft.app.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.app.config.AppPayProperties;
import com.gxwebsoft.app.entity.AppSubscription;
import com.gxwebsoft.app.mapper.AppSubscriptionMapper;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.wechat.pay.java.core.cipher.AeadAesCipher;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.Base64Utils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 微信支付回调通知处理
* 路径POST /api/app/subscription/wx-notify
* 由微信支付服务器主动调用Native 扫码支付成功后会回调此地址)
* <p>
* 注意:前端同时在轮询 check-status 接口,回调只是加速状态更新。
* 即使回调失败,前端轮询也能在 3s 内查到已支付状态。
*/
@Slf4j
@RestController
@RequestMapping("/api/app/subscription")
@RequiredArgsConstructor
@Tag(name = "微信支付回调")
public class AppWxPayController {
@Resource
private AppSubscriptionMapper subscriptionMapper;
@Resource
private RedisUtil redisUtil;
@Resource
private AppPayProperties appPayProperties;
@Value("${spring.profiles.active:dev}")
private String active;
/**
* 微信 Native 支付回调通知
* 微信支付成功后会 POST JSON 到此地址
*/
@PostMapping("/wx-notify")
@Operation(summary = "微信支付回调通知")
public String wxNotify(
@RequestHeader(value = "Wechatpay-Serial", required = false) String serialNumber,
@RequestHeader(value = "Wechatpay-Nonce", required = false) String nonce,
@RequestHeader(value = "Wechatpay-Signature", required = false) String signature,
@RequestHeader(value = "Wechatpay-Timestamp", required = false) String timestamp,
@RequestBody String body
) {
log.info("收到微信支付回调 — headers: serial={}, timestamp={}, body={}", serialNumber, timestamp, body);
try {
// 1. 解析通知(解密报文体)
JSONObject notification = parseNotification(body);
if (notification == null) {
log.error("通知解析失败");
return failResult("notification parse failed");
}
// 2. 提取关键字段
String eventType = notification.getString("event_type");
JSONObject resource = notification.getJSONObject("resource");
if (resource == null) {
log.error("通知报文体中无 resource 字段");
return failResult("no resource");
}
// 解密资源
String ciphertext = resource.getString("ciphertext");
String nonceStr = resource.getString("nonce");
String associatedData = resource.getString("associated_data");
String apiV3Key = getApiV3Key();
String plainText;
try {
plainText = decryptResource(ciphertext, apiV3Key, nonceStr, associatedData);
} catch (Exception e) {
log.error("资源解密失败: {}", e.getMessage());
return failResult("decrypt failed");
}
JSONObject tradeData = JSON.parseObject(plainText);
String outTradeNo = tradeData.getString("out_trade_no");
String tradeState = tradeData.getString("trade_state");
String transactionId = tradeData.getString("transaction_id");
BigDecimal totalAmount = tradeData.getBigDecimal("amount") != null
? tradeData.getJSONObject("amount").getBigDecimal("payer_total") : BigDecimal.ZERO;
log.info("解密后 — outTradeNo={}, tradeState={}, transactionId={}, amount={}",
outTradeNo, tradeState, transactionId, totalAmount);
// 3. 仅处理支付成功事件
if (!"TRANSACTION.SUCCESS".equals(eventType) && !"SUCCESS".equals(tradeState)) {
log.warn("非成功回调,忽略 — eventType: {}, tradeState: {}", eventType, tradeState);
return successResult();
}
// 4. 查询并更新订阅记录
LambdaQueryWrapper<AppSubscription> query = new LambdaQueryWrapper<>();
query.eq(AppSubscription::getSubscriptionNo, outTradeNo);
AppSubscription sub = subscriptionMapper.selectOne(query);
if (sub == null) {
log.warn("未找到订阅记录 outTradeNo: {}", outTradeNo);
return successResult(); // 返回成功,避免微信重复回调
}
if (sub.getPayStatus() != null && sub.getPayStatus() == 1) {
log.info("订阅 {} 已支付,跳过重复处理", outTradeNo);
return successResult();
}
// 5. 更新状态
sub.setPayStatus(1);
sub.setStatus("active");
sub.setPayTime(LocalDateTime.now());
sub.setTransactionId(transactionId);
sub.setPayType(1); // 1=微信支付
sub.setStartTime(LocalDateTime.now());
// 设置到期时间(订阅型)
if ("subscription".equals(sub.getPriceType())) {
int months = "year".equals(sub.getSubscriptionPeriod()) ? 12 : 1;
sub.setExpireTime(LocalDateTime.now().plusMonths(months));
}
subscriptionMapper.updateById(sub);
log.info("订阅 {} 支付成功,状态已更新", outTradeNo);
// 6. 写入 Redis加速前端轮询感知
redisUtil.set("wxpay:paid:" + outTradeNo, "1", Duration.ofHours(24));
return successResult();
} catch (Exception e) {
log.error("微信支付回调处理异常: {}", e.getMessage(), e);
return failResult("internal error: " + e.getMessage());
}
}
/**
* 解析通知V3 API 使用 AES-256-GCM 解密 resource 字段)
*/
private JSONObject parseNotification(String body) {
try {
return JSON.parseObject(body);
} catch (Exception e) {
log.error("JSON 解析失败: {}", body);
return null;
}
}
/**
* AES-256-GCM 解密微信 V3 通知 resource.ciphertext
* - resource.nonceBase64 编码的 12 字节随机数
* - resource.ciphertextBase64(AEAD_AES_256_GCM(nonce + plaintext))
* 其中 ciphertext 末尾 16 字节为 tag
*/
private String decryptResource(String ciphertext, String apiV3Key, String nonce, String associatedData)
throws Exception {
byte[] key = apiV3Key.getBytes(StandardCharsets.UTF_8);
// nonce 是 Base64 编码的 12 字节随机数,需要解码
byte[] nonceBytes = Base64Utils.decodeFromString(nonce);
// associated_data 原样 UTF-8 编码
byte[] aad = (associatedData == null ? "" : associatedData).getBytes(StandardCharsets.UTF_8);
// ciphertext = Base64(ciphertext_bytes),其中末尾 16 字节为 auth tag
byte[] cipherBytes = Base64Utils.decodeFromString(ciphertext);
// 新版 SDKAeadAesCipher.decrypt(nonce, associatedData, ciphertextWithTag)
AeadAesCipher cipher = new AeadAesCipher(key);
return cipher.decrypt(nonceBytes, aad, cipherBytes);
}
/**
* 获取 APIv3 密钥(根据环境选择正式/测试配置)
*/
private String getApiV3Key() {
if ("dev".equals(active) && appPayProperties.isTestMode()) {
String key = appPayProperties.getTestApiV3Key();
return key != null ? key : appPayProperties.getApiV3Key();
}
return appPayProperties.getApiV3Key();
}
private String successResult() {
return "{\"code\":\"SUCCESS\",\"message\":\"OK\"}";
}
private String failResult(String msg) {
return "{\"code\":\"FAIL\",\"message\":\"" + msg + "\"}";
}
}

View File

@@ -0,0 +1,256 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppBuild;
import com.gxwebsoft.app.entity.AppPipeline;
import com.gxwebsoft.app.param.AppBuildParam;
import com.gxwebsoft.app.param.AppPipelineParam;
import com.gxwebsoft.app.service.AppBuildService;
import com.gxwebsoft.app.service.AppPipelineService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* CI/CD 控制器
* 支持 Jenkins / GitHub Actions / Gitea CI
*
* @author 科技小王子
* @since 2026-04-03
*/
@Slf4j
@Tag(name = "CI/CD管理")
@RestController
@RequestMapping("/api/app/cicd")
public class CICDController extends BaseController {
@Resource
private AppBuildService appBuildService;
@Resource
private AppPipelineService appPipelineService;
// ========== 流水线接口 ==========
@Operation(summary = "分页查询流水线")
@GetMapping("/pipeline/page")
public ApiResult<PageResult<AppPipeline>> pagePipeline(AppPipelineParam param) {
User loginUser = getLoginUser();
if (loginUser != null) {
param.setUserId(loginUser.getUserId());
}
return success(appPipelineService.pagePipeline(param));
}
@Operation(summary = "查询应用的所有流水线")
@GetMapping("/pipeline/app/{appId}")
public ApiResult<List<AppPipeline>> listByApp(@PathVariable Long appId) {
return success(appPipelineService.getByAppId(appId));
}
@Operation(summary = "查询流水线详情")
@GetMapping("/pipeline/{id}")
public ApiResult<AppPipeline> getPipeline(@PathVariable Long id) {
return success(appPipelineService.getPipelineDetail(id));
}
@PreAuthorize("hasAuthority('app:cicd:pipeline:save')")
@Operation(summary = "创建流水线")
@PostMapping("/pipeline")
public ApiResult<?> createPipeline(@RequestBody AppPipeline pipeline) {
User loginUser = getLoginUser();
if (loginUser != null) {
pipeline.setUserId(loginUser.getUserId());
pipeline.setTenantId(loginUser.getTenantId());
}
if (appPipelineService.createPipeline(pipeline)) {
return success("创建成功");
}
return fail("创建失败");
}
@PreAuthorize("hasAuthority('app:cicd:pipeline:update')")
@Operation(summary = "更新流水线")
@PutMapping("/pipeline")
public ApiResult<?> updatePipeline(@RequestBody AppPipeline pipeline) {
if (appPipelineService.updatePipeline(pipeline)) {
return success("更新成功");
}
return fail("更新失败");
}
@PreAuthorize("hasAuthority('app:cicd:pipeline:remove')")
@Operation(summary = "删除流水线")
@DeleteMapping("/pipeline/{id}")
public ApiResult<?> deletePipeline(@PathVariable Long id) {
if (appPipelineService.deletePipeline(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('app:cicd:pipeline:update')")
@Operation(summary = "启用/禁用流水线")
@PostMapping("/pipeline/{id}/toggle")
public ApiResult<?> togglePipeline(@PathVariable Long id, @RequestParam boolean enabled) {
if (appPipelineService.togglePipeline(id, enabled)) {
return success(enabled ? "已启用" : "已禁用");
}
return fail("操作失败");
}
@Operation(summary = "获取流水线状态")
@GetMapping("/pipeline/{id}/status")
public ApiResult<Map<String, Object>> getPipelineStatus(@PathVariable Long id) {
AppPipeline pipeline = appPipelineService.getById(id);
Map<String, Object> result = new HashMap<>();
result.put("id", id);
result.put("status", appPipelineService.getPipelineStatus(id));
result.put("lastBuildId", pipeline != null ? pipeline.getLastBuildId() : null);
result.put("lastBuildTime", pipeline != null ? pipeline.getLastBuildTime() : null);
result.put("successCount", pipeline != null ? pipeline.getSuccessCount() : 0);
result.put("failureCount", pipeline != null ? pipeline.getFailureCount() : 0);
return success(result);
}
// ========== 构建接口 ==========
@Operation(summary = "分页查询构建记录")
@GetMapping("/build/page")
public ApiResult<PageResult<AppBuild>> pageBuild(AppBuildParam param) {
User loginUser = getLoginUser();
if (loginUser != null) {
param.setUserId(loginUser.getUserId());
}
return success(appBuildService.pageBuild(param));
}
@Operation(summary = "查询应用的所有构建记录")
@GetMapping("/build/app/{appId}")
public ApiResult<List<AppBuild>> listBuildByApp(@PathVariable Long appId) {
AppBuildParam param = new AppBuildParam();
param.setAppId(appId);
return success(appBuildService.pageBuild(param).getList());
}
@Operation(summary = "查询构建详情")
@GetMapping("/build/{id}")
public ApiResult<AppBuild> getBuild(@PathVariable Long id) {
return success(appBuildService.getBuildDetail(id));
}
@Operation(summary = "获取应用最新构建")
@GetMapping("/build/latest/{appId}")
public ApiResult<AppBuild> getLatestBuild(@PathVariable Long appId) {
return success(appBuildService.getLatestBuild(appId));
}
@PreAuthorize("hasAuthority('app:cicd:build:save')")
@Operation(summary = "触发构建")
@PostMapping("/build/trigger")
public ApiResult<Map<String, Object>> triggerBuild(
@RequestParam Long appId,
@RequestParam(required = false) String branch) {
User loginUser = getLoginUser();
Integer triggeredBy = loginUser != null ? loginUser.getUserId() : null;
try {
AppBuild build = appBuildService.triggerBuild(appId, branch, triggeredBy);
Map<String, Object> result = new HashMap<>();
result.put("id", build.getId());
result.put("buildNumber", build.getBuildNumber());
result.put("status", build.getStatus());
result.put("branch", build.getBranch());
return success("构建已触发", result);
} catch (Exception e) {
return fail(e.getMessage(), null);
}
}
@Operation(summary = "获取构建日志")
@GetMapping("/build/{id}/log")
public ApiResult<Map<String, Object>> getBuildLog(@PathVariable Long id) {
try {
String log = appBuildService.getBuildLog(id);
Map<String, Object> result = new HashMap<>();
result.put("log", log);
result.put("buildId", id);
return success(result);
} catch (Exception e) {
return fail(e.getMessage(), null);
}
}
@PreAuthorize("hasAuthority('app:cicd:build:update')")
@Operation(summary = "取消构建")
@PostMapping("/build/{id}/cancel")
public ApiResult<?> cancelBuild(@PathVariable Long id) {
try {
if (appBuildService.cancelBuild(id)) {
return success("构建已取消");
}
return fail("取消失败");
} catch (Exception e) {
return fail(e.getMessage());
}
}
@PreAuthorize("hasAuthority('app:cicd:build:save')")
@Operation(summary = "重试构建")
@PostMapping("/build/{id}/retry")
public ApiResult<Map<String, Object>> retryBuild(@PathVariable Long id) {
User loginUser = getLoginUser();
Integer triggeredBy = loginUser != null ? loginUser.getUserId() : null;
try {
AppBuild build = appBuildService.retryBuild(id, triggeredBy);
Map<String, Object> result = new HashMap<>();
result.put("id", build.getId());
result.put("buildNumber", build.getBuildNumber());
result.put("status", build.getStatus());
return success("构建已重试", result);
} catch (Exception e) {
return fail(e.getMessage(), null);
}
}
@Operation(summary = "获取构建统计")
@GetMapping("/build/stats/{appId}")
public ApiResult<Map<String, Integer>> getBuildStats(@PathVariable Long appId) {
return success(appBuildService.getBuildStats(appId));
}
// ========== Webhook 回调接口 ==========
@Operation(summary = "Gitea Webhook 回调")
@PostMapping("/webhook/gitea")
public ApiResult<?> giteaWebhook(@RequestBody Map<String, Object> payload) {
try {
appBuildService.handleWebhook("gitea", payload);
return success("回调处理成功");
} catch (Exception e) {
log.error("Gitea webhook处理失败: {}", e.getMessage());
return fail("处理失败: " + e.getMessage());
}
}
@Operation(summary = "获取CI系统配置")
@GetMapping("/config")
public ApiResult<Map<String, Object>> getCIConfig() {
Map<String, Object> config = new HashMap<>();
config.put("supportedCI", new String[]{"gitea", "jenkins", "github"});
config.put("defaultCI", "gitea");
config.put("giteaUrl", "https://git.websoft.top");
return success(config);
}
}

View File

@@ -0,0 +1,176 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppGitAccount;
import com.gxwebsoft.app.service.AppGitAccountService;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Git账号绑定控制器
*
* @author 科技小王子
* @since 2026-04-02
*/
@Slf4j
@Tag(name = "Git账号绑定")
@RestController
@RequestMapping("/api/app/developer")
public class GitAccountController extends BaseController {
@Resource
private AppGitAccountService appGitAccountService;
/**
* 保存Git账号绑定信息
*/
@OperationLog
@Operation(summary = "保存Git账号绑定")
@PostMapping("/git-account")
public ApiResult<Map<String, Object>> saveGitAccount(@RequestBody Map<String, String> params) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录", null);
}
String username = params.get("username");
String email = params.get("email");
String remark = params.get("remark");
try {
AppGitAccount account = appGitAccountService.saveGitAccount(username, email, remark, loginUser.getUserId(), loginUser.getTenantId());
Map<String, Object> result = new HashMap<>();
result.put("userId", account.getUserId());
result.put("gitUsername", account.getUsername());
result.put("email", account.getEmail());
result.put("remark", account.getRemark());
result.put("savedAt", account.getUpdateTime() != null ? account.getUpdateTime().toString() : account.getCreateTime().toString());
result.put("status", account.getStatus());
return success("Git账号绑定成功", result);
} catch (Exception e) {
log.error("保存Git账号绑定失败: {}", e.getMessage());
return fail(e.getMessage(), null);
}
}
/**
* 获取Git账号绑定状态
*/
@Operation(summary = "获取Git账号绑定状态")
@GetMapping({"/git-account", "/git-account/status"})
public ApiResult<Map<String, Object>> getGitAccountStatus() {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录", null);
}
AppGitAccount account = appGitAccountService.getGitAccountStatus(loginUser.getUserId(), loginUser.getTenantId());
Map<String, Object> result = new HashMap<>();
if (account != null && account.getStatus() != null && !"not_bound".equals(account.getStatus())) {
result.put("username", account.getUsername());
result.put("email", account.getEmail());
result.put("remark", account.getRemark());
result.put("status", account.getStatus());
result.put("verificationNote", account.getVerificationNote());
if (account.getUpdateTime() != null) {
result.put("lastUpdatedAt", account.getUpdateTime().toString());
} else if (account.getCreateTime() != null) {
result.put("lastUpdatedAt", account.getCreateTime().toString());
}
} else {
result.put("status", "not_bound");
}
return success(result);
}
/**
* 获取Gitea服务器信息静态配置
*/
@Operation(summary = "获取Gitea服务器信息")
@GetMapping("/gitea-info")
public ApiResult<Map<String, Object>> getGiteaInfo() {
// 这里返回静态配置,后续可以从配置文件中读取
Map<String, Object> info = new HashMap<>();
info.put("url", "https://git.websoft.top");
info.put("version", "1.21");
info.put("registrationEnabled", true);
info.put("requireEmailConfirmation", false);
info.put("maxRepoCreation", 100);
return success(info);
}
// ========== 管理端接口 ==========
/**
* 分页查询Git账号绑定列表管理端
*/
@Operation(summary = "分页查询Git账号绑定列表管理端")
@GetMapping("/git-account/list")
public ApiResult<Map<String, Object>> pageGitAccounts(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String status,
@RequestParam(required = false) String keyword) {
Map<String, Object> result = appGitAccountService.pageGitAccounts(status, keyword, page, size);
return success(result);
}
/**
* 审核Git账号绑定 - 通过
*/
@OperationLog
@Operation(summary = "审核Git账号绑定-通过")
@PutMapping("/git-account/{id}/approve")
public ApiResult<?> approveAccount(@PathVariable("id") Long id, @RequestBody(required = false) Map<String, String> params) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
try {
String note = params != null ? params.get("note") : null;
boolean result = appGitAccountService.approveAccount(id, loginUser.getUserId(), loginUser.getRealName(), note);
return result ? success("审核通过") : fail("审核失败");
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
/**
* 审核Git账号绑定 - 拒绝
*/
@OperationLog
@Operation(summary = "审核Git账号绑定-拒绝")
@PutMapping("/git-account/{id}/reject")
public ApiResult<?> rejectAccount(@PathVariable("id") Long id, @RequestBody Map<String, String> params) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
String reason = params != null ? params.get("reason") : null;
if (reason == null || reason.trim().isEmpty()) {
return fail("请填写拒绝原因");
}
try {
boolean result = appGitAccountService.rejectAccount(id, loginUser.getUserId(), loginUser.getRealName(), reason.trim());
return result ? success("已拒绝该绑定申请") : fail("拒绝失败");
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
}

View File

@@ -0,0 +1,74 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import java.io.Serializable;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用 API Key 实体
*
* @author 科技小王子
* @since 2026-04-02
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppApiKey对象", description = "应用API密钥")
public class AppApiKey implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增主键")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "API Key名称")
private String name;
@Schema(description = "API Key密钥值加密存储")
private String apiKey;
@Schema(description = "密钥前缀(用于显示,如 sk-xxxxx")
private String keyPrefix;
@Schema(description = "状态: 0正常, 1禁用")
private Integer status;
@Schema(description = "权限范围JSON数组字符串")
private String scopes;
@Schema(description = "到期时间NULL=永不过期")
private LocalDateTime expireTime;
@Schema(description = "最后使用时间")
private LocalDateTime lastUsedAt;
@Schema(description = "使用次数")
private Long usageCount;
@Schema(description = "备注")
private String remark;
@Schema(description = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,100 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 平台文章
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_article")
@Schema(name = "AppArticle对象", description = "平台文章")
public class AppArticle implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "文章ID")
@TableId(value = "article_id", type = IdType.AUTO)
private Integer articleId;
@Schema(description = "文章标题")
private String title;
@Schema(description = "文章类型 0常规 1视频")
private Integer type;
@Schema(description = "文章模型article 普通文章announcement 公告")
private String model;
@Schema(description = "文章编号")
private String code;
@Schema(description = "分类ID")
private Integer categoryId;
@Schema(description = "分类名称")
@TableField(exist = false)
private String categoryName;
@Schema(description = "父级分类ID")
@TableField(exist = false)
private Integer parentId;
@Schema(description = "封面图")
private String image;
@Schema(description = "来源")
private String source;
@Schema(description = "摘要")
private String overview;
@Schema(description = "正文内容")
private String content;
@Schema(description = "实际阅读量")
private Integer actualViews;
@Schema(description = "点赞数")
private Integer likes;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "作者")
private String author;
@Schema(description = "是否推荐")
private Integer recommend;
@Schema(description = "排序值")
private Integer sortNumber;
@Schema(description = "状态0已发布 1草稿/待审核 2已驳回 3违规")
private Integer status;
@Schema(description = "是否删除0否 1是")
@TableLogic
private Integer deleted;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,87 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 平台文章分类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_article_category")
@Schema(name = "AppArticleCategory对象", description = "平台文章分类")
public class AppArticleCategory implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "分类ID")
@TableId(value = "category_id", type = IdType.AUTO)
private Integer categoryId;
@Schema(description = "分类标识")
private String categoryCode;
@Schema(description = "分类名称")
private String title;
@Schema(description = "类型 0列表 1单页 2外链")
private Integer type;
@Schema(description = "分类图片")
private String image;
@Schema(description = "上级分类ID")
private Integer parentId;
@Schema(description = "访问路径")
private String path;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "文章数")
@TableField(exist = false)
private Integer count;
@Schema(description = "排序值")
private Integer sortNumber;
@Schema(description = "备注")
private String comments;
@Schema(description = "是否隐藏")
private Integer hide;
@Schema(description = "是否推荐")
private Integer recommend;
@Schema(description = "是否显示在首页")
private Integer showIndex;
@Schema(description = "状态 0正常 1禁用")
private Integer status;
@Schema(description = "是否删除0否 1是")
@TableLogic
private Integer deleted;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,118 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* CI/CD 构建记录实体
*
* @author 科技小王子
* @since 2026-04-03
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppBuild对象", description = "CI/CD构建记录")
@TableName("app_build")
public class AppBuild implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "关联应用ID")
private Long appId;
@Schema(description = "关联版本ID可选")
private Long versionId;
@Schema(description = "构建编号(如 run-123")
private String buildNumber;
@Schema(description = "构建名称")
private String name;
@Schema(description = "触发分支(如 main、develop")
private String branch;
@Schema(description = "提交哈希(可选)")
private String commitSha;
@Schema(description = "提交消息(可选)")
private String commitMessage;
@Schema(description = "提交作者(可选)")
private String commitAuthor;
@Schema(description = "CI系统类型: gitea/jenkins/github")
private String ciType;
@Schema(description = "CI系统中的任务ID")
private String ciJobId;
@Schema(description = "CI系统中的运行ID")
private String ciRunId;
@Schema(description = "CI系统API地址")
private String ciApiUrl;
@Schema(description = "状态: pending/running/success/failed/cancelled")
private String status;
@Schema(description = "构建开始时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startedAt;
@Schema(description = "构建结束时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime finishedAt;
@Schema(description = "构建耗时(秒)")
private Integer duration;
@Schema(description = "构建日志URL")
private String logUrl;
@Schema(description = "构建产物URL如 JAR、Docker镜像")
private String artifactUrl;
@Schema(description = "构建产物名称")
private String artifactName;
@Schema(description = "构建产物大小(字节)")
private Long artifactSize;
@Schema(description = "失败原因")
private String errorMessage;
@Schema(description = "触发方式: manual/webhook/schedule")
private String triggerType;
@Schema(description = "触发人用户ID")
private Integer triggeredBy;
@Schema(description = "扩展配置(JSON)")
private String config;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,74 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 云账号凭证(阿里云/腾讯云/华为云等)
*
* @author 科技小王子
* @since 2026-04-04
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_cloud_credential")
@Schema(name = "AppCloudCredential对象", description = "云账号凭证")
public class AppCloudCredential implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 云服务商: aliyun/tencent/huawei/qiniu */
private String provider;
/** 凭证名称 */
private String name;
/** 访问密钥 ID (AK) */
private String accessKeyId;
/** 访问密钥密钥 (SK)AES加密存储 */
private String accessKeySecret;
/** 额外配置JSON格式 */
private String configJson;
/** 状态: 0正常 1冻结 */
private Integer status;
/** 备注 */
private String remark;
/** 所属用户ID */
private Integer userId;
/** 租户ID */
private Integer tenantId;
@Schema(description = "是否删除: 0否 1是")
private Integer deleted;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
/** 测试状态: 0未测试 1成功 2失败 */
private Integer testStatus;
/** 测试消息 */
private String testMessage;
}

View File

@@ -0,0 +1,87 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 应用配置表
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_config")
public class AppConfig implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 配置ID
*/
@TableId(value = "config_id", type = IdType.AUTO)
private Integer configId;
/**
* 应用ID
*/
private Integer appId;
/**
* 配置键
*/
private String configKey;
/**
* 配置值JSON或字符串
*/
private String configValue;
/**
* 配置类型general/api/callback/wechat/payment/git等
*/
private String configType;
/**
* 是否加密 0否 1是
*/
private Integer isEncrypted;
/**
* 是否敏感信息 0否 1是
*/
private Integer isSecret;
/**
* 配置说明
*/
private String description;
/**
* 排序号
*/
private Integer sortNumber;
/**
* 租户id
*/
private Long tenantId;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Long createdTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updatedTime;
/**
* 是否删除 0否 1是
*/
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,90 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 合同管理
*
* @author 科技小王子
* @since 2026-04-13
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_contract")
@Schema(name = "AppContract对象", description = "合同管理")
public class AppContract implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "合同ID")
@TableId(value = "contract_id", type = IdType.AUTO)
private Long contractId;
@Schema(description = "合同编号")
private String contractNo;
@Schema(description = "合同名称")
private String title;
@Schema(description = "合同类型: service/cooperation/purchase/other")
private String contractType;
@Schema(description = "甲方名称")
private String partyA;
@Schema(description = "乙方名称")
private String partyB;
@Schema(description = "合同金额")
private BigDecimal amount;
@Schema(description = "合同开始日期")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate startDate;
@Schema(description = "合同结束日期")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate endDate;
@Schema(description = "状态: draft/pending/active/expired/terminated")
private String status;
@Schema(description = "合同附件URL")
private String fileUrl;
@Schema(description = "合同附件原始文件名")
private String fileName;
@Schema(description = "备注")
private String remark;
@Schema(description = "创建用户ID")
private Integer userId;
@Schema(description = "创建用户名(冗余)")
private String userName;
@Schema(description = "是否删除: 0否 1是")
private Integer deleted;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Schema(description = "租户ID")
private Integer tenantId;
}

View File

@@ -0,0 +1,77 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableLogic;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用密钥凭证
*
* @author 科技小王子
* @since 2026-03-28 21:29:43
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppCredential对象", description = "应用密钥凭证")
public class AppCredential implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "关联应用IDAppProduct.productId")
private String appId;
@Schema(description = "OAuth Client ID公开格式app_xxxxxxxxxxxx")
private String clientId;
@Schema(description = "OAuth Client Secret加密存储")
private String clientSecret;
@Schema(description = "凭证类型: server/client/webhook")
private String type;
@Schema(description = "权限范围,空格分隔")
private String scopes;
@Schema(description = "到期时间NULL=永不过期")
private LocalDateTime expireTime;
@Schema(description = "最后使用时间")
private LocalDateTime lastUsedAt;
private String remark;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;
@Schema(description = "状态, 0正常, 1冻结")
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,76 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用操作动态
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppEvent对象", description = "应用操作动态")
public class AppEvent implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "关联应用ID")
private Long appId;
@Schema(description = "事件类型: created/published/updated/domain_bound/member_added/status_changed")
private String eventType;
@Schema(description = "事件标题,如已发布")
private String title;
@Schema(description = "详细描述")
private String content;
@Schema(description = "操作人用户ID")
private Long operatorId;
@Schema(description = "操作人名称(冗余)")
private String operator;
@Schema(description = "关联ID如版本ID")
private Long refId;
@Schema(description = "关联类型")
private String refType;
@Schema(description = "扩展数据")
private String extra;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;
@Schema(description = "状态, 0正常, 1冻结")
private Integer status;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,64 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* Git账号绑定开发者Gitea账号
*
* @author 科技小王子
* @since 2026-04-02
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppGitAccount对象", description = "Git账号绑定开发者Gitea账号")
@TableName("app_git_account")
public class AppGitAccount implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "Gitea用户名唯一")
private String username;
@Schema(description = "联系邮箱")
private String email;
@Schema(description = "备注")
private String remark;
@Schema(description = "状态: pending待审核/verified已通过/rejected已拒绝")
private String status;
@Schema(description = "审核备注")
private String verificationNote;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,64 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 应用邀请Token实体
*/
@Data
@TableName("app_invite_token")
public class AppInviteToken {
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 邀请token
*/
private String token;
/**
* 应用ID
*/
private Integer appId;
/**
* 邀请角色
*/
private String role;
/**
* 邀请人ID
*/
private Integer inviterId;
/**
* 过期时间
*/
private LocalDateTime expireTime;
/**
* 状态0-未使用1-已使用
*/
private Integer status;
/**
* 接受人ID
*/
private Integer acceptUserId;
/**
* 接受时间
*/
private LocalDateTime acceptTime;
/**
* 创建时间
*/
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,74 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 站内消息通知
*
* @author 科技小王子
* @since 2026-04-03
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_notification")
@Schema(name = "AppNotification对象", description = "站内消息通知")
public class AppNotification implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "通知ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "接收用户ID")
private Integer userId;
@Schema(description = "通知类型: ticket/review/system/resource/permission/member/payment")
private String type;
@Schema(description = "通知标题")
private String title;
@Schema(description = "通知内容摘要")
private String content;
@Schema(description = "是否已读: 0未读 1已读")
private Integer isRead;
@Schema(description = "关联业务ID如工单ID、权限申请ID等")
private Long refId;
@Schema(description = "关联业务类型(如 ticket、permission_request 等)")
private String refType;
@Schema(description = "跳转链接")
private String linkUrl;
@Schema(description = "发送者ID系统通知为0")
private Integer senderId;
@Schema(description = "发送者名称")
private String senderName;
@Schema(description = "发送者头像")
private String senderAvatar;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,77 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 权限申请记录开发者Git仓库访问申请
*
* @author 科技小王子
* @since 2026-04-03
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppPermissionRequest对象", description = "权限申请记录开发者Git仓库访问申请")
@TableName("app_permission_request")
public class AppPermissionRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "Git用户名")
private String gitUsername;
@Schema(description = "申请仓库路径")
private String repo;
@Schema(description = "仓库名称")
private String repoName;
@Schema(description = "申请理由")
private String reason;
@Schema(description = "状态: pending待审核/approved已通过/rejected已拒绝")
private String status;
@Schema(description = "拒绝原因")
private String rejectReason;
@Schema(description = "审核人用户ID")
private Integer reviewerId;
@Schema(description = "审核人姓名")
private String reviewerName;
@Schema(description = "审核时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime reviewedAt;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Schema(description = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;
@Schema(description = "租户ID")
private Integer tenantId;
}

View File

@@ -0,0 +1,99 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* CI/CD 流水线配置实体
*
* @author 科技小王子
* @since 2026-04-03
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppPipeline对象", description = "CI/CD流水线配置")
@TableName("app_pipeline")
public class AppPipeline implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "关联应用ID")
private Long appId;
@Schema(description = "流水线名称")
private String name;
@Schema(description = "流水线描述")
private String description;
@Schema(description = "CI系统类型: gitea/jenkins/github")
private String ciType;
@Schema(description = "Gitea仓库全称如 gxwebsoft/my-app")
private String repoFullName;
@Schema(description = "Gitea工作流文件名如 build.yml")
private String workflowFile;
@Schema(description = "流水线阶段: build/test/deploy")
private String stages;
@Schema(description = "环境: development/staging/production")
private String env;
@Schema(description = "默认触发分支")
private String defaultBranch;
@Schema(description = "是否启用")
private Boolean enabled;
@Schema(description = "自动部署")
private Boolean autoDeploy;
@Schema(description = "构建超时时间(秒)")
private Integer timeout;
@Schema(description = "配置JSON变量、环境等")
private String config;
@Schema(description = "最近一次构建ID")
private Long lastBuildId;
@Schema(description = "最近构建状态")
private String lastBuildStatus;
@Schema(description = "最近构建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime lastBuildTime;
@Schema(description = "构建成功次数")
private Integer successCount;
@Schema(description = "构建失败次数")
private Integer failureCount;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,321 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 应用产品主表
*
* @author 科技小王子
* @since 2024-09-10
*/
@Data
@Schema(name = "AppProduct对象", description = "应用产品主表")
public class AppProduct implements Serializable {
private static final long serialVersionUID = 1L;
// ==================== 核心字段 ====================
@Schema(description = "应用ID")
@TableId(value = "product_id", type = IdType.AUTO)
private Integer productId;
@Schema(description = "应用名称")
@Size(max = 100, message = "应用名称长度不能超过100")
private String productName;
@Schema(description = "应用标识(唯一)")
@Size(max = 50, message = "应用标识长度不能超过50")
private String productCode;
@Schema(description = "应用密钥")
private String productSecret;
// ==================== 应用类型 ====================
@Schema(description = "应用类型: 10网站 20微信小程序 30抖音小程序 40百度小程序 50支付宝小程序 60Android 70iOS 80macOS 90Windows 100插件")
private Integer appType;
@Schema(description = "应用类型名称")
@TableField(exist = false)
private String appTypeName;
// ==================== 分类信息 ====================
@Schema(description = "分类ID")
private Integer categoryId;
@Schema(description = "行业类型(父级)")
private String industryParent;
@Schema(description = "行业类型(子级)")
private String industryChild;
// ==================== 基础信息 ====================
@Schema(description = "应用Logo")
private String logo;
@Schema(description = "应用图标")
private String icon;
@Schema(description = "二维码")
private String qrcode;
@Schema(description = "应用截图(JSON数组)")
private String screenshots;
@Schema(description = "应用简介")
@Size(max = 500, message = "应用简介长度不能超过500")
private String description;
@Schema(description = "详细说明")
private String content;
@Schema(description = "关键词")
@Size(max = 200, message = "关键词长度不能超过200")
private String keywords;
// ==================== 配置信息 ====================
@Schema(description = "域名")
private String domain;
@Schema(description = "域名前缀")
private String prefix;
@Schema(description = "包名/AppID")
private String packageName;
@Schema(description = "后台地址")
private String adminUrl;
@Schema(description = "API地址")
private String apiUrl;
@Schema(description = "下载地址")
private String downloadUrl;
// ==================== 版本信息 ====================
@Schema(description = "版本号")
private String version;
@Schema(description = "版本: standard标准版 professional专业版 perpetual永久授权")
private String edition;
@Schema(description = "最低版本要求")
private String minVersion;
// ==================== 价格与交付 ====================
@Schema(description = "定价: free免费 one_time一次性 subscription订阅")
private String priceType;
@Schema(description = "价格(元)")
private BigDecimal price;
@Schema(description = "划线价格")
private BigDecimal linePrice;
@Schema(description = "续费价格")
private BigDecimal renewPrice;
@Schema(description = "交付方式: 1源码 2托管 3授权")
private Integer deliveryMethod;
@Schema(description = "计费方式: 1按年 2按月 3一次性")
private Integer chargingMethod;
@Schema(description = "订阅周期: month/year")
private String subscriptionPeriod;
// ==================== 发布管理 ====================
@Schema(description = "发布状态: developing开发中 pending_review待审核 published已上架 rejected审核未通过 deprecated已下架")
private String publishStatus;
@Schema(description = "发布时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date publishTime;
@Schema(description = "审核时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reviewTime;
@Schema(description = "审核人ID")
private Integer reviewerId;
@Schema(description = "拒绝原因")
private String rejectReason;
// ==================== 统计数据 ====================
@Schema(description = "浏览次数")
private Integer clicks;
@Schema(description = "安装次数")
private Integer installs;
@Schema(description = "下载次数")
private Integer downloads;
@Schema(description = "评分(1-5)")
private BigDecimal rating;
@Schema(description = "点赞数")
private Integer likes;
// ==================== 开发者信息 ====================
@Schema(description = "开发者")
private String developer;
@Schema(description = "开发者电话")
private String developerPhone;
@Schema(description = "开发者邮箱")
private String developerEmail;
// ==================== 运营配置 ====================
@Schema(description = "是否推荐: 0否 1是")
private Integer recommend;
@Schema(description = "是否官方: 0否 1是")
private Integer official;
@Schema(description = "是否上架市场: 0否 1是")
private Integer market;
@Schema(description = "是否显示首页: 0否 1是")
private Integer showIndex;
@Schema(description = "是否可搜索: 0否 1是")
private Integer searchEnabled;
// ==================== 站点配置 ====================
@Schema(description = "模板ID")
private Integer templateId;
@Schema(description = "样式配置JSON")
private String style;
@Schema(description = "扩展配置JSON")
private String config;
@Schema(description = "主题色")
private String themeColor;
@Schema(description = "语言")
private String lang;
// ==================== 备案信息 ====================
@Schema(description = "ICP备案号")
private String icpNo;
@Schema(description = "公安备案号")
private String policeNo;
// ==================== 状态管理 ====================
@Schema(description = "状态: 0未开通 1运行中 2维护中 3已关闭 4已欠费 5违规停机")
private Integer status;
@Schema(description = "状态说明")
private String statusText;
@Schema(description = "运行状态: 0停止 1运行中 2维护中")
private Integer running;
@Schema(description = "是否到期")
@TableField(exist = false)
private Integer expired;
@Schema(description = "剩余天数")
@TableField(exist = false)
private Long expiredDays;
@Schema(description = "即将过期")
@TableField(exist = false)
private Integer soon;
@Schema(description = "到期时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expirationTime;
// ==================== 系统字段 ====================
@Schema(description = "排序号")
private Integer sortNumber;
@Schema(description = "是否删除: 0否 1是")
@TableLogic
private Integer deleted;
@Schema(description = "创建用户ID")
private Integer userId;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
// ==================== 非数据库字段 ====================
@Schema(description = "当前用户在该应用中的角色(仅 accessible 查询返回)")
@TableField(exist = false)
private String myRole;
// ==================== 应用类型常量 ====================
public static final int APP_TYPE_WEBSITE = 10; // 网站
public static final int APP_TYPE_WX_MINI = 20; // 微信小程序
public static final int APP_TYPE_DOUYIN_MINI = 30; // 抖音小程序
public static final int APP_TYPE_BAIDU_MINI = 40; // 百度小程序
public static final int APP_TYPE_ALIPAY_MINI = 50; // 支付宝小程序
public static final int APP_TYPE_ANDROID = 60; // Android APP
public static final int APP_TYPE_IOS = 70; // iOS APP
public static final int APP_TYPE_MACOS = 80; // macOS 应用
public static final int APP_TYPE_WINDOWS = 90; // Windows 应用
public static final int APP_TYPE_PLUGIN = 100; // 插件
// ==================== 发布状态常量 ====================
public static final String PUBLISH_DEVELOPING = "developing"; // 开发中
public static final String PUBLISH_PENDING_REVIEW = "pending_review"; // 待审核
public static final String PUBLISH_PUBLISHED = "published"; // 已上架
public static final String PUBLISH_REJECTED = "rejected"; // 审核未通过
public static final String PUBLISH_DEPRECATED = "deprecated"; // 已下架
// ==================== 价格类型常量 ====================
public static final String PRICE_FREE = "free"; // 免费
public static final String PRICE_ONE_TIME = "one_time"; // 一次性
public static final String PRICE_SUBSCRIPTION = "subscription"; // 订阅
// ==================== 版本常量 ====================
public static final String EDITION_STANDARD = "standard"; // 标准版
public static final String EDITION_PROFESSIONAL = "professional"; // 专业版
public static final String EDITION_PERPETUAL = "perpetual"; // 永久授权
/**
* 获取应用类型名称
*/
public String getAppTypeName() {
if (appType == null) return "未知";
switch (appType) {
case APP_TYPE_WEBSITE: return "网站";
case APP_TYPE_WX_MINI: return "微信小程序";
case APP_TYPE_DOUYIN_MINI: return "抖音小程序";
case APP_TYPE_BAIDU_MINI: return "百度小程序";
case APP_TYPE_ALIPAY_MINI: return "支付宝小程序";
case APP_TYPE_ANDROID: return "Android APP";
case APP_TYPE_IOS: return "iOS APP";
case APP_TYPE_MACOS: return "macOS 应用";
case APP_TYPE_WINDOWS: return "Windows 应用";
case APP_TYPE_PLUGIN: return "插件";
default: return "未知";
}
}
}

View File

@@ -0,0 +1,208 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 开发者资源(服务器/数据库/云存储/域名/SSL证书
*
* @author 科技小王子
* @since 2026-03-31
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_resource")
@Schema(name = "AppResource对象", description = "开发者资源")
public class AppResource implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "资源ID")
@TableId(value = "resource_id", type = IdType.AUTO)
private Long resourceId;
@Schema(description = "资源类型: server/database/storage/domain/ssl")
private String resourceType;
@Schema(description = "资源名称")
private String name;
@Schema(description = "服务商: tencent/aliyun/huawei/other")
private String provider;
@Schema(description = "关联应用ID可选")
private Long appId;
@Schema(description = "关联应用名称(冗余)")
@TableField(exist = false)
private String appName;
// ─── 服务器字段 ───────────────────────────────────────
@Schema(description = "IP地址服务器用")
private String ip;
@Schema(description = "SSH端口服务器用默认22")
private Integer sshPort;
@Schema(description = "SSH用户名服务器用用于远程执行命令")
private String sshUsername;
@Schema(description = "SSH密码AES加密服务器用用于远程执行命令")
private String sshPassword;
@Schema(description = "MySQL端口服务器用默认3306")
private Integer mysqlPort;
@Schema(description = "PostgreSQL端口服务器用默认5432")
private Integer pgPort;
@Schema(description = "1Panel面板端口服务器用默认8888")
private Integer panelPort;
@Schema(description = "1Panel面板用户名服务器用")
private String panelUsername;
@Schema(description = "1Panel面板密码AES加密服务器用")
private String panelPassword;
@Schema(description = "1Panel面板路径前缀服务器用如 /abc123")
private String panelPath;
@Schema(description = "MySQL管理员用户名服务器用用于远程建库")
private String adminUsername;
@Schema(description = "MySQL管理员密码AES加密服务器用用于远程建库")
private String adminPassword;
// ─── 数据库字段 ───────────────────────────────────────
@Schema(description = "数据库类型: MySQL/PostgreSQL/Redis/MongoDB数据库用")
private String dbType;
@Schema(description = "关联服务器资源ID数据库用")
private Long serverResourceId;
@Schema(description = "连接主机地址(数据库用)")
private String host;
@Schema(description = "数据库连接端口根据dbType从服务器资源获取前端显示用")
private Integer port;
@Schema(description = "数据库用户名(数据库用)")
private String dbUsername;
@Schema(description = "数据库密码(数据库用)")
private String dbPassword;
// ─── 云存储字段 ───────────────────────────────────────
@Schema(description = "地区/Region云存储用")
private String region;
@Schema(description = "云账号凭证ID云存储用关联app_cloud_credential")
private Long credentialId;
@Schema(description = "访问权限: public-read/private云存储用")
private String acl;
@Schema(description = "已用空间(字节,云存储用)")
private Long usedBytes;
@Schema(description = "对象数量(云存储用)")
private Integer usedCount;
// ─── 域名字段 ─────────────────────────────────────────
@Schema(description = "域名(域名用)")
private String domain;
@Schema(description = "注册商(域名用)")
private String registrar;
@Schema(description = "是否已备案(域名用)")
private Boolean icp;
@Schema(description = "ICP备案号域名用")
private String icpNo;
@Schema(description = "是否已绑定SSL域名用冗余")
private Boolean sslBound;
// ─── SSL证书字段 ──────────────────────────────────────
@Schema(description = "证书类型: DV/OV/EVSSL用")
private String certType;
@Schema(description = "颁发机构SSL用")
private String issuer;
@Schema(description = "私钥SSL用AES加密存储")
private String privateKey;
@Schema(description = "公钥SSL用")
private String publicKey;
@Schema(description = "证书内容/证书文件SSL用")
private String certificate;
@Schema(description = "证书链SSL用中间证书")
private String certChain;
// ─── Git仓库字段 ───────────────────────────────────────
@Schema(description = "Git仓库路径git用如: websopy/core")
private String gitPath;
@Schema(description = "Git Clone URLgit用")
private String gitCloneUrl;
@Schema(description = "Git Web访问URLgit用Gitea页面地址")
private String gitWebUrl;
@Schema(description = "Git权限级别: read/write/admingit用")
private String gitAccessLevel;
// ─── 通用字段 ─────────────────────────────────────────
@Schema(description = "状态: running/stopped/expired/pending")
private String status;
@Schema(description = "到期时间")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate expireAt;
@Schema(description = "备注")
private String remark;
@Schema(description = "所属用户ID")
private Integer userId;
@Schema(description = "资源创建者userId权限控制基准创建时自动设置")
private Long ownerUserId;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "是否删除: 0否 1是")
private Integer deleted;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
// ─── 协作权限(非持久化,由后端查询时计算) ────────────────────
@TableField(exist = false)
@Schema(description = "当前用户对此资源的访问级别: 0=无权限 1=基础查看 2=连接查看 3=完全权限(Owner)")
private Integer accessLevel;
@TableField(exist = false)
@Schema(description = "当前用户是否为资源Owner")
private Boolean isOwner;
}

View File

@@ -0,0 +1,92 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 平台设置表
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_setting")
public class AppSetting implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 设置ID
*/
@TableId(value = "setting_id", type = IdType.AUTO)
private Integer settingId;
/**
* 设置分类basic/review/market/register/notify/maintenance
*/
private String category;
/**
* 设置项标识
*/
private String settingKey;
/**
* 设置项名称
*/
private String settingName;
/**
* 设置值JSON格式
*/
private String settingValue;
/**
* 设置类型string/number/boolean/json
*/
private String valueType;
/**
* 设置说明
*/
private String description;
/**
* 排序号
*/
private Integer sortNumber;
/**
* 是否启用 0否 1是
*/
private Integer isEnabled;
/**
* 是否公开前端可读0否 1是
*/
private Integer isPublic;
/**
* 租户id
*/
private Long tenantId;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Long createdTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updatedTime;
/**
* 是否删除 0否 1是
*/
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,116 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 应用订阅实体
* 对应表app_subscription
*/
@Data
@TableName("app_subscription")
public class AppSubscription {
@TableId(type = IdType.AUTO)
private Long id;
/** 订阅编号(业务唯一) */
private String subscriptionNo;
/** 购买用户ID */
private Integer userId;
/** 应用产品ID */
private Integer productId;
/** 租户ID */
private Integer tenantId;
/** 订阅状态: pending/active/expired/cancelled */
private String status;
/** 价格类型: free/one_time/subscription */
private String priceType;
/** 原价(单位:元) */
private BigDecimal originalPrice;
/** 实付金额(单位:元) */
private BigDecimal payPrice;
/** 支付方式: 0-余额, 1-微信, 2-支付宝, 12-免费 */
private Integer payType;
/** 支付状态: 0-未支付, 1-已支付 */
private Integer payStatus;
/** 支付时间 */
private LocalDateTime payTime;
/** 第三方交易号 */
private String transactionId;
/** 订阅周期: month/year */
private String subscriptionPeriod;
/** 生效时间 */
private LocalDateTime startTime;
/** 到期时间(订阅型) */
private LocalDateTime expireTime;
/** 是否自动续费 0-否 1-是 */
private Integer autoRenew;
/** 分配的域名 */
private String instanceDomain;
/** 实例管理后台URL */
private String instanceAdminUrl;
/** 实例配置JSON */
private String instanceConfig;
/** 关联的支付订单号 */
private String orderNo;
/** 关联的支付订单ID */
private Long orderId;
/** 备注 */
private String remark;
@TableLogic
private Integer deleted;
private Integer sortNumber;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
// ===== 关联查询字段(非数据库字段) =====
@TableField(exist = false)
private String productName;
@TableField(exist = false)
private String productIcon;
@TableField(exist = false)
private String productLogo;
@TableField(exist = false)
private Integer productAppType;
@TableField(exist = false)
private String productDescription;
@TableField(exist = false)
private String developerName;
}

View File

@@ -0,0 +1,104 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.gxwebsoft.common.core.config.JsonArrayToStringDeserializer;
import com.gxwebsoft.common.core.config.JsonStringToArraySerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 应用工单
*
* @author 科技小王子
* @since 2026-03-30
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_ticket")
@Schema(name = "AppTicket对象", description = "应用工单")
public class AppTicket implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "工单ID")
@TableId(value = "ticket_id", type = IdType.AUTO)
private Long ticketId;
@Schema(description = "工单编号TK-yyyyMMddHHmmss+4位随机")
private String ticketNo;
@Schema(description = "工单标题")
private String title;
@Schema(description = "工单内容描述")
private String content;
@Schema(description = "关联应用ID")
@com.fasterxml.jackson.annotation.JsonProperty("productId")
private Long appId;
@Schema(description = "应用名称(冗余)")
private String appName;
@Schema(description = "工单分类: bug/feature/consultation/complaint/other")
private String category;
@Schema(description = "优先级: low/normal/high/urgent")
private String priority;
@Schema(description = "状态: pending/assigned/processing/resolved/closed/rejected")
private String status;
@Schema(description = "附件JSON数组")
@JsonDeserialize(using = JsonArrayToStringDeserializer.class)
@JsonSerialize(using = JsonStringToArraySerializer.class)
private String attachments;
@Schema(description = "提交人用户ID")
private Integer submitUserId;
@Schema(description = "提交人昵称(冗余)")
private String submitUserName;
@Schema(description = "提交人头像(冗余)")
private String submitUserAvatar;
@Schema(description = "分配的处理人用户ID")
private Integer assigneeId;
@Schema(description = "处理人昵称(冗余)")
private String assigneeName;
@Schema(description = "处理人头像(冗余)")
private String assigneeAvatar;
@Schema(description = "回复数量")
private Integer replyCount;
@Schema(description = "解决时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime resolvedTime;
@Schema(description = "关闭时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime closedTime;
@Schema(description = "是否删除: 0否 1是")
private Integer deleted;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,64 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.gxwebsoft.common.core.config.JsonArrayToStringDeserializer;
import com.gxwebsoft.common.core.config.JsonStringToArraySerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 工单回复
*
* @author 科技小王子
* @since 2026-03-30
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_ticket_reply")
@Schema(name = "AppTicketReply对象", description = "工单回复")
public class AppTicketReply implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "回复ID")
@TableId(value = "reply_id", type = IdType.AUTO)
private Long replyId;
@Schema(description = "关联工单ID")
private Long ticketId;
@Schema(description = "回复内容")
private String content;
@Schema(description = "附件JSON数组")
@JsonDeserialize(using = JsonArrayToStringDeserializer.class)
@JsonSerialize(using = JsonStringToArraySerializer.class)
private String attachments;
@Schema(description = "回复人用户ID")
private Integer userId;
@Schema(description = "回复人昵称(冗余)")
private String userName;
@Schema(description = "回复人头像(冗余)")
private String userAvatar;
@Schema(description = "是否是技术人员/客服: 0否 1是")
private Integer isStaff;
@Schema(description = "是否删除: 0否 1是")
private Integer deleted;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,98 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用成员
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppUser对象", description = "应用成员")
public class AppUser implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "关联应用ID")
private Long appId;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "用户名(冗余)")
@TableField(exist = false)
private String username;
@Schema(description = "昵称(冗余)")
@TableField(exist = false)
private String nickname;
@Schema(description = "头像(冗余)")
@TableField(exist = false)
private String avatar;
@Schema(description = "手机号(冗余,脱敏存储)")
@TableField(exist = false)
private String phone;
@Schema(description = "角色: owner/admin/developer/viewer")
private String role;
@Schema(description = "邀请人用户ID")
private Long inviteBy;
@Schema(description = "加入时间")
private LocalDateTime inviteTime;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;
@Schema(description = "状态, 0正常, 1冻结")
private Integer status;
@Schema(description = "邀请状态: 0-正常(直接加入), 1-待确认, 2-已拒绝")
private Integer inviteStatus;
@Schema(description = "邀请过期时间")
private LocalDateTime inviteExpireTime;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
// ========== 关联字段(非数据库字段)==========
@Schema(description = "应用名称关联app_product")
@TableField(exist = false)
private String productName;
@Schema(description = "应用图标关联app_product")
@TableField(exist = false)
private String icon;
@Schema(description = "应用编码关联app_product")
@TableField(exist = false)
private String productCode;
}

View File

@@ -0,0 +1,59 @@
package com.gxwebsoft.app.entity;
import cn.hutool.core.util.DesensitizedUtil;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 用户信息缓存表
* 用于缓存 sys_user 的常用字段,方便 app 模块其他表关联查询用户信息
*
* @author 科技小王子
* @since 2026-04-03
*/
@Data
@TableName("app_user_cache")
@Schema(name = "AppUserCache对象", description = "用户信息缓存")
public class AppUserCache implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID主键")
@TableId(value = "user_id", type = IdType.INPUT)
private Integer userId;
@Schema(description = "用户名")
private String username;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "头像")
private String avatar;
@Schema(description = "手机号(脱敏存储)")
private String phone;
@Schema(description = "用户状态0正常1冻结")
private Integer status;
@Schema(description = "缓存更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Schema(description = "租户ID")
private Integer tenantId;
/**
* 获取脱敏手机号
*/
public String getMobile() {
return DesensitizedUtil.mobilePhone(this.phone);
}
}

View File

@@ -0,0 +1,85 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用版本发布记录
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppVersion对象", description = "应用版本发布记录")
public class AppVersion implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "关联应用ID")
private Long appId;
@Schema(description = "版本号,如 1.0.0")
private String versionNo;
@Schema(description = "版本名称")
private String versionName;
@Schema(description = "版本更新说明")
private String changelog;
@Schema(description = "安装包地址")
private String packageUrl;
@Schema(description = "包大小(字节)")
private Long packageSize;
@Schema(description = "包MD5/SHA256")
private String packageHash;
@Schema(description = "环境: development/staging/production")
private String env;
@Schema(description = "状态 0=构建中 1=已发布 2=已回滚 3=构建失败")
private Integer status;
@Schema(description = "是否为当前版本")
private Boolean isCurrent;
@Schema(description = "发布人用户ID")
private Long publishBy;
@Schema(description = "发布时间")
private LocalDateTime publishTime;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,39 @@
package com.gxwebsoft.app.entity;
/**
* 资源访问级别枚举
*
* <pre>
* 0 - 无权限:不是应用团队成员
* 1 - 基础查看:所有团队成员,可看名称/IP/端口/状态
* 2 - 连接查看admin/owner 角色,额外可看用户名/Host/连接方式
* 3 - 完全权限资源创建者Owner可看密码/私钥,可编辑/删除
* </pre>
*
* @author 科技小王子
* @since 2026-04-05
*/
public enum ResourceAccessLevel {
NONE(0),
VIEW_BASIC(1),
VIEW_CONNECTION(2),
FULL(3);
private final int value;
ResourceAccessLevel(int value) {
this.value = value;
}
public int getValue() {
return value;
}
/**
* 当前级别是否大于等于指定级别
*/
public boolean atLeast(ResourceAccessLevel level) {
return this.value >= level.value;
}
}

View File

@@ -0,0 +1,34 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gxwebsoft.app.entity.AppApiKey;
import com.gxwebsoft.app.param.AppApiKeyParam;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 应用 API Key Mapper
*
* @author 科技小王子
* @since 2026-04-02
*/
@Mapper
public interface AppApiKeyMapper extends BaseMapper<AppApiKey> {
/**
* 分页查询(关联查询)
*/
IPage<AppApiKey> selectPageRel(Page<AppApiKey> page, @Param("param") AppApiKeyParam param);
/**
* 查询列表(关联查询)
*/
java.util.List<AppApiKey> selectListRel(@Param("param") AppApiKeyParam param);
/**
* 根据ID查询详情关联查询
*/
AppApiKey selectByIdRel(@Param("id") Long id);
}

View File

@@ -0,0 +1,10 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppArticleCategory;
/**
* 平台文章分类 Mapper
*/
public interface AppArticleCategoryMapper extends BaseMapper<AppArticleCategory> {
}

View File

@@ -0,0 +1,10 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppArticle;
/**
* 平台文章 Mapper
*/
public interface AppArticleMapper extends BaseMapper<AppArticle> {
}

View File

@@ -0,0 +1,39 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppBuild;
import com.gxwebsoft.app.param.AppBuildParam;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* CI/CD 构建记录 Mapper
*
* @author 科技小王子
* @since 2026-04-03
*/
@Mapper
public interface AppBuildMapper extends BaseMapper<AppBuild> {
/**
* 查询构建列表(关联应用信息)
*/
List<AppBuild> selectBuildList(@Param("param") AppBuildParam param);
/**
* 查询构建详情(关联应用信息)
*/
AppBuild selectBuildDetail(@Param("id") Long id);
/**
* 查询应用最新构建
*/
AppBuild selectLatestBuild(@Param("appId") Long appId);
/**
* 查询应用构建统计
*/
Integer countBuildsByStatus(@Param("appId") Long appId, @Param("status") String status);
}

View File

@@ -0,0 +1,28 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppCloudCredential;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 云账号凭证 Mapper
*
* @author 科技小王子
* @since 2026-04-04
*/
public interface AppCloudCredentialMapper extends BaseMapper<AppCloudCredential> {
/**
* 分页查询
*/
List<AppCloudCredential> selectPageRel(@Param("page") IPage<AppCloudCredential> page,
@Param("param") AppCloudCredential param);
/**
* 查询列表
*/
List<AppCloudCredential> selectListRel(@Param("param") AppCloudCredential param);
}

View File

@@ -0,0 +1,43 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppConfig;
import com.gxwebsoft.app.param.AppConfigParam;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* 应用配置表 Mapper
*
* @author 科技小王子
*/
@Mapper
public interface AppConfigMapper extends BaseMapper<AppConfig> {
/**
* 批量获取应用配置
*/
List<Map<String, Object>> selectConfigsByAppId(Integer appId);
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<AppConfig>
*/
List<AppConfig> selectPageRel(@Param("page") IPage<AppConfig> page,
@Param("param") AppConfigParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<AppConfig>
*/
List<AppConfig> selectListRel(@Param("param") AppConfigParam param);
}

View File

@@ -0,0 +1,22 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppContract;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 合同管理 Mapper
*
* @author 科技小王子
* @since 2026-04-13
*/
public interface AppContractMapper extends BaseMapper<AppContract> {
/**
* 查询当天最大合同序号
*/
@Select("SELECT COALESCE(MAX(SUBSTRING_INDEX(contract_no, '-', -1) + 0), 0) " +
"FROM app_contract WHERE contract_no LIKE CONCAT('HT-', #{datePart}, '-%')")
Integer selectMaxSeqForDate(@Param("datePart") String datePart);
}

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppCredential;
import com.gxwebsoft.app.param.AppCredentialParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 应用密钥凭证Mapper
*
* @author 科技小王子
* @since 2026-03-28 21:29:43
*/
public interface AppCredentialMapper extends BaseMapper<AppCredential> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<AppCredential>
*/
List<AppCredential> selectPageRel(@Param("page") IPage<AppCredential> page,
@Param("param") AppCredentialParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<AppCredential> selectListRel(@Param("param") AppCredentialParam param);
}

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppEvent;
import com.gxwebsoft.app.param.AppEventParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 应用操作动态Mapper
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
public interface AppEventMapper extends BaseMapper<AppEvent> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<AppEvent>
*/
List<AppEvent> selectPageRel(@Param("page") IPage<AppEvent> page,
@Param("param") AppEventParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<AppEvent> selectListRel(@Param("param") AppEventParam param);
}

View File

@@ -0,0 +1,36 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppGitAccount;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* Git账号绑定 Mapper
*
* @author 科技小王子
* @since 2026-04-02
*/
@Mapper
public interface AppGitAccountMapper extends BaseMapper<AppGitAccount> {
/**
* 根据用户ID查询
*/
AppGitAccount selectByUserId(@Param("userId") Integer userId);
/**
* 根据用户名查询(检查唯一性)
*/
AppGitAccount selectByUsername(@Param("username") String username);
/**
* 检查用户名是否被其他用户绑定(同一租户下)
*/
AppGitAccount checkUsernameOccupied(@Param("username") String username, @Param("excludeUserId") Integer excludeUserId, @Param("tenantId") Integer tenantId);
/**
* 根据用户ID和租户ID查询
*/
AppGitAccount selectByUserIdAndTenantId(@Param("userId") Integer userId, @Param("tenantId") Integer tenantId);
}

View File

@@ -0,0 +1,18 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppInviteToken;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 应用邀请Token Mapper
*/
public interface AppInviteTokenMapper extends BaseMapper<AppInviteToken> {
/**
* 根据token查询
*/
@Select("SELECT * FROM app_invite_token WHERE token = #{token} LIMIT 1")
AppInviteToken selectByToken(@Param("token") String token);
}

View File

@@ -0,0 +1,10 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppNotification;
/**
* 站内消息通知 Mapper
*/
public interface AppNotificationMapper extends BaseMapper<AppNotification> {
}

View File

@@ -0,0 +1,29 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppPermissionRequest;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* 权限申请Mapper
*
* @author 科技小王子
* @since 2026-04-03
*/
@Mapper
public interface AppPermissionRequestMapper extends BaseMapper<AppPermissionRequest> {
/**
* 获取用户已可访问的仓库列表
*/
List<String> getAccessibleRepos(@Param("userId") Integer userId);
/**
* 获取所有可用仓库列表(含访问状态)
*/
List<Map<String, Object>> getAllReposWithAccessStatus(@Param("userId") Integer userId);
}

View File

@@ -0,0 +1,34 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppPipeline;
import com.gxwebsoft.app.param.AppPipelineParam;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* CI/CD 流水线配置 Mapper
*
* @author 科技小王子
* @since 2026-04-03
*/
@Mapper
public interface AppPipelineMapper extends BaseMapper<AppPipeline> {
/**
* 查询流水线列表
*/
List<AppPipeline> selectPipelineList(@Param("param") AppPipelineParam param);
/**
* 查询流水线详情
*/
AppPipeline selectPipelineDetail(@Param("id") Long id);
/**
* 查询应用的所有流水线
*/
List<AppPipeline> selectByAppId(@Param("appId") Long appId);
}

View File

@@ -0,0 +1,88 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gxwebsoft.app.entity.AppProduct;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* 应用产品 Mapper 接口
*
* @author 科技小王子
*/
@Mapper
public interface AppProductMapper extends BaseMapper<AppProduct> {
/**
* 分页查询应用列表
* @param userId 用户ID可选为空则查所有
*/
IPage<AppProduct> selectPageList(Page<AppProduct> page, @Param("ew") AppProduct product, @Param("userId") Integer userId);
/**
* 查询用户的应用列表(创建者)
*/
List<AppProduct> selectByUserId(@Param("userId") Integer userId);
/**
* 分页查询用户创建的应用
*/
IPage<AppProduct> selectPageByUserId(Page<AppProduct> page, @Param("userId") Integer userId);
/**
* 分页查询用户参与的应用
*/
IPage<AppProduct> selectPageJoinedApps(Page<AppProduct> page, @Param("userId") Integer userId);
/**
* 根据应用标识查询
*/
AppProduct selectByCode(@Param("code") String code);
/**
* 更新发布状态
*/
int updatePublishStatus(@Param("productId") Integer productId, @Param("status") String status);
/**
* 增加浏览次数
*/
int incrementClicks(@Param("productId") Integer productId);
/**
* 增加安装次数
*/
int incrementInstalls(@Param("productId") Integer productId);
/**
* 增加下载次数
*/
int incrementDownloads(@Param("productId") Integer productId);
/**
* 增加点赞数
*/
int incrementLikes(@Param("productId") Integer productId);
/**
* 按用户ID列表批量统计应用数量
*
* @param userIds 用户ID列表
* @return 统计结果列表,每项包含 userId, totalCount, publishedCount
*/
List<Map> selectStatsByUserIds(@Param("userIds") List<Integer> userIds);
/**
* 查询用户可访问的应用列表(带 myRole 字段)
* 包含用户创建的应用owner+ 被邀请加入的应用(成员角色)
*
* @param userId 用户ID
* @return 应用列表(带 myRole 字段)
*/
List<AppProduct> selectAccessibleApps(@Param("userId") Integer userId);
}

View File

@@ -0,0 +1,49 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppResource;
import com.gxwebsoft.app.param.AppResourceParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 开发者资源 Mapper
*
* @author 科技小王子
* @since 2026-03-31
*/
public interface AppResourceMapper extends BaseMapper<AppResource> {
/**
* 分页查询(关联应用名称)
*/
List<AppResource> selectPageRel(@Param("page") IPage<AppResource> page,
@Param("param") AppResourceParam param);
/**
* 查询全部列表(关联应用名称)
*/
List<AppResource> selectListRel(@Param("param") AppResourceParam param);
/**
* 根据用户查询有权限的资源(包括协作资源)
* 查询条件owner_user_id = #{userId} OR app_id IN (用户有权限的应用ID列表)
*/
List<AppResource> selectListByUserAccess(@Param("param") AppResourceParam param,
@Param("userAppIds") List<Long> userAppIds);
/**
* 统计各类型资源数量(仅个人资源),返回 [{resourceType, cnt}]
*/
List<java.util.Map<String, Object>> countByType(@Param("userId") Integer userId,
@Param("tenantId") Integer tenantId);
/**
* 统计各类型资源数量(包含协作者有权限的资源)
*/
List<java.util.Map<String, Object>> countByTypeForUser(@Param("userId") Integer userId,
@Param("tenantId") Integer tenantId,
@Param("userAppIds") List<Long> userAppIds);
}

View File

@@ -0,0 +1,27 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppSetting;
import com.gxwebsoft.app.param.AppSettingParam;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 平台设置表 Mapper
*/
@Mapper
public interface AppSettingMapper extends BaseMapper<AppSetting> {
/**
* 分页查询
*/
IPage<AppSetting> selectPageRel(IPage<AppSetting> page, @Param("param") AppSettingParam param);
/**
* 查询列表
*/
List<AppSetting> selectListRel(@Param("param") AppSettingParam param);
}

View File

@@ -0,0 +1,9 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppSubscription;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AppSubscriptionMapper extends BaseMapper<AppSubscription> {
}

View File

@@ -0,0 +1,19 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppTicket;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 应用工单 Mapper
*/
public interface AppTicketMapper extends BaseMapper<AppTicket> {
/**
* 查询当天最大序号ticket_no 格式TK-YYMMDD-XXXX
*/
@Select("SELECT COALESCE(MAX(SUBSTRING(ticket_no, 13, 4) + 0), 0) " +
"FROM app_ticket WHERE ticket_no LIKE CONCAT('TK-', #{datePart}, '-%')")
Integer selectMaxSeqForDate(@Param("datePart") String datePart);
}

View File

@@ -0,0 +1,10 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppTicketReply;
/**
* 工单回复 Mapper
*/
public interface AppTicketReplyMapper extends BaseMapper<AppTicketReply> {
}

View File

@@ -0,0 +1,15 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppUserCache;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户缓存 Mapper
*
* @author 科技小王子
* @since 2026-04-03
*/
@Mapper
public interface AppUserCacheMapper extends BaseMapper<AppUserCache> {
}

View File

@@ -0,0 +1,46 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppUser;
import com.gxwebsoft.app.param.AppUserParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 应用成员Mapper
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
public interface AppUserMapper extends BaseMapper<AppUser> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<AppUser>
*/
List<AppUser> selectPageRel(@Param("page") IPage<AppUser> page,
@Param("param") AppUserParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<AppUser> selectListRel(@Param("param") AppUserParam param);
/**
* 查询用户参与的应用列表(含角色信息),用于 check-access 和 accessible 接口
* 返回appId, productName, productCode, icon, role, isOwner
*
* @param userId 用户ID
* @return 用户参与的应用及角色信息列表
*/
List<java.util.Map<String, Object>> selectUserAccessibleApps(@Param("userId") Integer userId);
}

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppVersion;
import com.gxwebsoft.app.param.AppVersionParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 应用版本发布记录Mapper
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
public interface AppVersionMapper extends BaseMapper<AppVersion> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<AppVersion>
*/
List<AppVersion> selectPageRel(@Param("page") IPage<AppVersion> page,
@Param("param") AppVersionParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<AppVersion> selectListRel(@Param("param") AppVersionParam param);
}

View File

@@ -0,0 +1,86 @@
package com.gxwebsoft.app.mapper;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.entity.UserBalanceLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.math.BigDecimal;
import java.util.List;
/**
* 跨库查询 Mapper
* 用于从 gxwebsoft_core 库查询 sys_user 表
*/
@Mapper
public interface SysUserCrossDbMapper {
/**
* 根据手机号查询用户
*/
@Select("SELECT user_id AS userId, username, nickname, avatar, phone, email, status, tenant_id AS tenantId, balance " +
"FROM gxwebsoft_core.sys_user WHERE phone = #{phone} AND deleted = 0 LIMIT 1")
User selectByPhone(@Param("phone") String phone);
/**
* 根据用户ID查询用户
*/
@Select("SELECT user_id AS userId, username, nickname, avatar, phone, email, status, tenant_id AS tenantId, balance " +
"FROM gxwebsoft_core.sys_user WHERE user_id = #{userId} AND deleted = 0 LIMIT 1")
User selectByUserId(@Param("userId") Integer userId);
/**
* 搜索用户(按手机号、用户名、昵称模糊搜索)
*/
@Select("<script>" +
"SELECT user_id AS userId, username, nickname, avatar, phone, email, status, tenant_id AS tenantId, balance " +
"FROM gxwebsoft_core.sys_user " +
"WHERE deleted = 0 " +
"AND (phone LIKE CONCAT('%', #{keyword}, '%') " +
" OR username LIKE CONCAT('%', #{keyword}, '%') " +
" OR nickname LIKE CONCAT('%', #{keyword}, '%')) " +
"LIMIT 20" +
"</script>")
List<User> searchUsers(@Param("keyword") String keyword);
/**
* 查询用户类型type字段0普通用户 1企业用户 2开发者用户
*/
@Select("SELECT type FROM gxwebsoft_core.sys_user WHERE user_id = #{userId} AND deleted = 0 LIMIT 1")
Integer selectUserType(@Param("userId") Integer userId);
/**
* 更新用户余额(跨库更新 gxwebsoft_core.sys_user
*/
@Update("UPDATE gxwebsoft_core.sys_user SET balance = #{balance} WHERE user_id = #{userId} AND deleted = 0")
int updateBalance(@Param("userId") Integer userId, @Param("balance") BigDecimal balance);
/**
* 根据用户ID更新跨库更新 gxwebsoft_core.sys_user
*/
@Update("<script>" +
"UPDATE gxwebsoft_core.sys_user SET " +
"<if test='balance != null'>balance = #{balance},</if>" +
"update_time = NOW() " +
"WHERE user_id = #{userId} AND deleted = 0" +
"</script>")
int updateByUserId(@Param("userId") Integer userId, @Param("balance") BigDecimal balance);
/**
* 插入余额日志(跨库插入 gxwebsoft_core.sys_user_balance_log
*/
@Insert("INSERT INTO gxwebsoft_core.sys_user_balance_log " +
"(user_id, scene, money, balance, order_no, comments, tenant_id, create_time) " +
"VALUES (#{userId}, #{scene}, #{money}, #{balance}, #{orderNo}, #{comments}, #{tenantId}, NOW())")
int insertBalanceLog(@Param("userId") Integer userId,
@Param("scene") Integer scene,
@Param("money") BigDecimal money,
@Param("balance") BigDecimal balance,
@Param("orderNo") String orderNo,
@Param("comments") String comments,
@Param("tenantId") Integer tenantId);
}

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppApiKeyMapper">
<resultMap id="BaseResultMap" type="com.gxwebsoft.app.entity.AppApiKey">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="api_key" property="apiKey"/>
<result column="key_prefix" property="keyPrefix"/>
<result column="status" property="status"/>
<result column="scopes" property="scopes"/>
<result column="expire_time" property="expireTime"/>
<result column="last_used_at" property="lastUsedAt"/>
<result column="usage_count" property="usageCount"/>
<result column="remark" property="remark"/>
<result column="deleted" property="deleted"/>
<result column="user_id" property="userId"/>
<result column="tenant_id" property="tenantId"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<sql id="Base_Column_List">
id, name, api_key, key_prefix, status, scopes, expire_time,
last_used_at, usage_count, remark, deleted, user_id, tenant_id,
create_time, update_time
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM app_api_key
WHERE deleted = 0
<if test="param.name != null and param.name != ''">
AND name LIKE CONCAT('%', #{param.name}, '%')
</if>
<if test="param.status != null">
AND status = #{param.status}
</if>
<if test="param.userId != null">
AND user_id = #{param.userId}
</if>
<if test="param.tenantId != null">
AND tenant_id = #{param.tenantId}
</if>
ORDER BY create_time DESC
</select>
<!-- 列表查询 -->
<select id="selectListRel" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM app_api_key
WHERE deleted = 0
<if test="param.name != null and param.name != ''">
AND name LIKE CONCAT('%', #{param.name}, '%')
</if>
<if test="param.status != null">
AND status = #{param.status}
</if>
<if test="param.userId != null">
AND user_id = #{param.userId}
</if>
<if test="param.tenantId != null">
AND tenant_id = #{param.tenantId}
</if>
ORDER BY create_time DESC
</select>
<!-- 根据ID查询 -->
<select id="selectByIdRel" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM app_api_key
WHERE id = #{id} AND deleted = 0
</select>
</mapper>

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppBuildMapper">
<resultMap id="BaseResultMap" type="com.gxwebsoft.app.entity.AppBuild">
<id column="id" property="id"/>
<result column="app_id" property="appId"/>
<result column="version_id" property="versionId"/>
<result column="build_number" property="buildNumber"/>
<result column="name" property="name"/>
<result column="branch" property="branch"/>
<result column="commit_sha" property="commitSha"/>
<result column="commit_message" property="commitMessage"/>
<result column="commit_author" property="commitAuthor"/>
<result column="ci_type" property="ciType"/>
<result column="ci_job_id" property="ciJobId"/>
<result column="ci_run_id" property="ciRunId"/>
<result column="ci_api_url" property="ciApiUrl"/>
<result column="status" property="status"/>
<result column="started_at" property="startedAt"/>
<result column="finished_at" property="finishedAt"/>
<result column="duration" property="duration"/>
<result column="log_url" property="logUrl"/>
<result column="artifact_url" property="artifactUrl"/>
<result column="artifact_name" property="artifactName"/>
<result column="artifact_size" property="artifactSize"/>
<result column="error_message" property="errorMessage"/>
<result column="trigger_type" property="triggerType"/>
<result column="triggered_by" property="triggeredBy"/>
<result column="config" property="config"/>
<result column="user_id" property="userId"/>
<result column="tenant_id" property="tenantId"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<sql id="Base_Column_List">
id, app_id, version_id, build_number, name, branch, commit_sha, commit_message,
commit_author, ci_type, ci_job_id, ci_run_id, ci_api_url, status, started_at,
finished_at, duration, log_url, artifact_url, artifact_name, artifact_size,
error_message, trigger_type, triggered_by, config, user_id, tenant_id,
create_time, update_time
</sql>
<select id="selectBuildList" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM app_build
WHERE deleted = 0
<if test="param.appId != null">
AND app_id = #{param.appId}
</if>
<if test="param.userId != null">
AND user_id = #{param.userId}
</if>
<if test="param.status != null and param.status != ''">
AND status = #{param.status}
</if>
<if test="param.ciType != null and param.ciType != ''">
AND ci_type = #{param.ciType}
</if>
<if test="param.branch != null and param.branch != ''">
AND branch = #{param.branch}
</if>
ORDER BY create_time DESC
</select>
<select id="selectBuildDetail" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM app_build
WHERE id = #{id} AND deleted = 0
</select>
<select id="selectLatestBuild" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM app_build
WHERE app_id = #{appId} AND deleted = 0
ORDER BY create_time DESC
LIMIT 1
</select>
<select id="countBuildsByStatus" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM app_build
WHERE app_id = #{appId} AND status = #{status} AND deleted = 0
</select>
</mapper>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppCloudCredentialMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*
FROM app_cloud_credential a
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.provider != null and param.provider != ''">
AND a.provider = #{param.provider}
</if>
<if test="param.name != null and param.name != ''">
AND a.name LIKE CONCAT('%', #{param.name}, '%')
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.deleted != null">
AND a.deleted = #{param.deleted}
</if>
<if test="param.deleted == null">
AND a.deleted = 0
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppCloudCredential">
<include refid="selectSql"></include>
ORDER BY a.id DESC
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppCloudCredential">
<include refid="selectSql"></include>
ORDER BY a.id DESC
</select>
</mapper>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppConfigMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*
FROM app_config a
<where>
a.deleted = 0
<if test="param.configId != null">
AND a.config_id = #{param.configId}
</if>
<if test="param.appId != null">
AND a.app_id = #{param.appId}
</if>
<if test="param.configKey != null and param.configKey != ''">
AND a.config_key LIKE CONCAT('%', #{param.configKey}, '%')
</if>
<if test="param.configType != null and param.configType != ''">
AND a.config_type = #{param.configType}
</if>
<if test="param.isSecret != null">
AND a.is_secret = #{param.isSecret}
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.keywords != null and param.keywords != ''">
AND a.config_key LIKE CONCAT('%', #{param.keywords}, '%')
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppConfig">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppConfig">
<include refid="selectSql"></include>
</select>
<!-- 批量获取应用配置(自动解密) -->
<select id="selectConfigsByAppId" resultType="java.util.HashMap">
SELECT
config_key as configKey,
config_value as configValue,
config_type as configType,
is_encrypted as isEncrypted,
is_secret as isSecret,
description
FROM app_config
WHERE app_id = #{appId}
AND deleted = 0
ORDER BY config_type, sort_number, config_id
</select>
</mapper>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppCredentialMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*, w.product_name, w.product_code, w.icon
FROM app_credential a
LEFT JOIN app_product w ON a.app_id = w.product_id AND w.deleted = 0
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.appId != null">
AND a.app_id = #{param.appId}
</if>
<if test="param.name != null and param.name != ''">
AND a.name LIKE CONCAT('%', #{param.name}, '%')
</if>
<if test="param.clientId != null and param.clientId != ''">
AND a.client_id = #{param.clientId}
</if>
<if test="param.type != null and param.type != ''">
AND a.type = #{param.type}
</if>
<if test="param.scopes != null and param.scopes != ''">
AND a.scopes LIKE CONCAT('%', #{param.scopes}, '%')
</if>
<if test="param.remark != null and param.remark != ''">
AND a.remark LIKE CONCAT('%', #{param.remark}, '%')
</if>
<if test="param.sortNumber != null">
AND a.sort_number = #{param.sortNumber}
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.deleted != null">
AND a.deleted = #{param.deleted}
</if>
<if test="param.deleted == null">
AND a.deleted = 0
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.keywords != null and param.keywords != ''">
AND (a.name LIKE CONCAT('%', #{param.keywords}, '%')
OR a.client_id LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppCredential">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppCredential">
<include refid="selectSql"></include>
</select>
</mapper>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppEventMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*, w.product_name, w.product_code, w.icon
FROM app_event a
LEFT JOIN app_product w ON a.app_id = w.product_id AND w.deleted = 0
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.appId != null">
AND a.app_id = #{param.appId}
</if>
<if test="param.eventType != null and param.eventType != ''">
AND a.event_type = #{param.eventType}
</if>
<if test="param.title != null and param.title != ''">
AND a.title LIKE CONCAT('%', #{param.title}, '%')
</if>
<if test="param.operatorId != null">
AND a.operator_id = #{param.operatorId}
</if>
<if test="param.refId != null">
AND a.ref_id = #{param.refId}
</if>
<if test="param.refType != null and param.refType != ''">
AND a.ref_type = #{param.refType}
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.keywords != null and param.keywords != ''">
AND (a.title LIKE CONCAT('%', #{param.keywords}, '%')
OR a.content LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
ORDER BY a.create_time DESC
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppEvent">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppEvent">
<include refid="selectSql"></include>
</select>
</mapper>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppGitAccountMapper">
<resultMap id="BaseResultMap" type="com.gxwebsoft.app.entity.AppGitAccount">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
<result column="remark" property="remark"/>
<result column="status" property="status"/>
<result column="verification_note" property="verificationNote"/>
<result column="tenant_id" property="tenantId"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<result column="deleted" property="deleted"/>
</resultMap>
<!-- 根据用户ID查询 -->
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT * FROM app_git_account
WHERE user_id = #{userId} AND deleted = 0
LIMIT 1
</select>
<!-- 根据用户名查询(检查唯一性) -->
<select id="selectByUsername" resultMap="BaseResultMap">
SELECT * FROM app_git_account
WHERE username = #{username} AND deleted = 0
LIMIT 1
</select>
<!-- 检查用户名是否被其他用户绑定(同一租户下) -->
<select id="checkUsernameOccupied" resultMap="BaseResultMap">
SELECT * FROM app_git_account
WHERE username = #{username} AND user_id != #{excludeUserId} AND tenant_id = #{tenantId} AND deleted = 0
LIMIT 1
</select>
<!-- 根据用户ID和租户ID查询 -->
<select id="selectByUserIdAndTenantId" resultMap="BaseResultMap">
SELECT * FROM app_git_account
WHERE user_id = #{userId} AND tenant_id = #{tenantId} AND deleted = 0
LIMIT 1
</select>
</mapper>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppPermissionRequestMapper">
<!-- 获取用户已可访问的仓库列表 -->
<select id="getAccessibleRepos" resultType="java.lang.String">
SELECT DISTINCT repo
FROM app_permission_request
WHERE user_id = #{userId}
AND status = 'approved'
AND deleted = 0
</select>
<!-- 获取所有可用仓库列表(含访问状态) -->
<select id="getAllReposWithAccessStatus" resultType="java.util.Map">
SELECT
r.path AS value,
r.name AS label,
r.description,
r.access_level AS accessLevel,
CASE WHEN pr.repo IS NOT NULL THEN 1 ELSE 0 END AS isAccessible
FROM app_repository r
LEFT JOIN (
SELECT repo
FROM app_permission_request
WHERE user_id = #{userId}
AND status = 'approved'
AND deleted = 0
) pr ON r.path = pr.repo
WHERE r.deleted = 0
ORDER BY r.name
</select>
</mapper>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppPipelineMapper">
<resultMap id="BaseResultMap" type="com.gxwebsoft.app.entity.AppPipeline">
<id column="id" property="id"/>
<result column="app_id" property="appId"/>
<result column="name" property="name"/>
<result column="description" property="description"/>
<result column="ci_type" property="ciType"/>
<result column="repo_full_name" property="repoFullName"/>
<result column="workflow_file" property="workflowFile"/>
<result column="stages" property="stages"/>
<result column="env" property="env"/>
<result column="default_branch" property="defaultBranch"/>
<result column="enabled" property="enabled"/>
<result column="auto_deploy" property="autoDeploy"/>
<result column="timeout" property="timeout"/>
<result column="config" property="config"/>
<result column="last_build_id" property="lastBuildId"/>
<result column="last_build_status" property="lastBuildStatus"/>
<result column="last_build_time" property="lastBuildTime"/>
<result column="success_count" property="successCount"/>
<result column="failure_count" property="failureCount"/>
<result column="user_id" property="userId"/>
<result column="tenant_id" property="tenantId"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<sql id="Base_Column_List">
id, app_id, name, description, ci_type, repo_full_name, workflow_file, stages,
env, default_branch, enabled, auto_deploy, timeout, config, last_build_id,
last_build_status, last_build_time, success_count, failure_count, user_id,
tenant_id, create_time, update_time
</sql>
<select id="selectPipelineList" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM app_pipeline
WHERE deleted = 0
<if test="param.appId != null">
AND app_id = #{param.appId}
</if>
<if test="param.userId != null">
AND user_id = #{param.userId}
</if>
<if test="param.ciType != null and param.ciType != ''">
AND ci_type = #{param.ciType}
</if>
<if test="param.enabled != null">
AND enabled = #{param.enabled}
</if>
ORDER BY create_time DESC
</select>
<select id="selectPipelineDetail" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM app_pipeline
WHERE id = #{id} AND deleted = 0
</select>
<select id="selectByAppId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM app_pipeline
WHERE app_id = #{appId} AND deleted = 0
ORDER BY create_time DESC
</select>
</mapper>

View File

@@ -0,0 +1,247 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppProductMapper">
<resultMap id="BaseResultMap" type="com.gxwebsoft.app.entity.AppProduct">
<id column="product_id" property="productId"/>
<result column="product_name" property="productName"/>
<result column="product_code" property="productCode"/>
<result column="product_secret" property="productSecret"/>
<result column="app_type" property="appType"/>
<result column="category_id" property="categoryId"/>
<result column="industry_parent" property="industryParent"/>
<result column="industry_child" property="industryChild"/>
<result column="logo" property="logo"/>
<result column="icon" property="icon"/>
<result column="qrcode" property="qrcode"/>
<result column="screenshots" property="screenshots"/>
<result column="description" property="description"/>
<result column="content" property="content"/>
<result column="keywords" property="keywords"/>
<result column="domain" property="domain"/>
<result column="prefix" property="prefix"/>
<result column="package_name" property="packageName"/>
<result column="admin_url" property="adminUrl"/>
<result column="api_url" property="apiUrl"/>
<result column="download_url" property="downloadUrl"/>
<result column="version" property="version"/>
<result column="edition" property="edition"/>
<result column="min_version" property="minVersion"/>
<result column="price_type" property="priceType"/>
<result column="price" property="price"/>
<result column="line_price" property="linePrice"/>
<result column="renew_price" property="renewPrice"/>
<result column="delivery_method" property="deliveryMethod"/>
<result column="charging_method" property="chargingMethod"/>
<result column="subscription_period" property="subscriptionPeriod"/>
<result column="publish_status" property="publishStatus"/>
<result column="publish_time" property="publishTime"/>
<result column="review_time" property="reviewTime"/>
<result column="reviewer_id" property="reviewerId"/>
<result column="reject_reason" property="rejectReason"/>
<result column="clicks" property="clicks"/>
<result column="installs" property="installs"/>
<result column="downloads" property="downloads"/>
<result column="rating" property="rating"/>
<result column="likes" property="likes"/>
<result column="developer" property="developer"/>
<result column="developer_phone" property="developerPhone"/>
<result column="developer_email" property="developerEmail"/>
<result column="recommend" property="recommend"/>
<result column="official" property="official"/>
<result column="market" property="market"/>
<result column="show_index" property="showIndex"/>
<result column="search_enabled" property="searchEnabled"/>
<result column="template_id" property="templateId"/>
<result column="style" property="style"/>
<result column="config" property="config"/>
<result column="theme_color" property="themeColor"/>
<result column="lang" property="lang"/>
<result column="icp_no" property="icpNo"/>
<result column="police_no" property="policeNo"/>
<result column="status" property="status"/>
<result column="status_text" property="statusText"/>
<result column="running" property="running"/>
<result column="expiration_time" property="expirationTime"/>
<result column="sort_number" property="sortNumber"/>
<result column="deleted" property="deleted"/>
<result column="user_id" property="userId"/>
<result column="tenant_id" property="tenantId"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<resultMap id="DetailResultMap" type="com.gxwebsoft.app.entity.AppProduct" extends="BaseResultMap">
</resultMap>
<!-- 带 myRole 的结果映射(用于 accessible 接口) -->
<resultMap id="AccessibleResultMap" type="com.gxwebsoft.app.entity.AppProduct" extends="BaseResultMap">
<result column="my_role" property="myRole"/>
</resultMap>
<!-- 分页查询 -->
<!-- 当 userId 不为空时,只返回该用户创建的应用或参与的应用 -->
<select id="selectPageList" resultMap="DetailResultMap">
SELECT DISTINCT p.*
FROM app_product p
<where>
<if test="ew.productName != null and ew.productName != ''">
AND p.product_name LIKE CONCAT('%', #{ew.productName}, '%')
</if>
<if test="ew.productCode != null and ew.productCode != ''">
AND p.product_code = #{ew.productCode}
</if>
<if test="ew.appType != null">
AND p.app_type = #{ew.appType}
</if>
<if test="ew.categoryId != null">
AND p.category_id = #{ew.categoryId}
</if>
<if test="ew.publishStatus != null and ew.publishStatus != ''">
AND p.publish_status = #{ew.publishStatus}
</if>
<if test="ew.status != null">
AND p.status = #{ew.status}
</if>
<if test="ew.userId != null">
AND p.user_id = #{ew.userId}
</if>
<if test="ew.tenantId != null">
AND p.tenant_id = #{ew.tenantId}
</if>
<if test="ew.market != null">
AND p.market = #{ew.market}
</if>
<if test="ew.recommend != null">
AND p.recommend = #{ew.recommend}
</if>
<if test="ew.deleted != null">
AND p.deleted = #{ew.deleted}
</if>
<!-- 如果传入了 userId过滤为该用户创建或参与的应用已确认的成员 -->
<if test="userId != null">
AND (
p.user_id = #{userId}
OR EXISTS (
SELECT 1 FROM app_user au
WHERE au.app_id = p.product_id
AND au.user_id = #{userId}
AND au.status = 0
AND au.invite_status = 0 <!-- 只查询已确认的邀请 -->
)
)
</if>
</where>
ORDER BY p.sort_number ASC, p.create_time DESC
</select>
<!-- 根据用户ID查询 -->
<select id="selectByUserId" resultMap="DetailResultMap">
SELECT p.*
FROM app_product p
WHERE p.user_id = #{userId}
AND p.deleted = 0
ORDER BY p.create_time DESC
</select>
<!-- 分页查询用户创建的应用 -->
<select id="selectPageByUserId" resultMap="DetailResultMap">
SELECT p.*
FROM app_product p
WHERE p.user_id = #{userId}
AND p.deleted = 0
ORDER BY p.sort_number ASC, p.create_time DESC
</select>
<!-- 分页查询用户参与的应用通过app_user关联不是创建者且已确认 -->
<!-- 按 app_user.update_time 排序,实现"最近使用"效果 -->
<select id="selectPageJoinedApps" resultMap="DetailResultMap">
SELECT p.*
FROM app_product p
INNER JOIN app_user au ON p.product_id = au.app_id
WHERE au.user_id = #{userId}
AND au.status = 0
AND au.invite_status = 0 <!-- 只查询已确认的邀请 -->
AND p.deleted = 0
ORDER BY au.update_time DESC, p.sort_number ASC
</select>
<!-- 根据标识查询 -->
<select id="selectByCode" resultMap="DetailResultMap">
SELECT p.*
FROM app_product p
WHERE p.product_code = #{code}
AND p.deleted = 0
</select>
<!-- 更新发布状态 -->
<update id="updatePublishStatus">
UPDATE app_product
SET publish_status = #{status},
update_time = NOW()
<if test="status == 'published'">
, publish_time = NOW()
</if>
<if test="status == 'pending_review'">
, review_time = NOW()
</if>
WHERE product_id = #{productId}
</update>
<!-- 增加浏览次数 -->
<update id="incrementClicks">
UPDATE app_product SET clicks = clicks + 1 WHERE product_id = #{productId}
</update>
<!-- 增加安装次数 -->
<update id="incrementInstalls">
UPDATE app_product SET installs = installs + 1 WHERE product_id = #{productId}
</update>
<!-- 增加下载次数 -->
<update id="incrementDownloads">
UPDATE app_product SET downloads = downloads + 1 WHERE product_id = #{productId}
</update>
<!-- 增加点赞数 -->
<update id="incrementLikes">
UPDATE app_product SET likes = likes + 1 WHERE product_id = #{productId}
</update>
<!-- 按用户ID列表批量统计应用数量一条SQL搞定 -->
<select id="selectStatsByUserIds" resultType="java.util.Map">
SELECT
user_id AS userId,
COUNT(*) AS totalCount,
SUM(CASE WHEN publish_status = 'published' THEN 1 ELSE 0 END) AS publishedCount
FROM app_product
WHERE deleted = 0
AND user_id IN
<foreach collection="userIds" item="uid" open="(" separator="," close=")">
#{uid}
</foreach>
GROUP BY user_id
</select>
<!-- 查询用户可访问的应用列表(带角色信息),用于 accessible 接口 -->
<select id="selectAccessibleApps" resultMap="AccessibleResultMap">
SELECT
p.*,
CASE
WHEN p.user_id = #{userId} THEN 'owner'
ELSE au.role
END AS my_role
FROM app_product p
LEFT JOIN app_user au ON p.product_id = au.app_id
AND au.user_id = #{userId}
AND au.status = 0
AND au.invite_status = 0 <!-- 只查询已确认的邀请 -->
WHERE p.deleted = 0
AND (
p.user_id = #{userId}
OR au.id IS NOT NULL
)
ORDER BY my_role DESC, p.create_time DESC
</select>
</mapper>

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppResourceMapper">
<!-- 关联查询 SQL基础版本仅 owner 资源) -->
<sql id="selectSql">
SELECT a.*, w.product_name as appName
FROM app_resource a
LEFT JOIN app_product w ON a.app_id = w.product_id AND w.deleted = 0
<where>
a.deleted = 0
<if test="param.resourceId != null">
AND a.resource_id = #{param.resourceId}
</if>
<if test="param.resourceType != null and param.resourceType != ''">
AND a.resource_type = #{param.resourceType}
</if>
<if test="param.appId != null">
AND a.app_id = #{param.appId}
</if>
<if test="param.provider != null and param.provider != ''">
AND a.provider = #{param.provider}
</if>
<if test="param.status != null and param.status != ''">
AND a.status = #{param.status}
</if>
<if test="param.userId != null">
<!-- 协作权限升级:改为 owner_user_id 或 app_id 有权限 -->
AND (a.owner_user_id = #{param.userId}
<if test="param.userAppIds != null and param.userAppIds.size > 0">
<!-- 用户有权限的应用ID列表 -->
OR a.app_id IN
<foreach collection="param.userAppIds" item="appId" open="(" separator="," close=")">
#{appId}
</foreach>
</if>
)
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</if>
<if test="param.keywords != null and param.keywords != ''">
AND (
a.name LIKE CONCAT('%', #{param.keywords}, '%')
OR a.ip LIKE CONCAT('%', #{param.keywords}, '%')
OR a.domain LIKE CONCAT('%', #{param.keywords}, '%')
OR a.host LIKE CONCAT('%', #{param.keywords}, '%')
OR w.product_name LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
</where>
ORDER BY a.create_time DESC
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppResource">
<include refid="selectSql"/>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppResource">
<include refid="selectSql"/>
</select>
<!-- 根据用户权限查询资源(可扩展使用) -->
<select id="selectListByUserAccess" resultType="com.gxwebsoft.app.entity.AppResource">
<include refid="selectSql"/>
<!-- 注意:此处已通过 <include> 引入了 where 条件,其中包含 userAppIds 判断 -->
</select>
<!-- 按类型统计数量(仅自己的资源) -->
<select id="countByType" resultType="java.util.Map">
SELECT resource_type AS resourceType, COUNT(*) AS cnt
FROM app_resource
WHERE deleted = 0
<if test="userId != null">
AND owner_user_id = #{userId}
</if>
<if test="tenantId != null">
AND tenant_id = #{tenantId}
</if>
GROUP BY resource_type
</select>
<!-- 按类型统计数量(包含协作资源) -->
<select id="countByTypeForUser" resultType="java.util.Map">
SELECT resource_type AS resourceType, COUNT(*) AS cnt
FROM app_resource
WHERE deleted = 0
AND (
owner_user_id = #{userId}
OR app_id IN
<if test="userAppIds != null and userAppIds.size() > 0">
<!-- 用户有权限的应用ID列表 -->
<foreach collection="userAppIds" item="appId" open="(" separator="," close=")">
#{appId}
</foreach>
</if>
<if test="userAppIds == null or userAppIds.size() == 0">
(-1) <!-- 没有权限的应用,仅计数自己的资源 -->
</if>
)
<if test="tenantId != null">
AND tenant_id = #{tenantId}
</if>
GROUP BY resource_type
</select>
</mapper>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppSettingMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*
FROM app_setting a
<where>
a.deleted = 0
<if test="param.settingId != null">
AND a.setting_id = #{param.settingId}
</if>
<if test="param.category != null and param.category != ''">
AND a.category = #{param.category}
</if>
<if test="param.settingKey != null and param.settingKey != ''">
AND a.setting_key = #{param.settingKey}
</if>
<if test="param.settingName != null and param.settingName != ''">
AND a.setting_name LIKE CONCAT('%', #{param.settingName}, '%')
</if>
<if test="param.valueType != null and param.valueType != ''">
AND a.value_type = #{param.valueType}
</if>
<if test="param.isEnabled != null">
AND a.is_enabled = #{param.isEnabled}
</if>
<if test="param.isPublic != null">
AND a.is_public = #{param.isPublic}
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</if>
<if test="param.keywords != null and param.keywords != ''">
AND (a.setting_key LIKE CONCAT('%', #{param.keywords}, '%')
OR a.setting_name LIKE CONCAT('%', #{param.keywords}, '%'))
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppSetting">
<include refid="selectSql"></include>
ORDER BY a.category, a.sort_number, a.setting_id
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppSetting">
<include refid="selectSql"></include>
ORDER BY a.category, a.sort_number, a.setting_id
</select>
</mapper>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppUserCacheMapper">
<resultMap id="BaseResultMap" type="com.gxwebsoft.app.entity.AppUserCache">
<id column="user_id" property="userId"/>
<result column="username" property="username"/>
<result column="nickname" property="nickname"/>
<result column="avatar" property="avatar"/>
<result column="phone" property="phone"/>
<result column="status" property="status"/>
<result column="update_time" property="updateTime"/>
</resultMap>
</mapper>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppUserMapper">
<!-- 关联查询sql使用 app_user_cache 获取用户信息,避免跨库 JOIN -->
<sql id="selectSql">
SELECT a.*, w.product_name, w.product_code, w.icon,
c.username, c.nickname, c.avatar, c.phone
FROM app_user a
LEFT JOIN app_product w ON a.app_id = w.product_id AND w.deleted = 0 AND w.tenant_id = a.tenant_id
LEFT JOIN app_user_cache c ON a.user_id = c.user_id
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.appId != null">
AND a.app_id = #{param.appId}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.username != null and param.username != ''">
AND (a.username LIKE CONCAT('%', #{param.username}, '%')
OR a.nickname LIKE CONCAT('%', #{param.username}, '%'))
</if>
<if test="param.role != null and param.role != ''">
AND a.role = #{param.role}
</if>
<if test="param.inviteBy != null">
AND a.invite_by = #{param.inviteBy}
</if>
<if test="param.sortNumber != null">
AND a.sort_number = #{param.sortNumber}
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.keywords != null and param.keywords != ''">
AND (a.username LIKE CONCAT('%', #{param.keywords}, '%')
OR a.nickname LIKE CONCAT('%', #{param.keywords}, '%'))
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppUser">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppUser">
<include refid="selectSql"></include>
</select>
<!-- 查询用户参与的应用(含角色信息),用于 check-access 和 accessible 接口 -->
<!-- 包含创建的应用owner+ 被邀请加入的应用(成员角色,且已确认) -->
<select id="selectUserAccessibleApps" resultType="java.util.Map">
SELECT
p.product_id AS appId,
p.product_name AS productName,
p.product_code AS productCode,
p.icon AS icon,
CASE
WHEN p.user_id = #{userId} THEN 'owner'
ELSE au.role
END AS role,
CASE
WHEN p.user_id = #{userId} THEN 1
ELSE 0
END AS isOwner
FROM app_product p
LEFT JOIN app_user au ON p.product_id = au.app_id
AND au.user_id = #{userId}
AND au.status = 0
AND au.invite_status = 0 <!-- 只查询已确认的邀请 -->
WHERE p.deleted = 0
AND (
p.user_id = #{userId}
OR au.id IS NOT NULL
)
ORDER BY isOwner DESC, p.create_time DESC
</select>
</mapper>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.app.mapper.AppVersionMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*, w.product_name, w.product_code, w.icon
FROM app_version a
LEFT JOIN app_product w ON a.app_id = w.product_id AND w.deleted = 0
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.appId != null">
AND a.app_id = #{param.appId}
</if>
<if test="param.versionNo != null and param.versionNo != ''">
AND a.version_no = #{param.versionNo}
</if>
<if test="param.versionName != null and param.versionName != ''">
AND a.version_name LIKE CONCAT('%', #{param.versionName}, '%')
</if>
<if test="param.env != null and param.env != ''">
AND a.env = #{param.env}
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.isCurrent != null">
AND a.is_current = #{param.isCurrent}
</if>
<if test="param.publishBy != null">
AND a.publish_by = #{param.publishBy}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.keywords != null and param.keywords != ''">
AND (a.version_no LIKE CONCAT('%', #{param.keywords}, '%')
OR a.version_name LIKE CONCAT('%', #{param.keywords}, '%')
OR a.changelog LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppVersion">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppVersion">
<include refid="selectSql"></include>
</select>
</mapper>

View File

@@ -0,0 +1,42 @@
package com.gxwebsoft.app.param;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用 API Key 查询参数
*
* @author 科技小王子
* @since 2026-04-02
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "AppApiKeyParam对象", description = "AppApiKey查询参数")
public class AppApiKeyParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "Key名称模糊搜索")
private String name;
@Schema(description = "状态: 0正常, 1禁用")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "用户ID")
@QueryField(type = QueryType.EQ)
private Integer userId;
@Schema(description = "应用ID")
@QueryField(type = QueryType.EQ)
private Long appId;
@Schema(description = "租户ID")
@QueryField(type = QueryType.EQ)
private Integer tenantId;
}

View File

@@ -0,0 +1,21 @@
package com.gxwebsoft.app.param;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 平台文章分类查询参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "平台文章分类查询参数")
public class AppArticleCategoryParam extends BaseParam {
@Schema(description = "分类ID")
private Integer categoryId;
@Schema(description = "状态")
private Integer status;
}

View File

@@ -0,0 +1,30 @@
package com.gxwebsoft.app.param;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 平台文章查询参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "平台文章查询参数")
public class AppArticleParam extends BaseParam {
@Schema(description = "文章ID")
private Integer articleId;
@Schema(description = "文章模型article/announcement")
private String model;
@Schema(description = "状态")
private Integer status;
@Schema(description = "分类ID")
private Integer categoryId;
@Schema(description = "是否推荐")
private Integer recommend;
}

View File

@@ -0,0 +1,35 @@
package com.gxwebsoft.app.param;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* CI/CD 构建查询参数
*
* @author 科技小王子
* @since 2026-04-03
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppBuildParam对象", description = "构建查询参数")
public class AppBuildParam extends BaseParam {
@Schema(description = "应用ID")
private Long appId;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "构建状态")
private String status;
@Schema(description = "CI系统类型")
private String ciType;
@Schema(description = "分支")
private String branch;
}

View File

@@ -0,0 +1,50 @@
package com.gxwebsoft.app.param;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 云账号凭证查询参数
*
* @author 科技小王子
* @since 2026-04-04
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "AppCloudCredentialParam对象", description = "云账号凭证查询参数")
public class AppCloudCredentialParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@QueryField(type = QueryType.EQ)
private Long id;
@Schema(description = "云服务商: aliyun/tencent/huawei/qiniu")
@QueryField(type = QueryType.EQ)
private String provider;
@Schema(description = "凭证名称")
private String name;
@Schema(description = "状态: 0正常 1冻结")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "是否删除: 0否 1是")
@QueryField(type = QueryType.EQ)
private Integer deleted;
@Schema(description = "用户ID")
@QueryField(type = QueryType.EQ)
private Integer userId;
@Schema(description = "租户ID")
@QueryField(type = QueryType.EQ)
private Integer tenantId;
}

View File

@@ -0,0 +1,38 @@
package com.gxwebsoft.app.param;
import com.gxwebsoft.common.core.web.BaseParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用配置表查询参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class AppConfigParam extends BaseParam {
/**
* 配置ID
*/
private Integer configId;
/**
* 应用ID
*/
private Integer appId;
/**
* 配置键
*/
private String configKey;
/**
* 配置类型
*/
private String configType;
/**
* 是否敏感信息
*/
private Integer isSecret;
}

View File

@@ -0,0 +1,33 @@
package com.gxwebsoft.app.param;
import com.gxwebsoft.common.core.web.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 合同查询参数
*
* @author 科技小王子
* @since 2026-04-13
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "合同查询参数")
public class AppContractParam extends PageParam {
@Schema(description = "合同类型: service/cooperation/purchase/other")
private String contractType;
@Schema(description = "状态: draft/pending/active/expired/terminated")
private String status;
@Schema(description = "关键词(合同名称/编号)")
private String keywords;
@Schema(description = "开始日期(用于筛选创建时间范围)")
private String startDate;
@Schema(description = "结束日期(用于筛选创建时间范围)")
private String endDate;
}

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