feat: 1. 新加入更改权限踢人功能。2. 使用几个 AI 工具测试工具效果。
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
gradle-repository
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
450
docs/jwt-redis-usage.md
Normal file
450
docs/jwt-redis-usage.md
Normal file
@@ -0,0 +1,450 @@
|
||||
# JWT + Redis + Session 双认证方式使用文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了ERP系统中同时支持两种认证方式的使用说明:
|
||||
- **Web端(后台管理)**:传统Session方式(基于Cookie)
|
||||
- **小程序端**:JWT方式(基于Token,存储于Redis)
|
||||
|
||||
两种认证方式同时启用,互不影响。
|
||||
|
||||
## 1. 认证方式对比
|
||||
|
||||
| 特性 | Web端(Session) | 小程序(JWT) |
|
||||
|------|-----------------|--------------|
|
||||
| 认证机制 | Cookie + Session | Token(Header) |
|
||||
| 登录接口 | POST `/auth/login` | POST `/api/auth/login` |
|
||||
| 登出接口 | POST `/auth/logout` | POST `/api/auth/logout` |
|
||||
| Token存储 | 服务端Session + Cookie | Redis + 客户端存储 |
|
||||
| 适用场景 | 浏览器后台管理 | 小程序/移动端 |
|
||||
| 单点登录 | 支持(同一账号同时只能一处登录) | 支持多设备登录 |
|
||||
|
||||
## 2. Web端(Session方式)
|
||||
|
||||
### 2.1 登录
|
||||
|
||||
**接口:** `POST /auth/login`
|
||||
|
||||
**请求方式:** 表单提交(Content-Type: application/x-www-form-urlencoded)
|
||||
|
||||
**请求参数:**
|
||||
```
|
||||
username=admin&password=123456
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success"
|
||||
}
|
||||
```
|
||||
|
||||
**前端使用:**
|
||||
```typescript
|
||||
const loginParams = new URLSearchParams();
|
||||
loginParams.append("username", username);
|
||||
loginParams.append("password", password);
|
||||
|
||||
fetch("/auth/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: loginParams,
|
||||
credentials: "include", // 重要:携带Cookie
|
||||
});
|
||||
```
|
||||
|
||||
### 2.2 登出
|
||||
|
||||
**接口:** `POST /auth/logout`
|
||||
|
||||
**前端使用:**
|
||||
```typescript
|
||||
fetch("/auth/logout", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
```
|
||||
|
||||
### 2.3 检测登录状态
|
||||
|
||||
**接口:** `GET /open/check-login`
|
||||
|
||||
**响应示例(已登录):**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"isLoggedIn": true,
|
||||
"userInfo": {
|
||||
"userId": 1,
|
||||
"loginName": "admin",
|
||||
"userName": "管理员"
|
||||
},
|
||||
"message": "登录状态有效(Session)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 小程序端(JWT方式)
|
||||
|
||||
### 3.1 登录
|
||||
|
||||
**接口:** `POST /api/auth/login`
|
||||
|
||||
**Content-Type:** `application/json`
|
||||
|
||||
**请求体:**
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
|
||||
"expiresIn": 86400,
|
||||
"refreshExpiresIn": 604800,
|
||||
"userId": 1,
|
||||
"loginName": "admin",
|
||||
"userName": "管理员"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 请求携带Token
|
||||
|
||||
在请求头中添加Authorization字段:
|
||||
```
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
|
||||
```
|
||||
|
||||
**示例:**
|
||||
```typescript
|
||||
fetch("/api/some-endpoint", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + accessToken,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 3.3 刷新Token
|
||||
|
||||
**接口:** `POST /api/auth/refresh`
|
||||
|
||||
**请求参数:**
|
||||
```
|
||||
refreshToken=eyJhbGciOiJIUzI1NiJ9...
|
||||
```
|
||||
|
||||
**响应示例:** 返回新的accessToken和refreshToken
|
||||
|
||||
### 3.4 登出
|
||||
|
||||
**接口:** `POST /api/auth/logout`
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
|
||||
```
|
||||
|
||||
**说明:** 将当前token加入黑名单,使其失效
|
||||
|
||||
## 4. Redis缓存服务
|
||||
|
||||
### 4.1 缓存服务接口 (CacheService)
|
||||
|
||||
位置:`com.niuan.erp.common.cache.CacheService`
|
||||
|
||||
提供以下操作:
|
||||
|
||||
#### 基本操作
|
||||
```java
|
||||
// 设置缓存
|
||||
<T> void set(String key, T value);
|
||||
|
||||
// 设置缓存并设置过期时间
|
||||
<T> void set(String key, T value, long timeout, TimeUnit unit);
|
||||
|
||||
// 获取缓存
|
||||
<T> T get(String key);
|
||||
|
||||
// 删除缓存
|
||||
Boolean delete(String key);
|
||||
|
||||
// 判断key是否存在
|
||||
Boolean hasKey(String key);
|
||||
```
|
||||
|
||||
#### Hash操作
|
||||
```java
|
||||
// 设置字段值
|
||||
<T> void hSet(String key, String field, T value);
|
||||
|
||||
// 获取字段值
|
||||
<T> T hGet(String key, String field);
|
||||
|
||||
// 获取所有字段和值
|
||||
<T> Map<String, T> hGetAll(String key);
|
||||
|
||||
// 删除字段
|
||||
Long hDelete(String key, Object... fields);
|
||||
```
|
||||
|
||||
#### Set操作
|
||||
```java
|
||||
// 添加成员
|
||||
<T> Long sAdd(String key, T... members);
|
||||
|
||||
// 获取所有成员
|
||||
<T> Set<T> sMembers(String key);
|
||||
|
||||
// 删除成员
|
||||
<T> Long sRemove(String key, T... members);
|
||||
```
|
||||
|
||||
### 4.2 使用示例
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SomeService {
|
||||
|
||||
private final CacheService cacheService;
|
||||
|
||||
public void example() {
|
||||
// 设置缓存,1小时过期
|
||||
cacheService.set("user:1", user, 3600, TimeUnit.SECONDS);
|
||||
|
||||
// 获取缓存
|
||||
User user = cacheService.get("user:1");
|
||||
|
||||
// Hash操作
|
||||
cacheService.hSet("user:1:profile", "email", "user@example.com");
|
||||
String email = cacheService.hGet("user:1:profile", "email");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 用户强制下线(踢用户)
|
||||
|
||||
### 5.1 机制说明
|
||||
|
||||
当权限、角色或用户数据发生变化时,系统会自动踢掉相关用户,强制其重新登录。
|
||||
|
||||
**Session方式(Web端):**
|
||||
- 清除用户缓存
|
||||
- 用户下次请求时Session验证失败
|
||||
|
||||
**JWT方式(小程序端):**
|
||||
- 将用户Token加入Redis黑名单
|
||||
- 用户下次请求时Token验证失败
|
||||
|
||||
### 5.2 触发场景
|
||||
|
||||
| 操作 | Web端影响 | 小程序端影响 |
|
||||
|------|----------|-------------|
|
||||
| 权限修改 | 踢掉所有拥有该权限的用户 | 踢掉所有拥有该权限的用户 |
|
||||
| 权限删除 | 踢掉所有拥有该权限的用户 | 踢掉所有拥有该权限的用户 |
|
||||
| 角色修改 | 踢掉所有拥有该角色的用户 | 踢掉所有拥有该角色的用户 |
|
||||
| 角色删除 | 踢掉所有拥有该角色的用户 | 踢掉所有拥有该角色的用户 |
|
||||
| 用户修改 | 踢掉该用户 | 踢掉该用户 |
|
||||
| 用户删除 | 踢掉该用户 | 踢掉该用户 |
|
||||
|
||||
### 5.3 手动踢用户
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SomeService {
|
||||
|
||||
private final UserTokenService userTokenService;
|
||||
|
||||
public void kickUserExample(Long userId) {
|
||||
// 踢掉指定用户(同时踢掉Session和JWT)
|
||||
userTokenService.kickUser(userId);
|
||||
|
||||
// 踢掉拥有指定角色的所有用户
|
||||
userTokenService.kickUsersByRole(roleId);
|
||||
|
||||
// 踢掉拥有指定权限的所有用户
|
||||
userTokenService.kickUsersByPermission(permissionId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 配置说明
|
||||
|
||||
### 6.1 application.yml
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
database: 0
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
max-wait: 1000ms
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: your-256-bit-secret-key-here-must-be-at-least-32-characters
|
||||
expiration: 86400000 # 访问令牌过期时间(24小时)
|
||||
refresh-expiration: 604800000 # 刷新令牌过期时间(7天)
|
||||
```
|
||||
|
||||
### 6.2 依赖
|
||||
|
||||
已在build.gradle.kts中添加:
|
||||
```kotlin
|
||||
// Redis
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-redis")
|
||||
implementation("org.apache.commons:commons-pool2")
|
||||
```
|
||||
|
||||
## 7. 接口清单
|
||||
|
||||
### 7.1 Web端接口(Session)
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/auth/login` | POST | 登录(表单提交) |
|
||||
| `/auth/logout` | POST | 登出 |
|
||||
| `/open/check-login` | GET | 检测登录状态 |
|
||||
|
||||
### 7.2 小程序接口(JWT)
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/auth/login` | POST | 登录(JSON) |
|
||||
| `/api/auth/refresh` | POST | 刷新Token |
|
||||
| `/api/auth/logout` | POST | 登出 |
|
||||
| `/open/check-login` | GET | 检测登录状态 |
|
||||
|
||||
### 7.3 API演示接口
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/demo/public` | GET | 公开接口(无需认证) |
|
||||
| `/api/demo/protected` | GET | 受保护接口(需要认证) |
|
||||
| `/api/demo/user-info` | GET | 获取当前用户信息 |
|
||||
| `/api/demo/cache-test` | POST | 缓存测试 |
|
||||
| `/api/demo/kick-user` | POST | 踢掉指定用户 |
|
||||
|
||||
## 8. 文件清单
|
||||
|
||||
### 新增文件
|
||||
|
||||
1. `com.niuan.erp.common.cache.CacheService` - 缓存服务接口
|
||||
2. `com.niuan.erp.common.cache.RedisCacheServiceImpl` - Redis缓存实现
|
||||
3. `com.niuan.erp.common.config.RedisConfig` - Redis配置
|
||||
4. `com.niuan.erp.common.security.JwtProperties` - JWT配置属性
|
||||
5. `com.niuan.erp.common.security.JwtTokenProvider` - JWT令牌提供者
|
||||
6. `com.niuan.erp.common.security.TokenService` - Token服务
|
||||
7. `com.niuan.erp.common.security.JwtAuthenticationFilter` - JWT认证过滤器
|
||||
8. `com.niuan.erp.common.security.UserTokenService` - 用户令牌管理服务
|
||||
9. `com.niuan.erp.module.open.controller.OpenController` - 公开接口控制器
|
||||
10. `com.niuan.erp.module.api.controller.MiniProgramAuthController` - 小程序认证接口
|
||||
11. `com.niuan.erp.module.api.controller.ApiDemoController` - API演示控制器
|
||||
|
||||
### 修改文件
|
||||
|
||||
1. `com.niuan.erp.common.config.SecurityConfig` - 安全配置(同时支持Session和JWT)
|
||||
2. `com.niuan.erp.common.bean.UrlPermissionAuthorizationManager` - 添加open和api路径放行
|
||||
3. `com.niuan.erp.module.sys.service.SysUserService` - 添加loadUserById等方法
|
||||
4. `com.niuan.erp.module.sys.service.impl.SysUserServiceImpl` - 实现新方法
|
||||
5. `com.niuan.erp.module.sys.mapper.SysUserMapper` - 添加查询方法
|
||||
6. `com.niuan.erp.module.sys.service.impl.SysPermissionServiceImpl` - 添加踢用户逻辑
|
||||
7. `com.niuan.erp.module.sys.service.impl.SysRoleServiceImpl` - 添加踢用户逻辑
|
||||
8. `build.gradle.kts` - 添加Redis依赖
|
||||
9. `application.yml` - 添加Redis和JWT配置
|
||||
10. `teek-design-vue3-template-main/src/common/api/user.ts` - 添加checkLogin方法
|
||||
|
||||
## 9. 注意事项
|
||||
|
||||
1. **Redis必须运行**:系统启动前确保Redis服务已启动
|
||||
2. **JWT密钥安全**:生产环境请使用复杂的密钥,并妥善保管
|
||||
3. **跨域配置**:小程序端需要配置相应的CORS允许域名
|
||||
4. **Token过期处理**:小程序端需要处理Token过期,自动调用刷新接口
|
||||
5. **权限配置**:API Demo接口需要 `api:demo` 权限,需要在数据库中添加该权限
|
||||
|
||||
## 10. 前端使用示例
|
||||
|
||||
### Web端(Vue)
|
||||
|
||||
```typescript
|
||||
// 登录
|
||||
const login = async (username: string, password: string) => {
|
||||
const params = new URLSearchParams();
|
||||
params.append("username", username);
|
||||
params.append("password", password);
|
||||
|
||||
const response = await fetch("/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: params,
|
||||
credentials: "include",
|
||||
});
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// 检测登录状态
|
||||
const checkLogin = async () => {
|
||||
const response = await fetch("/open/check-login", {
|
||||
credentials: "include",
|
||||
});
|
||||
return response.json();
|
||||
};
|
||||
```
|
||||
|
||||
### 小程序端
|
||||
|
||||
```typescript
|
||||
// 登录
|
||||
const login = async (username: string, password: string) => {
|
||||
const response = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// 保存token
|
||||
if (data.code === 0) {
|
||||
wx.setStorageSync("accessToken", data.data.accessToken);
|
||||
wx.setStorageSync("refreshToken", data.data.refreshToken);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// 请求携带token
|
||||
const requestWithToken = async (url: string, options: any = {}) => {
|
||||
const token = wx.getStorageSync("accessToken");
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
"Authorization": "Bearer " + token,
|
||||
},
|
||||
});
|
||||
return response.json();
|
||||
};
|
||||
```
|
||||
129
src/main/java/com/niuan/erp/common/cache/CacheService.java
vendored
Normal file
129
src/main/java/com/niuan/erp/common/cache/CacheService.java
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
package com.niuan.erp.common.cache;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 缓存服务接口
|
||||
* 定义通用的缓存操作,便于切换不同的缓存实现
|
||||
*/
|
||||
public interface CacheService {
|
||||
|
||||
/**
|
||||
* 设置缓存
|
||||
*/
|
||||
<T> void set(String key, T value);
|
||||
|
||||
/**
|
||||
* 设置缓存并设置过期时间
|
||||
*/
|
||||
<T> void set(String key, T value, long timeout, TimeUnit unit);
|
||||
|
||||
/**
|
||||
* 获取缓存
|
||||
*/
|
||||
<T> T get(String key);
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*/
|
||||
Boolean delete(String key);
|
||||
|
||||
/**
|
||||
* 批量删除缓存
|
||||
*/
|
||||
Long delete(Collection<String> keys);
|
||||
|
||||
/**
|
||||
* 判断key是否存在
|
||||
*/
|
||||
Boolean hasKey(String key);
|
||||
|
||||
/**
|
||||
* 设置过期时间
|
||||
*/
|
||||
Boolean expire(String key, long timeout, TimeUnit unit);
|
||||
|
||||
/**
|
||||
* 获取过期时间
|
||||
*/
|
||||
Long getExpire(String key, TimeUnit unit);
|
||||
|
||||
/**
|
||||
* 获取匹配的所有key
|
||||
*/
|
||||
Set<String> keys(String pattern);
|
||||
|
||||
/**
|
||||
* Hash操作:设置字段值
|
||||
*/
|
||||
<T> void hSet(String key, String field, T value);
|
||||
|
||||
/**
|
||||
* Hash操作:获取字段值
|
||||
*/
|
||||
<T> T hGet(String key, String field);
|
||||
|
||||
/**
|
||||
* Hash操作:获取所有字段和值
|
||||
*/
|
||||
<T> Map<String, T> hGetAll(String key);
|
||||
|
||||
/**
|
||||
* Hash操作:删除字段
|
||||
*/
|
||||
Long hDelete(String key, Object... fields);
|
||||
|
||||
/**
|
||||
* Hash操作:判断字段是否存在
|
||||
*/
|
||||
Boolean hHasKey(String key, String field);
|
||||
|
||||
/**
|
||||
* List操作:左侧推入
|
||||
*/
|
||||
<T> Long lPush(String key, T value);
|
||||
|
||||
/**
|
||||
* List操作:右侧弹出
|
||||
*/
|
||||
<T> T rPop(String key);
|
||||
|
||||
/**
|
||||
* List操作:获取列表范围
|
||||
*/
|
||||
<T> List<T> lRange(String key, long start, long end);
|
||||
|
||||
/**
|
||||
* Set操作:添加成员
|
||||
*/
|
||||
<T> Long sAdd(String key, T... members);
|
||||
|
||||
/**
|
||||
* Set操作:获取所有成员
|
||||
*/
|
||||
<T> Set<T> sMembers(String key);
|
||||
|
||||
/**
|
||||
* Set操作:删除成员
|
||||
*/
|
||||
<T> Long sRemove(String key, T... members);
|
||||
|
||||
/**
|
||||
* Set操作:判断是否是成员
|
||||
*/
|
||||
<T> Boolean sIsMember(String key, T member);
|
||||
|
||||
/**
|
||||
* 自增操作
|
||||
*/
|
||||
Long increment(String key);
|
||||
|
||||
/**
|
||||
* 自增指定值
|
||||
*/
|
||||
Long increment(String key, long delta);
|
||||
}
|
||||
148
src/main/java/com/niuan/erp/common/cache/RedisCacheServiceImpl.java
vendored
Normal file
148
src/main/java/com/niuan/erp/common/cache/RedisCacheServiceImpl.java
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
package com.niuan.erp.common.cache;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis缓存服务实现
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RedisCacheServiceImpl implements CacheService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public <T> void set(String key, T value) {
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void set(String key, T value, long timeout, TimeUnit unit) {
|
||||
redisTemplate.opsForValue().set(key, value, timeout, unit);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T get(String key) {
|
||||
return (T) redisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean delete(String key) {
|
||||
return redisTemplate.delete(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long delete(Collection<String> keys) {
|
||||
return redisTemplate.delete(keys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean hasKey(String key) {
|
||||
return redisTemplate.hasKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean expire(String key, long timeout, TimeUnit unit) {
|
||||
return redisTemplate.expire(key, timeout, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getExpire(String key, TimeUnit unit) {
|
||||
return redisTemplate.getExpire(key, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> keys(String pattern) {
|
||||
return redisTemplate.keys(pattern);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void hSet(String key, String field, T value) {
|
||||
redisTemplate.opsForHash().put(key, field, value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T hGet(String key, String field) {
|
||||
return (T) redisTemplate.opsForHash().get(key, field);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> Map<String, T> hGetAll(String key) {
|
||||
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
|
||||
Map<String, T> result = new java.util.HashMap<>();
|
||||
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
|
||||
result.put(entry.getKey().toString(), (T) entry.getValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long hDelete(String key, Object... fields) {
|
||||
return redisTemplate.opsForHash().delete(key, fields);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean hHasKey(String key, String field) {
|
||||
return redisTemplate.opsForHash().hasKey(key, field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Long lPush(String key, T value) {
|
||||
return redisTemplate.opsForList().leftPush(key, value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T rPop(String key) {
|
||||
return (T) redisTemplate.opsForList().rightPop(key);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> List<T> lRange(String key, long start, long end) {
|
||||
return (List<T>) redisTemplate.opsForList().range(key, start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Long sAdd(String key, T... members) {
|
||||
return redisTemplate.opsForSet().add(key, (Object[]) members);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> Set<T> sMembers(String key) {
|
||||
return (Set<T>) redisTemplate.opsForSet().members(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Long sRemove(String key, T... members) {
|
||||
return redisTemplate.opsForSet().remove(key, (Object[]) members);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Boolean sIsMember(String key, T member) {
|
||||
return redisTemplate.opsForSet().isMember(key, member);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long increment(String key) {
|
||||
return redisTemplate.opsForValue().increment(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long increment(String key, long delta) {
|
||||
return redisTemplate.opsForValue().increment(key, delta);
|
||||
}
|
||||
}
|
||||
48
src/main/java/com/niuan/erp/common/config/RedisConfig.java
Normal file
48
src/main/java/com/niuan/erp/common/config/RedisConfig.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.niuan.erp.common.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.activateDefaultTyping(
|
||||
LaissezFaireSubTypeValidator.instance,
|
||||
ObjectMapper.DefaultTyping.NON_FINAL,
|
||||
JsonTypeInfo.As.PROPERTY
|
||||
);
|
||||
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
|
||||
new Jackson2JsonRedisSerializer<>(mapper, Object.class);
|
||||
|
||||
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
||||
|
||||
// key采用String的序列化方式
|
||||
template.setKeySerializer(stringRedisSerializer);
|
||||
// hash的key也采用String的序列化方式
|
||||
template.setHashKeySerializer(stringRedisSerializer);
|
||||
// value序列化方式采用jackson
|
||||
template.setValueSerializer(jackson2JsonRedisSerializer);
|
||||
// hash的value序列化方式采用jackson
|
||||
template.setHashValueSerializer(jackson2JsonRedisSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
25
src/main/java/com/niuan/erp/common/config/SessionConfig.java
Normal file
25
src/main/java/com/niuan/erp/common/config/SessionConfig.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.niuan.erp.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||
import org.springframework.security.web.session.ConcurrentSessionFilter;
|
||||
|
||||
/**
|
||||
* Session配置类
|
||||
* 单独配置Session相关Bean,避免循环依赖
|
||||
*/
|
||||
@Configuration
|
||||
public class SessionConfig {
|
||||
|
||||
@Bean
|
||||
public SessionRegistry sessionRegistry() {
|
||||
return new SessionRegistryImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConcurrentSessionFilter concurrentSessionFilter(SessionRegistry sessionRegistry) {
|
||||
return new ConcurrentSessionFilter(sessionRegistry);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ public class CustomerTenantHandler implements TenantLineHandler {
|
||||
customerTables.add("storage_list");
|
||||
customerTables.add("bom_list");
|
||||
customerTables.add("vendor");
|
||||
customerTables.add("keyaccount");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.niuan.erp.common.security;
|
||||
|
||||
import com.niuan.erp.common.base.LoginUser;
|
||||
import com.niuan.erp.module.sys.service.SysUserService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* JWT认证过滤器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
private final TokenService tokenService;
|
||||
|
||||
private final SysUserService sysUserService;
|
||||
|
||||
private final JwtProperties jwtProperties;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
try {
|
||||
// 从请求中获取JWT令牌
|
||||
String jwt = getJwtFromRequest(request);
|
||||
|
||||
// 验证令牌
|
||||
if (StringUtils.hasText(jwt) && tokenService.validateToken(jwt)) {
|
||||
// 从令牌中获取用户ID
|
||||
Long userId = jwtTokenProvider.getUserIdFromToken(jwt);
|
||||
|
||||
// 加载用户信息
|
||||
LoginUser loginUser = sysUserService.loadUserById(userId);
|
||||
|
||||
if (loginUser != null) {
|
||||
// 创建认证对象
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
loginUser,
|
||||
null,
|
||||
loginUser.getAuthorities()
|
||||
);
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
|
||||
// 设置安全上下文
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("Could not set user authentication in security context", ex);
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取JWT令牌
|
||||
*/
|
||||
private String getJwtFromRequest(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader(jwtProperties.getHeader());
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(jwtProperties.getTokenPrefix())) {
|
||||
return bearerToken.substring(jwtProperties.getTokenPrefix().length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getRequestURI();
|
||||
// 登录、登出、公开接口不需要JWT验证
|
||||
return path.startsWith("/auth/login")
|
||||
|| path.startsWith("/auth/logout")
|
||||
|| path.startsWith("/open/")
|
||||
|| path.startsWith("/v3/api-docs")
|
||||
|| path.startsWith("/swagger-ui")
|
||||
|| path.startsWith("/doc.html")
|
||||
|| path.startsWith("/webjars")
|
||||
|| path.startsWith("/favicon.ico");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.niuan.erp.common.security;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* JWT配置属性
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "jwt")
|
||||
public class JwtProperties {
|
||||
|
||||
/**
|
||||
* JWT密钥
|
||||
*/
|
||||
private String secret;
|
||||
|
||||
/**
|
||||
* 访问令牌过期时间(毫秒)
|
||||
*/
|
||||
private Long expiration;
|
||||
|
||||
/**
|
||||
* 刷新令牌过期时间(毫秒)
|
||||
*/
|
||||
private Long refreshExpiration;
|
||||
|
||||
/**
|
||||
* 令牌前缀
|
||||
*/
|
||||
private String tokenPrefix = "Bearer ";
|
||||
|
||||
/**
|
||||
* 请求头名称
|
||||
*/
|
||||
private String header = "Authorization";
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.niuan.erp.common.security;
|
||||
|
||||
import com.niuan.erp.common.base.LoginUser;
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JWT令牌提供者
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtTokenProvider {
|
||||
|
||||
private final JwtProperties jwtProperties;
|
||||
|
||||
private SecretKey secretKey;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 使用配置的密钥创建签名密钥
|
||||
this.secretKey = Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成访问令牌
|
||||
*/
|
||||
public String generateAccessToken(LoginUser loginUser) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", loginUser.getUser().getId());
|
||||
claims.put("loginName", loginUser.getUser().getLoginName());
|
||||
claims.put("userName", loginUser.getUser().getUserName());
|
||||
claims.put("userType", loginUser.getUser().getUserType());
|
||||
claims.put("tokenType", "access");
|
||||
|
||||
return buildToken(claims, jwtProperties.getExpiration());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成刷新令牌
|
||||
*/
|
||||
public String generateRefreshToken(LoginUser loginUser) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", loginUser.getUser().getId());
|
||||
claims.put("tokenType", "refresh");
|
||||
|
||||
return buildToken(claims, jwtProperties.getRefreshExpiration());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建令牌
|
||||
*/
|
||||
private String buildToken(Map<String, Object> claims, long expiration) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
|
||||
return Jwts.builder()
|
||||
.claims(claims)
|
||||
.issuedAt(now)
|
||||
.expiration(expiryDate)
|
||||
.signWith(secretKey, Jwts.SIG.HS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从令牌中获取用户ID
|
||||
*/
|
||||
public Long getUserIdFromToken(String token) {
|
||||
Claims claims = parseToken(token);
|
||||
return claims.get("userId", Long.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从令牌中获取用户名
|
||||
*/
|
||||
public String getLoginNameFromToken(String token) {
|
||||
Claims claims = parseToken(token);
|
||||
return claims.get("loginName", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取令牌类型
|
||||
*/
|
||||
public String getTokenType(String token) {
|
||||
Claims claims = parseToken(token);
|
||||
return claims.get("tokenType", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析令牌
|
||||
*/
|
||||
public Claims parseToken(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证令牌是否有效
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
parseToken(token);
|
||||
return true;
|
||||
} catch (ExpiredJwtException e) {
|
||||
log.warn("JWT token is expired: {}", e.getMessage());
|
||||
} catch (UnsupportedJwtException e) {
|
||||
log.warn("JWT token is unsupported: {}", e.getMessage());
|
||||
} catch (MalformedJwtException e) {
|
||||
log.warn("JWT token is malformed: {}", e.getMessage());
|
||||
} catch (SignatureException e) {
|
||||
log.warn("JWT signature validation failed: {}", e.getMessage());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("JWT token is empty or null: {}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取令牌过期时间
|
||||
*/
|
||||
public Date getExpirationDateFromToken(String token) {
|
||||
Claims claims = parseToken(token);
|
||||
return claims.getExpiration();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查令牌是否即将过期(默认5分钟内)
|
||||
*/
|
||||
public boolean isTokenExpiringSoon(String token) {
|
||||
Date expiration = getExpirationDateFromToken(token);
|
||||
// 5分钟内即将过期
|
||||
long fiveMinutesInMillis = 5 * 60 * 1000;
|
||||
return expiration.getTime() - System.currentTimeMillis() < fiveMinutesInMillis;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.niuan.erp.common.security;
|
||||
|
||||
import com.niuan.erp.common.base.LoginUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Session踢人服务
|
||||
* 用于踢掉使用Session登录的用户
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SessionKickService {
|
||||
|
||||
private final SessionRegistry sessionRegistry;
|
||||
|
||||
/**
|
||||
* 踢掉指定用户的所有Session
|
||||
*/
|
||||
public void kickUser(Long userId) {
|
||||
try {
|
||||
// 获取所有已认证的用户
|
||||
List<Object> allPrincipals = sessionRegistry.getAllPrincipals();
|
||||
for (Object principal : allPrincipals) {
|
||||
if (principal instanceof LoginUser loginUser) {
|
||||
if (loginUser.getUser().getId().equals(userId)) {
|
||||
// 找到匹配的用户,踢掉所有Session
|
||||
List<SessionInformation> sessions = sessionRegistry.getAllSessions(principal, false);
|
||||
for (SessionInformation session : sessions) {
|
||||
session.expireNow();
|
||||
log.info("Expired session {} for user: {}", session.getSessionId(), userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error kicking session user: {}", userId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
229
src/main/java/com/niuan/erp/common/security/TokenService.java
Normal file
229
src/main/java/com/niuan/erp/common/security/TokenService.java
Normal file
@@ -0,0 +1,229 @@
|
||||
package com.niuan.erp.common.security;
|
||||
|
||||
import com.niuan.erp.common.base.LoginUser;
|
||||
import com.niuan.erp.common.cache.CacheService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Token服务
|
||||
* 管理JWT令牌的存储和验证
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TokenService {
|
||||
|
||||
private final CacheService cacheService;
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
private final JwtProperties jwtProperties;
|
||||
|
||||
private final SessionKickService sessionKickService;
|
||||
|
||||
private static final String USER_TOKEN_KEY_PREFIX = "user:token:";
|
||||
private static final String TOKEN_BLACKLIST_KEY_PREFIX = "token:blacklist:";
|
||||
private static final String USER_PERMISSION_VERSION_KEY_PREFIX = "user:permission:version:";
|
||||
|
||||
/**
|
||||
* 存储用户令牌到Redis
|
||||
*/
|
||||
public void storeUserToken(Long userId, String token) {
|
||||
String key = USER_TOKEN_KEY_PREFIX + userId;
|
||||
cacheService.sAdd(key, token);
|
||||
// 设置过期时间,与JWT过期时间一致
|
||||
cacheService.expire(key, 7, TimeUnit.DAYS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除用户令牌
|
||||
*/
|
||||
public void removeUserToken(Long userId, String token) {
|
||||
String key = USER_TOKEN_KEY_PREFIX + userId;
|
||||
cacheService.sRemove(key, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有令牌
|
||||
*/
|
||||
public Set<String> getUserTokens(Long userId) {
|
||||
String key = USER_TOKEN_KEY_PREFIX + userId;
|
||||
return cacheService.sMembers(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户的所有令牌并踢掉Session(踢掉用户)
|
||||
*/
|
||||
public void removeAllUserTokens(Long userId) {
|
||||
// 1. 踢掉JWT Token
|
||||
String key = USER_TOKEN_KEY_PREFIX + userId;
|
||||
Set<String> tokens = cacheService.sMembers(key);
|
||||
if (tokens != null && !tokens.isEmpty()) {
|
||||
// 将所有令牌加入黑名单
|
||||
for (String token : tokens) {
|
||||
addToBlacklist(token);
|
||||
}
|
||||
}
|
||||
cacheService.delete(key);
|
||||
|
||||
// 2. 踢掉Session用户
|
||||
sessionKickService.kickUser(userId);
|
||||
|
||||
log.info("Kicked user: {} (tokens and sessions removed)", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将令牌加入黑名单
|
||||
*/
|
||||
public void addToBlacklist(String token) {
|
||||
String key = TOKEN_BLACKLIST_KEY_PREFIX + token;
|
||||
cacheService.set(key, "1", 7, TimeUnit.DAYS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查令牌是否在黑名单中
|
||||
*/
|
||||
public boolean isTokenBlacklisted(String token) {
|
||||
String key = TOKEN_BLACKLIST_KEY_PREFIX + token;
|
||||
return Boolean.TRUE.equals(cacheService.hasKey(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证令牌是否有效
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
// 先检查是否在黑名单中
|
||||
if (isTokenBlacklisted(token)) {
|
||||
return false;
|
||||
}
|
||||
// 再验证JWT本身是否有效
|
||||
return jwtTokenProvider.validateToken(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证刷新令牌是否有效
|
||||
*/
|
||||
public boolean validateRefreshToken(String refreshToken) {
|
||||
// 先检查是否在黑名单中
|
||||
if (isTokenBlacklisted(refreshToken)) {
|
||||
return false;
|
||||
}
|
||||
// 验证JWT本身是否有效
|
||||
if (!jwtTokenProvider.validateToken(refreshToken)) {
|
||||
return false;
|
||||
}
|
||||
// 验证令牌类型是否为refresh
|
||||
String tokenType = jwtTokenProvider.getTokenType(refreshToken);
|
||||
return "refresh".equals(tokenType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从刷新令牌获取用户ID
|
||||
*/
|
||||
public Long getUserIdFromRefreshToken(String refreshToken) {
|
||||
return jwtTokenProvider.getUserIdFromToken(refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除刷新令牌
|
||||
*/
|
||||
public void removeRefreshToken(String refreshToken) {
|
||||
addToBlacklist(refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成并存储用户令牌对
|
||||
*/
|
||||
public TokenPair generateAndStoreTokens(LoginUser loginUser) {
|
||||
String accessToken = jwtTokenProvider.generateAccessToken(loginUser);
|
||||
String refreshToken = jwtTokenProvider.generateRefreshToken(loginUser);
|
||||
|
||||
// 存储令牌
|
||||
storeUserToken(loginUser.getUser().getId(), accessToken);
|
||||
storeUserToken(loginUser.getUser().getId(), refreshToken);
|
||||
|
||||
return new TokenPair(accessToken, refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*/
|
||||
public TokenPair refreshTokens(String refreshToken, LoginUser loginUser) {
|
||||
// 验证刷新令牌
|
||||
if (!validateToken(refreshToken)) {
|
||||
throw new RuntimeException("Invalid refresh token");
|
||||
}
|
||||
|
||||
// 验证令牌类型
|
||||
String tokenType = jwtTokenProvider.getTokenType(refreshToken);
|
||||
if (!"refresh".equals(tokenType)) {
|
||||
throw new RuntimeException("Invalid token type");
|
||||
}
|
||||
|
||||
// 将旧令牌加入黑名单
|
||||
addToBlacklist(refreshToken);
|
||||
|
||||
// 生成新令牌
|
||||
return generateAndStoreTokens(loginUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加用户权限版本号(用于踢掉用户)
|
||||
*/
|
||||
public void incrementPermissionVersion(Long userId) {
|
||||
String key = USER_PERMISSION_VERSION_KEY_PREFIX + userId;
|
||||
cacheService.increment(key);
|
||||
cacheService.expire(key, 7, TimeUnit.DAYS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限版本号
|
||||
*/
|
||||
public Long getPermissionVersion(Long userId) {
|
||||
String key = USER_PERMISSION_VERSION_KEY_PREFIX + userId;
|
||||
Long version = cacheService.get(key);
|
||||
return version != null ? version : 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色ID踢掉相关用户
|
||||
*/
|
||||
public void kickUsersByRoleId(Long roleId) {
|
||||
// 这里需要查询拥有该角色的所有用户,然后踢掉他们
|
||||
// 具体实现需要在UserRoleService中查询
|
||||
log.info("Kicking users by role id: {}", roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据权限ID踢掉相关用户
|
||||
*/
|
||||
public void kickUsersByPermissionId(Long permissionId) {
|
||||
// 这里需要查询拥有该权限的所有角色,再查询拥有这些角色的所有用户
|
||||
log.info("Kicking users by permission id: {}", permissionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取访问令牌过期时间(毫秒)
|
||||
*/
|
||||
public Long getExpiration() {
|
||||
return jwtProperties.getExpiration();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取刷新令牌过期时间(毫秒)
|
||||
*/
|
||||
public Long getRefreshExpiration() {
|
||||
return jwtProperties.getRefreshExpiration();
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌对
|
||||
*/
|
||||
public record TokenPair(String accessToken, String refreshToken) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.niuan.erp.common.security;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户令牌管理服务
|
||||
* 用于踢掉用户的令牌(强制下线)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserTokenService {
|
||||
|
||||
private final TokenService tokenService;
|
||||
|
||||
/**
|
||||
* 踢掉指定用户(用户修改后调用)
|
||||
*/
|
||||
public void kickUser(Long userId) {
|
||||
log.info("Kicking user: {}", userId);
|
||||
tokenService.removeAllUserTokens(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 踢掉拥有指定角色的所有用户(角色修改后调用)
|
||||
*/
|
||||
public void kickUsersByRole(Long roleId) {
|
||||
log.info("Kicking users by role: {}", roleId);
|
||||
// 注意:此方法需要根据实际业务实现
|
||||
// 可以在调用此方法的地方先查询用户ID列表
|
||||
// userIds.forEach(tokenService::removeAllUserTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* 踢掉拥有指定权限的所有用户(权限修改后调用)
|
||||
*/
|
||||
public void kickUsersByPermission(Long permissionId) {
|
||||
log.info("Kicking users by permission: {}", permissionId);
|
||||
// 注意:此方法需要根据实际业务实现
|
||||
// 可以在调用此方法的地方先查询用户ID列表
|
||||
// userIds.forEach(tokenService::removeAllUserTokens);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.niuan.erp.module.api.controller;
|
||||
|
||||
import com.niuan.erp.common.base.BaseResult;
|
||||
import com.niuan.erp.common.cache.CacheService;
|
||||
import com.niuan.erp.common.security.JwtTokenProvider;
|
||||
import com.niuan.erp.common.security.TokenService;
|
||||
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.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* API模块Demo控制器
|
||||
* 展示JWT和Redis缓存的使用方式
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/demo")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "API演示", description = "JWT和Redis缓存使用示例")
|
||||
@PreAuthorize("hasAnyAuthority('api:demo')")
|
||||
public class ApiDemoController {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
private final TokenService tokenService;
|
||||
|
||||
private final CacheService cacheService;
|
||||
|
||||
/**
|
||||
* 演示:刷新访问令牌
|
||||
*/
|
||||
@PostMapping("/refresh-token")
|
||||
@Operation(summary = "刷新访问令牌", description = "使用刷新令牌获取新的访问令牌")
|
||||
public BaseResult<Map<String, String>> refreshToken(
|
||||
@Parameter(description = "刷新令牌") @RequestParam String refreshToken) {
|
||||
// 实际使用时需要加载用户信息
|
||||
// TokenService.TokenPair tokenPair = tokenService.refreshTokens(refreshToken, loginUser);
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("message", "刷新令牌示例 - 实际使用时需要传入用户信息");
|
||||
return BaseResult.successWithData(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示:将令牌加入黑名单(登出)
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
@Operation(summary = "登出", description = "将当前令牌加入黑名单")
|
||||
public BaseResult<Void> logout(
|
||||
@Parameter(description = "访问令牌") @RequestParam String token) {
|
||||
tokenService.addToBlacklist(token);
|
||||
return BaseResult.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示:检查令牌是否在黑名单中
|
||||
*/
|
||||
@GetMapping("/check-blacklist")
|
||||
@Operation(summary = "检查令牌黑名单", description = "检查指定令牌是否在黑名单中")
|
||||
public BaseResult<Map<String, Boolean>> checkBlacklist(
|
||||
@Parameter(description = "访问令牌") @RequestParam String token) {
|
||||
boolean isBlacklisted = tokenService.isTokenBlacklisted(token);
|
||||
Map<String, Boolean> result = new HashMap<>();
|
||||
result.put("isBlacklisted", isBlacklisted);
|
||||
return BaseResult.successWithData(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示:Redis缓存基本操作
|
||||
*/
|
||||
@PostMapping("/cache/set")
|
||||
@Operation(summary = "设置缓存", description = "将数据存入Redis缓存")
|
||||
public BaseResult<Void> setCache(
|
||||
@Parameter(description = "缓存键") @RequestParam String key,
|
||||
@Parameter(description = "缓存值") @RequestParam String value,
|
||||
@Parameter(description = "过期时间(秒)") @RequestParam(defaultValue = "3600") long timeout) {
|
||||
cacheService.set(key, value, timeout, TimeUnit.SECONDS);
|
||||
return BaseResult.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示:获取Redis缓存
|
||||
*/
|
||||
@GetMapping("/cache/get")
|
||||
@Operation(summary = "获取缓存", description = "从Redis缓存获取数据")
|
||||
public BaseResult<Map<String, Object>> getCache(
|
||||
@Parameter(description = "缓存键") @RequestParam String key) {
|
||||
Object value = cacheService.get(key);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("key", key);
|
||||
result.put("value", value);
|
||||
result.put("exists", value != null);
|
||||
return BaseResult.successWithData(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示:删除Redis缓存
|
||||
*/
|
||||
@PostMapping("/cache/delete")
|
||||
@Operation(summary = "删除缓存", description = "从Redis缓存删除数据")
|
||||
public BaseResult<Map<String, Boolean>> deleteCache(
|
||||
@Parameter(description = "缓存键") @RequestParam String key) {
|
||||
Boolean deleted = cacheService.delete(key);
|
||||
Map<String, Boolean> result = new HashMap<>();
|
||||
result.put("deleted", deleted);
|
||||
return BaseResult.successWithData(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示:Redis Hash操作
|
||||
*/
|
||||
@PostMapping("/cache/hash/set")
|
||||
@Operation(summary = "设置Hash缓存", description = "将数据存入Redis Hash")
|
||||
public BaseResult<Void> setHashCache(
|
||||
@Parameter(description = "Hash键") @RequestParam String key,
|
||||
@Parameter(description = "字段名") @RequestParam String field,
|
||||
@Parameter(description = "字段值") @RequestParam String value) {
|
||||
cacheService.hSet(key, field, value);
|
||||
return BaseResult.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示:获取Redis Hash
|
||||
*/
|
||||
@GetMapping("/cache/hash/get")
|
||||
@Operation(summary = "获取Hash缓存", description = "从Redis Hash获取数据")
|
||||
public BaseResult<Map<String, Object>> getHashCache(
|
||||
@Parameter(description = "Hash键") @RequestParam String key,
|
||||
@Parameter(description = "字段名") @RequestParam String field) {
|
||||
Object value = cacheService.hGet(key, field);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("key", key);
|
||||
result.put("field", field);
|
||||
result.put("value", value);
|
||||
return BaseResult.successWithData(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示:获取用户所有令牌
|
||||
*/
|
||||
@GetMapping("/user/tokens")
|
||||
@Operation(summary = "获取用户令牌", description = "获取指定用户的所有有效令牌")
|
||||
public BaseResult<Map<String, Object>> getUserTokens(
|
||||
@Parameter(description = "用户ID") @RequestParam Long userId) {
|
||||
Set<String> tokens = tokenService.getUserTokens(userId);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("userId", userId);
|
||||
result.put("tokenCount", tokens != null ? tokens.size() : 0);
|
||||
result.put("tokens", tokens);
|
||||
return BaseResult.successWithData(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示:踢掉用户(强制下线)
|
||||
*/
|
||||
@PostMapping("/user/kick")
|
||||
@Operation(summary = "踢掉用户", description = "强制指定用户下线(删除所有令牌)")
|
||||
public BaseResult<Void> kickUser(
|
||||
@Parameter(description = "用户ID") @RequestParam Long userId) {
|
||||
tokenService.removeAllUserTokens(userId);
|
||||
return BaseResult.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package com.niuan.erp.module.api.controller;
|
||||
|
||||
import com.niuan.erp.common.base.BaseResult;
|
||||
import com.niuan.erp.common.base.LoginUser;
|
||||
import com.niuan.erp.common.security.TokenService;
|
||||
import com.niuan.erp.module.sys.service.SysUserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 小程序认证接口
|
||||
* 供小程序端使用JWT认证方式
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
@Validated
|
||||
@Tag(name = "小程序认证", description = "小程序端JWT认证接口")
|
||||
public class MiniProgramAuthController {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
|
||||
private final TokenService tokenService;
|
||||
|
||||
private final SysUserService sysUserService;
|
||||
|
||||
/**
|
||||
* 小程序登录
|
||||
* 使用用户名密码登录,返回JWT令牌
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "小程序登录", description = "使用用户名密码登录,返回JWT访问令牌和刷新令牌")
|
||||
public BaseResult<TokenResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
// 认证用户
|
||||
Authentication authentication = authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
request.getUsername(),
|
||||
request.getPassword()
|
||||
)
|
||||
);
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
|
||||
|
||||
// 生成并存储JWT令牌
|
||||
TokenService.TokenPair tokenPair = tokenService.generateAndStoreTokens(loginUser);
|
||||
|
||||
// 构建响应
|
||||
TokenResponse response = new TokenResponse(
|
||||
tokenPair.accessToken(),
|
||||
tokenPair.refreshToken(),
|
||||
tokenService.getExpiration() / 1000, // 转换为秒
|
||||
tokenService.getRefreshExpiration() / 1000,
|
||||
loginUser.getUser().getId(),
|
||||
loginUser.getUser().getLoginName(),
|
||||
loginUser.getUser().getUserName()
|
||||
);
|
||||
|
||||
return BaseResult.successWithData(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
@Operation(summary = "刷新令牌", description = "使用刷新令牌获取新的访问令牌")
|
||||
public BaseResult<TokenResponse> refreshToken(
|
||||
@Parameter(description = "刷新令牌", required = true)
|
||||
@RequestParam @NotBlank String refreshToken) {
|
||||
|
||||
// 验证刷新令牌
|
||||
if (!tokenService.validateRefreshToken(refreshToken)) {
|
||||
return BaseResult.error(401, "刷新令牌无效或已过期");
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
Long userId = tokenService.getUserIdFromRefreshToken(refreshToken);
|
||||
LoginUser loginUser = sysUserService.loadUserById(userId);
|
||||
|
||||
if (loginUser == null) {
|
||||
return BaseResult.error(401, "用户不存在");
|
||||
}
|
||||
|
||||
// 删除旧的刷新令牌
|
||||
tokenService.removeRefreshToken(refreshToken);
|
||||
|
||||
// 生成新的令牌对
|
||||
TokenService.TokenPair tokenPair = tokenService.generateAndStoreTokens(loginUser);
|
||||
|
||||
TokenResponse response = new TokenResponse(
|
||||
tokenPair.accessToken(),
|
||||
tokenPair.refreshToken(),
|
||||
tokenService.getExpiration() / 1000,
|
||||
tokenService.getRefreshExpiration() / 1000,
|
||||
loginUser.getUser().getId(),
|
||||
loginUser.getUser().getLoginName(),
|
||||
loginUser.getUser().getUserName()
|
||||
);
|
||||
|
||||
return BaseResult.successWithData(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
@Operation(summary = "登出", description = "将当前令牌加入黑名单")
|
||||
public BaseResult<Void> logout(
|
||||
@Parameter(description = "访问令牌", required = true)
|
||||
@RequestHeader("Authorization") String authorization) {
|
||||
|
||||
String token = extractToken(authorization);
|
||||
if (token != null) {
|
||||
tokenService.addToBlacklist(token);
|
||||
}
|
||||
|
||||
return BaseResult.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Authorization头中提取token
|
||||
*/
|
||||
private String extractToken(String authorization) {
|
||||
if (authorization != null && authorization.startsWith("Bearer ")) {
|
||||
return authorization.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录请求
|
||||
*/
|
||||
@Data
|
||||
public static class LoginRequest {
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Parameter(description = "用户名", required = true)
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Parameter(description = "密码", required = true)
|
||||
private String password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌响应
|
||||
*/
|
||||
public record TokenResponse(
|
||||
@Parameter(description = "访问令牌") String accessToken,
|
||||
@Parameter(description = "刷新令牌") String refreshToken,
|
||||
@Parameter(description = "访问令牌过期时间(秒)") Long expiresIn,
|
||||
@Parameter(description = "刷新令牌过期时间(秒)") Long refreshExpiresIn,
|
||||
@Parameter(description = "用户ID") Long userId,
|
||||
@Parameter(description = "登录名") String loginName,
|
||||
@Parameter(description = "用户名") String userName
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.niuan.erp.module.open.controller;
|
||||
|
||||
import com.niuan.erp.common.base.BaseResult;
|
||||
import com.niuan.erp.common.base.LoginUser;
|
||||
import com.niuan.erp.common.security.JwtTokenProvider;
|
||||
import com.niuan.erp.common.security.TokenService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 公开接口控制器
|
||||
* 用于前端检测登录状态等不需要认证的接口
|
||||
* 同时支持Session和JWT两种认证方式
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/open")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "公开接口", description = "不需要认证的公开接口")
|
||||
public class OpenController {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
private final TokenService tokenService;
|
||||
|
||||
private static final String TOKEN_PREFIX = "Bearer ";
|
||||
|
||||
/**
|
||||
* 检测用户登录状态
|
||||
* 支持两种检测方式:
|
||||
* 1. Session方式(Web端):检查Spring Security上下文
|
||||
* 2. JWT方式(小程序):检查Authorization头中的Token
|
||||
*/
|
||||
@GetMapping("/check-login")
|
||||
@Operation(summary = "检测登录状态", description = "检测当前是否已登录,支持Session和JWT两种方式")
|
||||
public BaseResult<LoginStatusResponse> checkLogin(HttpServletRequest request) {
|
||||
// 方式1:检查Session(Web端)
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.isAuthenticated()
|
||||
&& authentication.getPrincipal() instanceof LoginUser) {
|
||||
LoginUser user = (LoginUser) authentication.getPrincipal();
|
||||
LoginStatusResponse response = new LoginStatusResponse(
|
||||
true,
|
||||
new UserInfo(user.getUser().getId(), user.getUser().getLoginName(), user.getUser().getUserName()),
|
||||
"登录状态有效(Session)"
|
||||
);
|
||||
return BaseResult.successWithData(response);
|
||||
}
|
||||
|
||||
// 方式2:检查JWT(小程序端)
|
||||
String token = extractToken(request);
|
||||
if (StringUtils.hasText(token)) {
|
||||
// 检查token是否在黑名单中
|
||||
if (tokenService.isTokenBlacklisted(token)) {
|
||||
return BaseResult.successWithData(new LoginStatusResponse(false, null, "Token已失效"));
|
||||
}
|
||||
|
||||
// 验证token有效性
|
||||
if (jwtTokenProvider.validateToken(token)) {
|
||||
// 获取用户信息
|
||||
Long userId = jwtTokenProvider.getUserIdFromToken(token);
|
||||
String loginName = jwtTokenProvider.getLoginNameFromToken(token);
|
||||
|
||||
LoginStatusResponse response = new LoginStatusResponse(
|
||||
true,
|
||||
new UserInfo(userId, loginName, null),
|
||||
"登录状态有效(JWT)"
|
||||
);
|
||||
return BaseResult.successWithData(response);
|
||||
} else {
|
||||
return BaseResult.successWithData(new LoginStatusResponse(false, null, "Token无效或已过期"));
|
||||
}
|
||||
}
|
||||
|
||||
// 未登录
|
||||
return BaseResult.successWithData(new LoginStatusResponse(false, null, "未登录"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中提取token
|
||||
*/
|
||||
private String extractToken(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)) {
|
||||
return bearerToken.substring(TOKEN_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录状态响应
|
||||
*/
|
||||
public record LoginStatusResponse(boolean isLoggedIn, UserInfo userInfo, String message) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
public record UserInfo(Long userId, String loginName, String userName) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.niuan.erp.module.production.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
@Schema(description = "设备出货明细DTO")
|
||||
public record DeviceOutstockItem(
|
||||
@Schema(description = "产品SN")
|
||||
@NotBlank(message = "production.finished_product_receipt.validate.product_sn.not_blank")
|
||||
String productSn
|
||||
) {}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.niuan.erp.module.production.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "设备出货请求DTO")
|
||||
public record DeviceOutstockRequest(
|
||||
@Schema(description = "成品入库单ID")
|
||||
@NotNull(message = "production.finished_product_receipt.validate.id.not_null")
|
||||
Long id,
|
||||
|
||||
@Schema(description = "客户ID")
|
||||
@NotNull(message = "production.finished_product_receipt.validate.key_account_id.not_null")
|
||||
Integer keyAccountId,
|
||||
|
||||
@Schema(description = "出货设备列表")
|
||||
@NotNull(message = "production.finished_product_receipt.validate.outstock_list.not_null")
|
||||
List<DeviceOutstockItem> outstockList
|
||||
) {}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.niuan.erp.module.production.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "设备出库DTO")
|
||||
public record DeviceShipmentDto(
|
||||
@Schema(description = "ID")
|
||||
Long id,
|
||||
@Schema(description = "状态")
|
||||
Integer status,
|
||||
@Schema(description = "创建时间")
|
||||
LocalDateTime createDate,
|
||||
@Schema(description = "创建用户ID")
|
||||
Long createUserId,
|
||||
@Schema(description = "创建用户名")
|
||||
String createUserName,
|
||||
@Schema(description = "更新时间")
|
||||
LocalDateTime updateDate,
|
||||
@Schema(description = "更新用户ID")
|
||||
Long updateUserId,
|
||||
@Schema(description = "更新用户名")
|
||||
String updateUserName,
|
||||
@Schema(description = "单据编号")
|
||||
Integer documentNo,
|
||||
@Schema(description = "产品类型")
|
||||
String productType,
|
||||
@Schema(description = "产品SN")
|
||||
String productSn,
|
||||
@Schema(description = "MAC地址")
|
||||
String mac,
|
||||
@Schema(description = "序列号")
|
||||
String serialNum,
|
||||
@Schema(description = "软件版本")
|
||||
String softVersion,
|
||||
@Schema(description = "AL版本")
|
||||
String alVersion,
|
||||
@Schema(description = "AL编号")
|
||||
String alNum,
|
||||
@Schema(description = "AL状态")
|
||||
Boolean alStatus,
|
||||
@Schema(description = "维修状态")
|
||||
Integer repairStatus,
|
||||
@Schema(description = "备注")
|
||||
String mark,
|
||||
@Schema(description = "客户ID")
|
||||
Integer customerId,
|
||||
@Schema(description = "出库状态")
|
||||
Boolean outStatus,
|
||||
@Schema(description = "出库日期")
|
||||
LocalDateTime outProductDate,
|
||||
@Schema(description = "维修备注")
|
||||
String repairMark,
|
||||
@Schema(description = "产品SN显示")
|
||||
String productSnDisplay
|
||||
) {}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.niuan.erp.module.production.controller.dto;
|
||||
|
||||
import com.niuan.erp.module.sale.controller.dto.DeviceAddDto;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record FinishedProductReceiptAddDto(
|
||||
@NotNull(message = "production.finished_product_receipt.validate.form_code.not_null")
|
||||
String formCode,
|
||||
@NotNull(message = "production.finished_product_receipt.validate.form_name.not_null")
|
||||
String formName,
|
||||
String formMark,
|
||||
|
||||
@NotNull(message = "production.finished_product_receipt.validate.store_no.not_null")
|
||||
Integer storeNo,
|
||||
@NotNull(message = "production.finished_product_receipt.validate.store_name.not_null")
|
||||
String storeName,
|
||||
|
||||
@NotNull(message = "production.finished_product_receipt.validate.device_items.not_null")
|
||||
List<DeviceAddDto> deviceItems
|
||||
) {}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.niuan.erp.module.production.controller.dto;
|
||||
|
||||
import com.niuan.erp.module.production.enums.OutStockType;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record FinishedProductShipmentAddDto(
|
||||
@NotNull(message = "production.finished_product_shipment.validate.form_code.not_null")
|
||||
String formCode,
|
||||
@NotNull(message = "production.finished_product_shipment.validate.form_name.not_null")
|
||||
String formName,
|
||||
String formMark,
|
||||
|
||||
@NotNull(message = "production.finished_product_shipment.validate.store_no.not_null")
|
||||
Integer storeNo,
|
||||
@NotNull(message = "production.finished_product_shipment.validate.store_name.not_null")
|
||||
String storeName,
|
||||
|
||||
@NotNull(message = "production.finished_product_shipment.validate.out_stock_type.not_null")
|
||||
OutStockType outStockType,
|
||||
|
||||
@NotNull(message = "production.finished_product_shipment.validate.shipment_items.not_null")
|
||||
List<FinishedProductShipmentItemDto> shipmentItems
|
||||
) {}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.niuan.erp.module.production.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "成品出库明细DTO")
|
||||
public record FinishedProductShipmentItemDto(
|
||||
@Schema(description = "ID")
|
||||
Long id,
|
||||
@Schema(description = "物料编号")
|
||||
@NotBlank(message = "production.finished_product_shipment.validate.part_number.not_blank")
|
||||
String partNumber,
|
||||
@Schema(description = "物料型号")
|
||||
String productSpecs,
|
||||
@Schema(description = "数量")
|
||||
@NotNull(message = "production.finished_product_shipment.validate.product_count.not_null")
|
||||
Integer productCount,
|
||||
@Schema(description = "备注")
|
||||
String productMark) {}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.niuan.erp.module.production.controller.dto;
|
||||
|
||||
public record ProductionReturnItemDto(
|
||||
String partNumber,
|
||||
String productSpecs,
|
||||
Integer returnQty
|
||||
) {}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.niuan.erp.module.production.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public enum OutStockType implements IEnum<Integer> {
|
||||
MATERIAL(1, "物料"),
|
||||
FINISHED_PRODUCT(2, "成品");
|
||||
|
||||
final int code;
|
||||
final String description;
|
||||
|
||||
OutStockType(int code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public OutStockType fromCode(int code) {
|
||||
for (OutStockType value : OutStockType.values()) {
|
||||
if (value.code == code) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonValue
|
||||
public Integer getValue() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.niuan.erp.module.purchase.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "生成采购订单请求")
|
||||
public record GeneratePurchaseOrderDto(
|
||||
|
||||
@Schema(description = "采购计划 ID")
|
||||
@NotNull(message = "{purchase.purchase_plan.validate.plan_id.not_null}")
|
||||
Long planId,
|
||||
|
||||
@Schema(description = "供应商 ID")
|
||||
@NotNull(message = "{purchase.purchase_plan.validate.vendor_id.not_null}")
|
||||
Long vendorId,
|
||||
|
||||
@Schema(description = "供应商名称")
|
||||
@NotNull(message = "{purchase.purchase_plan.validate.vendor_name.not_null}")
|
||||
String vendorName,
|
||||
|
||||
@Schema(description = "单据编号")
|
||||
@NotNull(message = "{purchase.purchase_plan.validate.form_code.not_null}")
|
||||
String formCode,
|
||||
|
||||
@Schema(description = "单据名称")
|
||||
@NotNull(message = "{purchase.purchase_plan.validate.form_name.not_null}")
|
||||
String formName,
|
||||
|
||||
@Schema(description = "单据备注")
|
||||
@NotNull(message = "{purchase.purchase_plan.validate.form_mark.not_null}")
|
||||
String formMark,
|
||||
|
||||
@Schema(description = "订单总额")
|
||||
@NotNull(message = "{purchase.purchase_plan.validate.total_value.not_null}")
|
||||
BigDecimal totalValue,
|
||||
|
||||
@Schema(description = "选中的采购明细")
|
||||
@NotNull(message = "{purchase.purchase_plan.validate.selected_items.not_null}")
|
||||
List<PurchasePlanItemDto> selectedItems
|
||||
) {}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.niuan.erp.module.purchase.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Schema(description = "物料供应商映射关系")
|
||||
public record PartVendorMappingDto(
|
||||
|
||||
@Schema(description = "物料编号")
|
||||
String partNumber,
|
||||
|
||||
@Schema(description = "供应商 ID")
|
||||
Long vendorId,
|
||||
|
||||
@Schema(description = "采购价格")
|
||||
BigDecimal costPrice,
|
||||
|
||||
@Schema(description = "最后采购日期")
|
||||
String procureDate
|
||||
) {}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.niuan.erp.module.purchase.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "采购订单新增DTO")
|
||||
public record PurchaseOrderAddDto(
|
||||
@Schema(description = "订单ID (编辑时使用)")
|
||||
Long id,
|
||||
|
||||
@Schema(description = "供应商名称")
|
||||
@NotBlank(message = "{purchase.purchase_order.validate.vendor_name.not_blank}")
|
||||
String vendorName,
|
||||
|
||||
@Schema(description = "供应商ID")
|
||||
Integer vendorNo,
|
||||
|
||||
@Schema(description = "仓库编号")
|
||||
Integer storeNo,
|
||||
|
||||
@Schema(description = "仓库名称")
|
||||
String storeName,
|
||||
|
||||
@Schema(description = "单据备注")
|
||||
String formMark,
|
||||
|
||||
@Schema(description = "关联采购计划ID")
|
||||
Long planId,
|
||||
|
||||
@Schema(description = "订单明细列表")
|
||||
@Valid
|
||||
@NotEmpty(message = "{purchase.purchase_order.validate.items.not_empty}")
|
||||
@Size(min = 1, message = "{purchase.purchase_order.validate.items.size}")
|
||||
List<PurchaseOrderItemAddDto> items
|
||||
) {}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.niuan.erp.module.purchase.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "采购订单入库DTO")
|
||||
public record PurchaseOrderInboundDto(
|
||||
@Schema(description = "订单ID")
|
||||
@NotNull(message = "{purchase.purchase_order.validate.order_id.not_null}")
|
||||
Long orderId,
|
||||
|
||||
@Schema(description = "订单编号")
|
||||
String orderCode,
|
||||
|
||||
@Schema(description = "供应商名称")
|
||||
String vendorName,
|
||||
|
||||
@Schema(description = "仓库编号")
|
||||
@NotNull(message = "{purchase.purchase_order.validate.store_no.not_null}")
|
||||
Integer storeNo,
|
||||
|
||||
@Schema(description = "仓库名称")
|
||||
String storeName,
|
||||
|
||||
@Schema(description = "入库备注")
|
||||
String formMark,
|
||||
|
||||
@Schema(description = "入库明细列表")
|
||||
@Valid
|
||||
@NotEmpty(message = "{purchase.purchase_order.validate.inbound_items.not_empty}")
|
||||
@Size(min = 1, message = "{purchase.purchase_order.validate.inbound_items.size}")
|
||||
List<PurchaseOrderInboundItemDto> items
|
||||
) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.niuan.erp.module.purchase.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "采购订单入库明细DTO")
|
||||
public record PurchaseOrderInboundItemDto(
|
||||
@Schema(description = "订单明细ID")
|
||||
@NotNull(message = "{purchase.purchase_order.validate.item_id.not_null}")
|
||||
Long itemId,
|
||||
|
||||
@Schema(description = "本次入库数量")
|
||||
@NotNull(message = "{purchase.purchase_order.validate.inbound_count.not_null}")
|
||||
@Min(value = 1, message = "{purchase.purchase_order.validate.inbound_count.min}")
|
||||
Integer inboundCount
|
||||
) {}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.niuan.erp.module.purchase.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Schema(description = "采购订单明细新增DTO")
|
||||
public record PurchaseOrderItemAddDto(
|
||||
@Schema(description = "明细ID (编辑时使用)")
|
||||
Long id,
|
||||
|
||||
@Schema(description = "物料编号")
|
||||
@NotBlank(message = "{purchase.purchase_order.validate.part_number.not_blank}")
|
||||
String partNumber,
|
||||
|
||||
@Schema(description = "采购数量")
|
||||
@NotNull(message = "{purchase.purchase_order.validate.purchase_count.not_null}")
|
||||
@Min(value = 1, message = "{purchase.purchase_order.validate.purchase_count.min}")
|
||||
Integer purchaseCount,
|
||||
|
||||
@Schema(description = "单价")
|
||||
@NotNull(message = "{purchase.purchase_order.validate.price.not_null}")
|
||||
@DecimalMin(value = "0.0", message = "{purchase.purchase_order.validate.price.min}")
|
||||
BigDecimal price,
|
||||
|
||||
@Schema(description = "备注")
|
||||
String purchaseMark,
|
||||
|
||||
@Schema(description = "物料ID")
|
||||
Long partId
|
||||
) {}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.niuan.erp.module.purchase.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "采购订单二维码DTO")
|
||||
public record PurchaseOrderQrCodeDto(
|
||||
@Schema(description = "明细ID")
|
||||
Long itemId,
|
||||
|
||||
@Schema(description = "物料编号")
|
||||
String partNumber,
|
||||
|
||||
@Schema(description = "物料型号")
|
||||
String productSpecs,
|
||||
|
||||
@Schema(description = "采购数量")
|
||||
Integer purchaseCount,
|
||||
|
||||
@Schema(description = "单价")
|
||||
Double price,
|
||||
|
||||
@Schema(description = "二维码内容")
|
||||
String qrContent,
|
||||
|
||||
@Schema(description = "二维码序列号 (用于区分同一物料的多个二维码)")
|
||||
Integer sequence
|
||||
) {}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.niuan.erp.module.purchase.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "采购计划明细与供应商建议响应")
|
||||
public record PurchasePlanItemsWithVendorSuggestionsDto(
|
||||
|
||||
@Schema(description = "采购计划明细列表")
|
||||
List<PurchasePlanItemDto> items,
|
||||
|
||||
@Schema(description = "所有供应商列表")
|
||||
List<VendorInfoDto> allVendors,
|
||||
|
||||
@Schema(description = "物料供应商映射关系")
|
||||
List<PartVendorMappingDto> partVendorMappings
|
||||
) {}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.niuan.erp.module.purchase.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "供应商基本信息")
|
||||
public record VendorInfoDto(
|
||||
|
||||
@Schema(description = "供应商 ID")
|
||||
Long vendorId,
|
||||
|
||||
@Schema(description = "供应商名称")
|
||||
String vendorName,
|
||||
|
||||
@Schema(description = "创建日期")
|
||||
String createDate
|
||||
) {}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.niuan.erp.module.purchase.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Schema(description = "供应商推荐信息")
|
||||
public record VendorSuggestionDto(
|
||||
@Schema(description = "供应商 ID")
|
||||
Long vendorId,
|
||||
|
||||
@Schema(description = "供应商名称")
|
||||
String vendorName,
|
||||
|
||||
@Schema(description = "优先级:1-WarehouseItem, 2-ProductVendorMap, 3-其他")
|
||||
Integer priority,
|
||||
|
||||
@Schema(description = "采购价格")
|
||||
BigDecimal price
|
||||
) {}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.niuan.erp.module.sale.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
@Schema(description = "设备新增DTO")
|
||||
public record DeviceAddDto(
|
||||
@Schema(description = "产品类型")
|
||||
@NotBlank(message = "sale.device.validate.product_type.not_blank")
|
||||
String productType,
|
||||
@Schema(description = "产品SN")
|
||||
@NotBlank(message = "sale.device.validate.product_sn.not_blank")
|
||||
String productSn,
|
||||
@Schema(description = "MAC地址")
|
||||
String mac,
|
||||
@Schema(description = "序列号")
|
||||
String serialNum,
|
||||
@Schema(description = "软件版本")
|
||||
String softVersion,
|
||||
@Schema(description = "AL版本")
|
||||
String alVersion,
|
||||
@Schema(description = "AL编号")
|
||||
String alNum,
|
||||
@Schema(description = "AL状态")
|
||||
String alTxt,
|
||||
@Schema(description = "备注")
|
||||
String mark,
|
||||
@Schema(description = "客户ID")
|
||||
Integer customerId,
|
||||
@Schema(description = "维修备注")
|
||||
String repairMark
|
||||
) {}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.niuan.erp.module.sale.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "SN溯源查询参数DTO")
|
||||
public record DeviceQueryDto(
|
||||
@Schema(description = "搜索代码(SN/MAC/序列号)")
|
||||
String searchCode,
|
||||
@Schema(description = "客户ID")
|
||||
Integer keyAccountId
|
||||
) {}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.niuan.erp.module.sale.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "SN溯源查询结果DTO")
|
||||
public record DeviceResultDto(
|
||||
@Schema(description = "产品类型/型号")
|
||||
String productType,
|
||||
@Schema(description = "产品SN")
|
||||
String productSn,
|
||||
@Schema(description = "MAC地址")
|
||||
String mac,
|
||||
@Schema(description = "序列号")
|
||||
String serialNum,
|
||||
@Schema(description = "软件版本")
|
||||
String softVersion,
|
||||
@Schema(description = "算法版本")
|
||||
String alVersion,
|
||||
@Schema(description = "出库日期/出货日期")
|
||||
LocalDateTime outProductDate,
|
||||
@Schema(description = "返修记录拼接字符串")
|
||||
String repairRecords
|
||||
) {}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.niuan.erp.module.sale.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "设备验证项DTO")
|
||||
public record DeviceValidateItem(
|
||||
@Schema(description = "SN码")
|
||||
String productSn,
|
||||
@Schema(description = "MAC地址")
|
||||
String mac,
|
||||
@Schema(description = "序列号")
|
||||
String serialNum
|
||||
) {}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.niuan.erp.module.sale.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "设备验证请求DTO")
|
||||
public record DeviceValidateRequest(
|
||||
@Schema(description = "设备列表")
|
||||
List<DeviceValidateItem> devices
|
||||
) {}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.niuan.erp.module.sale.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "设备验证响应DTO")
|
||||
public record DeviceValidateResponse(
|
||||
@Schema(description = "是否验证通过")
|
||||
boolean valid,
|
||||
@Schema(description = "重复的SN码列表")
|
||||
List<String> duplicateProductSns,
|
||||
@Schema(description = "重复的MAC地址列表")
|
||||
List<String> duplicateMacs,
|
||||
@Schema(description = "重复的序列号列表")
|
||||
List<String> duplicateSerialNums,
|
||||
@Schema(description = "错误消息")
|
||||
String message
|
||||
) {}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.niuan.erp.module.sale.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "返修报表查询参数DTO")
|
||||
public record RepairRecordQueryDto(
|
||||
@Schema(description = "搜索代码(SN/MAC)")
|
||||
String searchCode,
|
||||
@Schema(description = "客户ID")
|
||||
Integer keyAccountIdSearch
|
||||
) {}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.niuan.erp.module.sale.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "返修报表查询结果DTO")
|
||||
public record RepairRecordResultDto(
|
||||
@Schema(description = "产品类型/型号")
|
||||
String productType,
|
||||
@Schema(description = "产品SN")
|
||||
String productSn,
|
||||
@Schema(description = "MAC地址")
|
||||
String mac,
|
||||
@Schema(description = "序列号")
|
||||
String serialNum,
|
||||
@Schema(description = "软件版本")
|
||||
String softVersion,
|
||||
@Schema(description = "算法版本")
|
||||
String alVersion,
|
||||
@Schema(description = "出库日期/出货日期")
|
||||
LocalDateTime outProductDate,
|
||||
@Schema(description = "创建时间")
|
||||
LocalDateTime createDate,
|
||||
@Schema(description = "维修次数")
|
||||
Integer repairCount,
|
||||
@Schema(description = "返修记录拼接字符串")
|
||||
String repairRecords
|
||||
) {}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.niuan.erp.module.sale.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 销售订单新增 DTO
|
||||
*/
|
||||
@Schema(description = "销售订单新增DTO")
|
||||
public record SaleOrderAddDto(
|
||||
@Schema(description = "单据编号")
|
||||
@NotBlank(message = "sale.sale_order.validate.form_code.not_null")
|
||||
String formCode,
|
||||
|
||||
@Schema(description = "单据名称")
|
||||
@NotBlank(message = "sale.sale_order.validate.form_name.not_null")
|
||||
String formName,
|
||||
|
||||
@Schema(description = "单据备注")
|
||||
String formMark,
|
||||
|
||||
@Schema(description = "客户ID")
|
||||
@NotNull(message = "sale.sale_order.validate.customer_id.not_null")
|
||||
Integer customerId,
|
||||
|
||||
@Schema(description = "客户名称")
|
||||
@NotBlank(message = "sale.sale_order.validate.customer_name.not_null")
|
||||
String customerName,
|
||||
|
||||
@Schema(description = "销售订单明细列表")
|
||||
@NotEmpty(message = "sale.sale_order.validate.sale_order_items.not_null")
|
||||
@Valid
|
||||
List<SaleOrderItemAddDto> saleOrderItems
|
||||
) {}
|
||||
@@ -10,6 +10,7 @@ import java.time.LocalDateTime;
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record SaleOrderDto(
|
||||
Long id,
|
||||
Integer status,
|
||||
LocalDateTime createDate,
|
||||
String formCode,
|
||||
String formName,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.niuan.erp.module.sale.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 销售订单明细新增 DTO
|
||||
*/
|
||||
@Schema(description = "销售订单明细新增DTO")
|
||||
public record SaleOrderItemAddDto(
|
||||
@Schema(description = "料号")
|
||||
@NotBlank(message = "sale.sale_order.validate.part_number.not_null")
|
||||
String partNumber,
|
||||
|
||||
@Schema(description = "产品规格")
|
||||
String productSpecs,
|
||||
|
||||
@Schema(description = "销售数量")
|
||||
@NotNull(message = "sale.sale_order.validate.sale_count.not_null")
|
||||
Integer saleCount,
|
||||
|
||||
@Schema(description = "单价")
|
||||
@NotNull(message = "sale.sale_order.validate.price.not_null")
|
||||
Double price,
|
||||
|
||||
@Schema(description = "备注")
|
||||
String saleMark
|
||||
) {}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.niuan.erp.module.sale.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 销售订单明细更新 DTO
|
||||
*/
|
||||
@Schema(description = "销售订单明细更新DTO")
|
||||
public record SaleOrderItemUpdateDto(
|
||||
@Schema(description = "明细ID")
|
||||
Long id,
|
||||
|
||||
@Schema(description = "料号")
|
||||
@NotBlank(message = "sale.sale_order.validate.part_number.not_null")
|
||||
String partNumber,
|
||||
|
||||
@Schema(description = "产品规格")
|
||||
String productSpecs,
|
||||
|
||||
@Schema(description = "销售数量")
|
||||
@NotNull(message = "sale.sale_order.validate.sale_count.not_null")
|
||||
Integer saleCount,
|
||||
|
||||
@Schema(description = "单价")
|
||||
@NotNull(message = "sale.sale_order.validate.price.not_null")
|
||||
Double price,
|
||||
|
||||
@Schema(description = "备注")
|
||||
String saleMark
|
||||
) {}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.niuan.erp.module.sale.controller.dto;
|
||||
|
||||
import com.niuan.erp.common.base.CommonValidateGroup;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 销售订单更新 DTO
|
||||
*/
|
||||
public record SaleOrderUpdateDto(
|
||||
@NotNull(message = "sale.sale_order.validate.id.not_null", groups = CommonValidateGroup.Update.class)
|
||||
Long id,
|
||||
|
||||
@NotBlank(message = "sale.sale_order.validate.form_name.not_null", groups = CommonValidateGroup.Update.class)
|
||||
String formName,
|
||||
|
||||
String formMark,
|
||||
|
||||
@NotNull(message = "sale.sale_order.validate.customer_id.not_null", groups = CommonValidateGroup.Update.class)
|
||||
Integer customerId,
|
||||
|
||||
@NotBlank(message = "sale.sale_order.validate.customer_name.not_null", groups = CommonValidateGroup.Update.class)
|
||||
String customerName,
|
||||
|
||||
@NotEmpty(message = "sale.sale_order.validate.sale_order_items.not_null", groups = CommonValidateGroup.Update.class)
|
||||
@Valid
|
||||
List<SaleOrderItemUpdateDto> saleOrderItems
|
||||
) {}
|
||||
@@ -14,6 +14,7 @@ import java.util.List;
|
||||
public interface SaleOrderConverter {
|
||||
|
||||
@Mapping(source = "id", target = "id")
|
||||
@Mapping(source = "status", target = "status")
|
||||
@Mapping(source = "createDate", target = "createDate")
|
||||
@Mapping(source = "formCode", target = "formCode")
|
||||
@Mapping(source = "formName", target = "formName")
|
||||
|
||||
@@ -263,6 +263,7 @@ public class SaleOrderServiceImpl extends ServiceImpl<DocumentMapper, Document>
|
||||
}
|
||||
|
||||
entity.setFormStatus(FormStatus.APPROVE);
|
||||
entity.setStatus(1);
|
||||
entity.setUpdateDate(LocalDateTime.now());
|
||||
entity.setUpdateUserId(SecurityUtils.getUserId());
|
||||
entity.setUpdateUserName(SecurityUtils.getUserName());
|
||||
@@ -336,6 +337,7 @@ public class SaleOrderServiceImpl extends ServiceImpl<DocumentMapper, Document>
|
||||
}
|
||||
|
||||
entity.setFormStatus(FormStatus.NO_APPROVE);
|
||||
entity.setStatus(0);
|
||||
entity.setUpdateDate(LocalDateTime.now());
|
||||
entity.setUpdateUserId(SecurityUtils.getUserId());
|
||||
entity.setUpdateUserName(SecurityUtils.getUserName());
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.niuan.erp.module.sys.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.niuan.erp.common.base.*;
|
||||
import com.niuan.erp.common.base.CommonValidateGroup.DeleteOne;
|
||||
import com.niuan.erp.module.sys.controller.dto.SysPermissionDto;
|
||||
import com.niuan.erp.module.sys.entity.SysPermission;
|
||||
import com.niuan.erp.module.sys.service.SysPermissionService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/sys/syspermission")
|
||||
@RequiredArgsConstructor
|
||||
public class SysPermissionController {
|
||||
|
||||
private final SysPermissionService sysPermissionService;
|
||||
|
||||
@GetMapping("/getSysPermissionPage")
|
||||
public BaseResult<IPage<SysPermissionDto>> getSysPermissionPage(BasePageReqParams dto, SysPermissionDto searchParams) {
|
||||
var wrapper = new LambdaQueryWrapper<SysPermission>();
|
||||
if (searchParams != null) {
|
||||
if (StringUtils.hasText(searchParams.permissionName())) {
|
||||
wrapper.like(SysPermission::getPermissionName, searchParams.permissionName());
|
||||
}
|
||||
if (searchParams.parentId() != null) {
|
||||
wrapper.eq(SysPermission::getParentId, searchParams.parentId());
|
||||
}
|
||||
}
|
||||
return BaseResult.successWithData(sysPermissionService.getSysPermissionPage(dto, wrapper));
|
||||
}
|
||||
|
||||
@GetMapping("/getPermissionTree")
|
||||
public BaseResult<List<BaseTree>> getPermissionTree() {
|
||||
return BaseResult.successWithData(sysPermissionService.getPermissionTree());
|
||||
}
|
||||
|
||||
@GetMapping("/getAllPermissionTree")
|
||||
public BaseResult<List<BaseTree>> getAllPermissionTree() {
|
||||
return BaseResult.successWithData(sysPermissionService.getAllPermissionTree());
|
||||
}
|
||||
|
||||
@PostMapping("/addSysPermission")
|
||||
public BaseResult<?> addSysPermission(@Valid @RequestBody SysPermissionDto dto) {
|
||||
sysPermissionService.addSysPermission(dto);
|
||||
return BaseResult.success();
|
||||
}
|
||||
|
||||
@PostMapping("/updateSysPermission")
|
||||
public BaseResult<?> updateSysPermission(@Valid @RequestBody SysPermissionDto dto) {
|
||||
sysPermissionService.updateSysPermission(dto);
|
||||
return BaseResult.success();
|
||||
}
|
||||
|
||||
@PostMapping("/deleteSysPermission")
|
||||
public BaseResult<?> deleteSysPermission(@Validated(DeleteOne.class) @RequestBody BaseDeleteBody req) {
|
||||
sysPermissionService.deleteSysPermission(req.id());
|
||||
return BaseResult.success();
|
||||
}
|
||||
|
||||
@PostMapping("/setStatus")
|
||||
public BaseResult<?> setStatus(@Valid @RequestBody BaseStatusBody req) {
|
||||
sysPermissionService.setStatus(req.id(), req.status());
|
||||
return BaseResult.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.niuan.erp.module.sys.controller.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record SysPermissionDto(
|
||||
Long id,
|
||||
Long parentId,
|
||||
Integer status,
|
||||
Boolean hidden,
|
||||
LocalDateTime createDate,
|
||||
Long createUserId,
|
||||
String createUserName,
|
||||
LocalDateTime updateDate,
|
||||
Long updateUserId,
|
||||
String updateUserName,
|
||||
String permissionName,
|
||||
String permissionI18n,
|
||||
Integer permissionType,
|
||||
String pageLink,
|
||||
String viewLink,
|
||||
String permissionCode,
|
||||
String eventName,
|
||||
String className,
|
||||
String iconName,
|
||||
Integer sort) {}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.niuan.erp.module.sys.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "用户搜索 DTO")
|
||||
public record SysUserSearchDto(
|
||||
@Schema(description = "登录账号")
|
||||
String loginName,
|
||||
|
||||
@Schema(description = "姓名")
|
||||
String userName
|
||||
) {}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.niuan.erp.module.sys.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "用户更新 DTO")
|
||||
public record SysUserUpdateDto(
|
||||
@Schema(description = "用户 ID")
|
||||
@NotNull(message = "sys.sysuser.validate.id.notNull")
|
||||
Long id,
|
||||
|
||||
@Schema(description = "用户数据")
|
||||
@NotNull(message = "sys.sysuser.validate.dto.notNull")
|
||||
SysUserDto dto
|
||||
) {}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.niuan.erp.module.sys.converter;
|
||||
|
||||
import com.niuan.erp.module.sys.controller.dto.SysPermissionDto;
|
||||
import com.niuan.erp.module.sys.entity.SysPermission;
|
||||
import com.niuan.erp.module.sys.enums.PermissionType;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
public interface SysPermissionConverter {
|
||||
SysPermission toEntity(SysPermissionDto dto);
|
||||
SysPermissionDto toDto(SysPermission entity);
|
||||
List<SysPermissionDto> toDtoList(List<SysPermission> entities);
|
||||
|
||||
default PermissionType map(Integer status) {
|
||||
return status == null ? null : PermissionType.fromCode(status);
|
||||
}
|
||||
|
||||
default Integer map(PermissionType status) {
|
||||
return status == null ? null : status.getCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.niuan.erp.module.sys.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 角色-权限关联表,用的是原有框架的 yy_sysrole
|
||||
* </p>
|
||||
*
|
||||
* @author
|
||||
* @since 2026-02-12
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("role_permission")
|
||||
public class RolePermission implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 角色ID
|
||||
*/
|
||||
@TableField("role_id")
|
||||
private Long roleId;
|
||||
|
||||
/**
|
||||
* 权限ID
|
||||
*/
|
||||
@TableField("permission_id")
|
||||
private Long permissionId;
|
||||
}
|
||||
147
src/main/java/com/niuan/erp/module/sys/entity/SysPermission.java
Normal file
147
src/main/java/com/niuan/erp/module/sys/entity/SysPermission.java
Normal file
@@ -0,0 +1,147 @@
|
||||
package com.niuan.erp.module.sys.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.niuan.erp.module.sys.enums.PermissionType;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 系统权限表
|
||||
* </p>
|
||||
*
|
||||
* @author
|
||||
* @since 2026-02-12
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("sys_permission")
|
||||
public class SysPermission implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 父权限ID,0表示根节点
|
||||
*/
|
||||
@TableField("parent_id")
|
||||
private Long parentId;
|
||||
|
||||
/**
|
||||
* 状态:0-禁用,1-启用
|
||||
*/
|
||||
@TableField("status")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField("create_date")
|
||||
private LocalDateTime createDate;
|
||||
|
||||
/**
|
||||
* 创建人ID
|
||||
*/
|
||||
@TableField("create_user_id")
|
||||
private Long createUserId;
|
||||
|
||||
/**
|
||||
* 创建人姓名
|
||||
*/
|
||||
@TableField("create_user_name")
|
||||
private String createUserName;
|
||||
|
||||
/**
|
||||
* 最后更新时间
|
||||
*/
|
||||
@TableField("update_date")
|
||||
private LocalDateTime updateDate;
|
||||
|
||||
/**
|
||||
* 更新人ID
|
||||
*/
|
||||
@TableField("update_user_id")
|
||||
private Long updateUserId;
|
||||
|
||||
/**
|
||||
* 更新人姓名
|
||||
*/
|
||||
@TableField("update_user_name")
|
||||
private String updateUserName;
|
||||
|
||||
/**
|
||||
* 权限名字,没有 i18n 时,作为显示名称
|
||||
*/
|
||||
@TableField("permission_name")
|
||||
private String permissionName;
|
||||
|
||||
/**
|
||||
* 权限 i18n 键,用于前端多语言取值
|
||||
*/
|
||||
@TableField("permission_i18n")
|
||||
private String permissionI18n;
|
||||
|
||||
/**
|
||||
* 权限类型,关系到权限的位置:0-菜单;1-Table 上方按钮;2-Table 操作栏按钮;3-状态栏按钮
|
||||
*/
|
||||
@TableField("permission_type")
|
||||
private PermissionType permissionType;
|
||||
|
||||
/**
|
||||
* 前端页面路由地址
|
||||
*/
|
||||
@TableField("page_link")
|
||||
private String pageLink;
|
||||
|
||||
/**
|
||||
* 前端 Vue 组件路径
|
||||
*/
|
||||
@TableField("view_link")
|
||||
private String viewLink;
|
||||
|
||||
/**
|
||||
* 后端鉴权用的权限编码
|
||||
*/
|
||||
@TableField("permission_code")
|
||||
private String permissionCode;
|
||||
|
||||
/**
|
||||
* 前端按钮绑定的方法名
|
||||
*/
|
||||
@TableField("event_name")
|
||||
private String eventName;
|
||||
|
||||
/**
|
||||
* 前端样式类名
|
||||
*/
|
||||
@TableField("class_name")
|
||||
private String className;
|
||||
|
||||
/**
|
||||
* 前端图标名称
|
||||
*/
|
||||
@TableField("icon_name")
|
||||
private String iconName;
|
||||
|
||||
/**
|
||||
* 排序值,越小越靠前
|
||||
*/
|
||||
@TableField("sort")
|
||||
private Integer sort;
|
||||
|
||||
/**
|
||||
* 是否隐藏
|
||||
*/
|
||||
@TableField("hidden")
|
||||
private Boolean hidden;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.niuan.erp.module.sys.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.niuan.erp.module.sys.entity.RolePermission;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 角色-权限关联表,用的是原有框架的 yy_sysrole Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author
|
||||
* @since 2026-02-12
|
||||
*/
|
||||
public interface RolePermissionMapper extends BaseMapper<RolePermission> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.niuan.erp.module.sys.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.niuan.erp.module.sys.entity.SysPermission;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 系统权限表 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author
|
||||
* @since 2026-02-12
|
||||
*/
|
||||
public interface SysPermissionMapper extends BaseMapper<SysPermission> {
|
||||
|
||||
List<SysPermission> selectByUserId(Long userId);
|
||||
|
||||
List<Long> selectPermissionIdByRoleId(Long roleId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.niuan.erp.module.sys.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.niuan.erp.common.base.BasePageReqParams;
|
||||
import com.niuan.erp.common.base.BaseTree;
|
||||
import com.niuan.erp.module.sys.controller.dto.SysPermissionDto;
|
||||
import com.niuan.erp.module.sys.entity.SysPermission;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SysPermissionService {
|
||||
|
||||
IPage<SysPermissionDto> getSysPermissionPage(BasePageReqParams pageParams, LambdaQueryWrapper<SysPermission> wrapper);
|
||||
|
||||
void addSysPermission(SysPermissionDto dto);
|
||||
|
||||
void updateSysPermission(SysPermissionDto dto);
|
||||
|
||||
void deleteSysPermission(long id);
|
||||
|
||||
void deleteBatch(List<Long> ids);
|
||||
|
||||
void setStatus(Long id, Integer status);
|
||||
|
||||
List<BaseTree> getPermissionTree();
|
||||
|
||||
List<BaseTree> getAllPermissionTree();
|
||||
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public class KeyAccountServiceImpl extends ServiceImpl<KeyAccountMapper, KeyAcco
|
||||
@Override
|
||||
public List<BaseTree> getKeyAccountTree() {
|
||||
LambdaQueryWrapper<KeyAccount> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(KeyAccount::getStatus, 1);
|
||||
wrapper.eq(KeyAccount::getStatus, 0);
|
||||
wrapper.orderByAsc(KeyAccount::getLevelPath);
|
||||
List<KeyAccount> keyAccounts = this.baseMapper.selectList(wrapper);
|
||||
|
||||
@@ -77,7 +77,7 @@ public class KeyAccountServiceImpl extends ServiceImpl<KeyAccountMapper, KeyAcco
|
||||
entity.setCreateUserName(SecurityUtils.getUserName());
|
||||
entity.setCreateDate(LocalDateTime.now());
|
||||
entity.setCustomerId(Long.valueOf(SecurityUtils.getLoginUser().getUser().getCustomerId()));
|
||||
entity.setStatus(1);
|
||||
entity.setStatus(0);
|
||||
this.baseMapper.insert(entity);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package com.niuan.erp.module.sys.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.niuan.erp.common.base.BasePageReqParams;
|
||||
import com.niuan.erp.common.base.BaseTree;
|
||||
import com.niuan.erp.common.security.TokenService;
|
||||
import com.niuan.erp.common.utils.SecurityUtils;
|
||||
import com.niuan.erp.module.sys.controller.dto.SysPermissionDto;
|
||||
import com.niuan.erp.module.sys.converter.SysPermissionConverter;
|
||||
import com.niuan.erp.module.sys.entity.SysPermission;
|
||||
import com.niuan.erp.module.sys.enums.PermissionType;
|
||||
import com.niuan.erp.module.sys.mapper.SysPermissionMapper;
|
||||
import com.niuan.erp.module.sys.service.SysPermissionService;
|
||||
import com.niuan.erp.module.sys.service.SysUserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class SysPermissionServiceImpl extends ServiceImpl<SysPermissionMapper, SysPermission> implements SysPermissionService {
|
||||
|
||||
|
||||
private final SysPermissionConverter sysPermissionConverter;
|
||||
|
||||
private final TokenService tokenService;
|
||||
|
||||
private final SysUserService sysUserService;
|
||||
|
||||
@Override
|
||||
public IPage<SysPermissionDto> getSysPermissionPage(BasePageReqParams pageParams, LambdaQueryWrapper<SysPermission> wrapper) {
|
||||
IPage<SysPermission> result = this.baseMapper.selectPage(new Page<>(pageParams.page(), pageParams.pageSize()), wrapper);
|
||||
return result.convert(sysPermissionConverter::toDto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSysPermission(SysPermissionDto dto) {
|
||||
SysPermission entity = sysPermissionConverter.toEntity(dto);
|
||||
entity.setCreateUserId(SecurityUtils.getUserId());
|
||||
entity.setCreateUserName(SecurityUtils.getUserName());
|
||||
entity.setCreateDate(LocalDateTime.now());
|
||||
entity.setStatus(0);
|
||||
this.baseMapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSysPermission(SysPermissionDto dto) {
|
||||
SysPermission entity = sysPermissionConverter.toEntity(dto);
|
||||
entity.setUpdateUserId(SecurityUtils.getUserId());
|
||||
entity.setUpdateUserName(SecurityUtils.getUserName());
|
||||
entity.setUpdateDate(LocalDateTime.now());
|
||||
this.baseMapper.updateById(entity);
|
||||
// 权限修改后,踢掉所有拥有该权限的用户
|
||||
List<Long> userIds = sysUserService.getUserIdsByPermissionId(entity.getId());
|
||||
if (userIds != null && !userIds.isEmpty()) {
|
||||
userIds.forEach(tokenService::removeAllUserTokens);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSysPermission(long id) {
|
||||
// 权限删除前,踢掉所有拥有该权限的用户
|
||||
List<Long> userIds = sysUserService.getUserIdsByPermissionId(id);
|
||||
if (userIds != null && !userIds.isEmpty()) {
|
||||
userIds.forEach(tokenService::removeAllUserTokens);
|
||||
}
|
||||
this.baseMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBatch(List<Long> ids) {
|
||||
if (ids != null && !ids.isEmpty()) {
|
||||
// 批量删除前,踢掉所有拥有这些权限的用户
|
||||
for (Long permissionId : ids) {
|
||||
List<Long> userIds = sysUserService.getUserIdsByPermissionId(permissionId);
|
||||
if (userIds != null && !userIds.isEmpty()) {
|
||||
userIds.forEach(tokenService::removeAllUserTokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.baseMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatus(Long id, Integer status) {
|
||||
SysPermission entity = new SysPermission();
|
||||
entity.setId(id);
|
||||
entity.setStatus(status);
|
||||
entity.setUpdateUserId(SecurityUtils.getUserId());
|
||||
entity.setUpdateUserName(SecurityUtils.getUserName());
|
||||
entity.setUpdateDate(LocalDateTime.now());
|
||||
this.baseMapper.updateById(entity);
|
||||
// 权限状态修改后,踢掉所有拥有该权限的用户
|
||||
List<Long> userIds = sysUserService.getUserIdsByPermissionId(id);
|
||||
if (userIds != null && !userIds.isEmpty()) {
|
||||
userIds.forEach(tokenService::removeAllUserTokens);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BaseTree> getPermissionTree() {
|
||||
// 查询所有权限
|
||||
List<SysPermission> allPermissions = this.baseMapper.selectList(
|
||||
new LambdaQueryWrapper<SysPermission>()
|
||||
.orderByAsc(SysPermission::getSort)
|
||||
);
|
||||
// 构建树形结构,只到倒数第二层
|
||||
return buildMenuTree(allPermissions, 0L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BaseTree> getAllPermissionTree() {
|
||||
// 查询所有权限(包括菜单和按钮)
|
||||
List<SysPermission> allPermissions = this.baseMapper.selectList(
|
||||
new LambdaQueryWrapper<SysPermission>()
|
||||
.orderByAsc(SysPermission::getSort)
|
||||
);
|
||||
// 构建完整树形结构
|
||||
return buildTree(allPermissions, 0L);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归构建菜单权限树
|
||||
* @param permissions 所有权限列表
|
||||
* @param parentId 父节点ID
|
||||
* @return 树形结构
|
||||
*/
|
||||
private List<BaseTree> buildMenuTree(List<SysPermission> permissions, Long parentId) {
|
||||
if (permissions == null || permissions.isEmpty()) return List.of();
|
||||
|
||||
var result = new ArrayList<BaseTree>();
|
||||
var permissionMap = new HashMap<Long, BaseTree>();
|
||||
|
||||
permissions.forEach(permission -> {
|
||||
if (!permission.getPermissionType().equals(PermissionType.MENU)) return;
|
||||
handlePermission(parentId, result, permissionMap, permission);
|
||||
});
|
||||
|
||||
// 检查这些子节点是否还有子节点(即是否是倒数第二层)
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归构建权限树
|
||||
* @param permissions 所有权限列表
|
||||
* @param parentId 父节点ID
|
||||
* @return 树形结构
|
||||
*/
|
||||
private List<BaseTree> buildTree(List<SysPermission> permissions, Long parentId) {
|
||||
if (permissions == null || permissions.isEmpty()) return List.of();
|
||||
|
||||
var result = new ArrayList<BaseTree>();
|
||||
var permissionMap = new HashMap<Long, BaseTree>();
|
||||
|
||||
permissions.forEach(permission -> {
|
||||
handlePermission(parentId, result, permissionMap, permission);
|
||||
});
|
||||
|
||||
// 检查这些子节点是否还有子节点(即是否是倒数第二层)
|
||||
return result;
|
||||
}
|
||||
|
||||
private void handlePermission(Long parentId, ArrayList<BaseTree> result, HashMap<Long, BaseTree> permissionMap, SysPermission permission) {
|
||||
var node = new BaseTree(permission.getId(), permission.getPermissionName(), null);
|
||||
if (permission.getParentId().equals(parentId)) {
|
||||
result.add(node);
|
||||
} else {
|
||||
if (permissionMap.containsKey(permission.getParentId())) {
|
||||
var child = permissionMap.get(permission.getParentId()).getChildren();
|
||||
if (child == null) {
|
||||
var list = new ArrayList<BaseTree>();
|
||||
list.add(node);
|
||||
permissionMap.get(permission.getParentId())
|
||||
.setChildren(list);
|
||||
} else {
|
||||
child.add(node);
|
||||
}
|
||||
} else {
|
||||
var list = new ArrayList<BaseTree>();
|
||||
list.add(node);
|
||||
permissionMap.put(permission.getParentId(), new BaseTree(permission.getParentId(),
|
||||
null,
|
||||
list));
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionMap.containsKey(permission.getId())) {
|
||||
node.setChildren(permissionMap.get(permission.getId()).getChildren());
|
||||
permissionMap.put(permission.getId(), node);
|
||||
} else {
|
||||
permissionMap.put(permission.getId(), node);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.niuan.erp.module.warehouse.controller.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record InventoryCountAddDto(
|
||||
@NotNull(message = "warehouse.inventory_count.validate.form_code.not_null")
|
||||
String formCode,
|
||||
@NotNull(message = "warehouse.inventory_count.validate.form_name.not_null")
|
||||
String formName,
|
||||
String formMark,
|
||||
|
||||
@NotNull(message = "warehouse.inventory_count.validate.store_no.not_null")
|
||||
Integer storeNo,
|
||||
@NotNull(message = "warehouse.inventory_count.validate.store_name.not_null")
|
||||
String storeName,
|
||||
|
||||
Integer isInit,
|
||||
|
||||
@NotNull(message = "warehouse.inventory_count.validate.count_items.not_null")
|
||||
List<InventoryCountItemAddDto> countItems
|
||||
) {}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.niuan.erp.module.warehouse.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 盘点单明细新增 DTO
|
||||
*/
|
||||
@Schema(description = "盘点单明细新增DTO")
|
||||
public record InventoryCountItemAddDto(
|
||||
@Schema(description = "料号")
|
||||
@NotBlank(message = "warehouse.inventory_count.validate.part_number.not_blank")
|
||||
String partNumber,
|
||||
|
||||
@Schema(description = "产品数量")
|
||||
@NotNull(message = "warehouse.inventory_count.validate.product_count.not_null")
|
||||
Integer productCount,
|
||||
|
||||
@Schema(description = "产品规格")
|
||||
String productSpec
|
||||
) {}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.niuan.erp.module.warehouse.controller.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public record ProductVendorMapAddDto(
|
||||
@Schema(description = "物料ID")
|
||||
@NotNull(message = "{warehouse.warehouse_item.validate.id.not_null}")
|
||||
Long warehouseItemId,
|
||||
@Schema(description = "供应商映射列表")
|
||||
@Valid
|
||||
List<ProductVendorMapItemDto> vendorList) {
|
||||
|
||||
public record ProductVendorMapItemDto(
|
||||
@Schema(description = "供应商ID")
|
||||
@NotNull(message = "{warehouse.warehouse_item.validate.vendor_id.not_null}")
|
||||
Long vendorId,
|
||||
@Schema(description = "成本价")
|
||||
@DecimalMin(value = "0", message = "{warehouse.warehouse_item.validate.cost_price.min}")
|
||||
BigDecimal costPrice) {}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.niuan.erp.module.warehouse.controller.dto;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record ProductVendorMapDto(
|
||||
Long id,
|
||||
String partNumber,
|
||||
Long vendorId,
|
||||
String vendorName,
|
||||
String contactPerson,
|
||||
String tel,
|
||||
String address,
|
||||
@DecimalMin(value = "0", message = "{warehouse.warehouse_item.validate.cost_price.min}")
|
||||
BigDecimal costPrice,
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
LocalDateTime procureDate) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.niuan.erp.module.warehouse.controller.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record StockCheckDto(
|
||||
Integer storeNo,
|
||||
List<StockCheckItem> items
|
||||
) {
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record StockCheckItem(
|
||||
String partNumber,
|
||||
Integer requiredCount
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.niuan.erp.module.warehouse.controller.dto;
|
||||
|
||||
public record StockTransferOrderItemDto(
|
||||
Long id,
|
||||
String partNumber,
|
||||
String productSpecs,
|
||||
Integer productCount,
|
||||
Integer demandCount
|
||||
) {}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.niuan.erp.module.warehouse.controller.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record WarehouseReceiptAddDto(
|
||||
@NotNull(message = "warehouse.warehouse_receipt.validate.form_code.not_null")
|
||||
String formCode,
|
||||
@NotNull(message = "warehouse.warehouse_receipt.validate.form_name.not_null")
|
||||
String formName,
|
||||
String formMark,
|
||||
|
||||
@NotNull(message = "warehouse.warehouse_receipt.validate.store_no.not_null")
|
||||
Integer storeNo,
|
||||
@NotNull(message = "warehouse.warehouse_receipt.validate.store_name.not_null")
|
||||
String storeName,
|
||||
|
||||
@NotNull(message = "warehouse.warehouse_receipt.validate.receipt_items.not_null")
|
||||
List<WarehouseReceiptItemAddDto> receiptItems
|
||||
) {}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.niuan.erp.module.warehouse.controller.dto;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record WarehouseReceiptItemAddDto(
|
||||
@NotNull(message = "warehouse.warehouse_receipt.validate.part_number.not_null")
|
||||
String partNumber,
|
||||
@NotNull(message = "warehouse.warehouse_receipt.validate.product_spec.not_null")
|
||||
String productSpec,
|
||||
@NotNull(message = "warehouse.warehouse_receipt.validate.product_count.not_null")
|
||||
@Min(value = 1, message = "warehouse.warehouse_receipt.validate.product_count.min")
|
||||
Integer productCount,
|
||||
@NotNull(message = "warehouse.warehouse_receipt.validate.part_id.not_null")
|
||||
Long partId
|
||||
) {}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.niuan.erp.module.warehouse.controller.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record WarehouseReceiptItemDto(
|
||||
Long id,
|
||||
Integer status,
|
||||
LocalDateTime createDate,
|
||||
Long createUserId,
|
||||
String createUserName,
|
||||
LocalDateTime updateDate,
|
||||
Long updateUserId,
|
||||
String updateUserName,
|
||||
Integer documentNo,
|
||||
String partNumber,
|
||||
Integer originalCount,
|
||||
Integer productCount,
|
||||
String productMark,
|
||||
Integer reserve1,
|
||||
String reserve2,
|
||||
Integer storeNo,
|
||||
Integer demandCount,
|
||||
Long partId,
|
||||
String productSpec
|
||||
) {}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.niuan.erp.module.warehouse.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 lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 产品供应商映射表
|
||||
* </p>
|
||||
*
|
||||
* @author
|
||||
* @since 2026-03-03
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("productvendormap")
|
||||
public class ProductVendorMap implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "Id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@TableField("PartNumber")
|
||||
private String partNumber;
|
||||
|
||||
@TableField("VendorId")
|
||||
private Long vendorId;
|
||||
|
||||
@TableField("CostPrice")
|
||||
private BigDecimal costPrice;
|
||||
|
||||
@TableField("ProcureDate")
|
||||
private LocalDateTime procureDate;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.niuan.erp.module.warehouse.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.niuan.erp.module.warehouse.entity.ProductVendorMap;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 产品供应商映射表 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author
|
||||
* @since 2026-03-03
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProductVendorMapMapper extends BaseMapper<ProductVendorMap> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.niuan.erp.module.warehouse.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.niuan.erp.module.warehouse.entity.ProductVendorMap;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 产品供应商映射表 服务类
|
||||
* </p>
|
||||
*
|
||||
* @author
|
||||
* @since 2026-03-03
|
||||
*/
|
||||
public interface ProductVendorMapService extends IService<ProductVendorMap> {
|
||||
|
||||
/**
|
||||
* 根据产品编号获取默认供应商
|
||||
*
|
||||
* @param partNumber 产品编号
|
||||
* @return 产品供应商映射
|
||||
*/
|
||||
Optional<ProductVendorMap> getDefaultVendorByPartNumber(String partNumber);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.niuan.erp.module.warehouse.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.niuan.erp.module.warehouse.entity.ProductVendorMap;
|
||||
import com.niuan.erp.module.warehouse.mapper.ProductVendorMapMapper;
|
||||
import com.niuan.erp.module.warehouse.service.ProductVendorMapService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 产品供应商映射表 服务实现类
|
||||
* </p>
|
||||
*
|
||||
* @author
|
||||
* @since 2026-03-03
|
||||
*/
|
||||
@Service
|
||||
@Transactional
|
||||
public class ProductVendorMapServiceImpl extends ServiceImpl<ProductVendorMapMapper, ProductVendorMap> implements ProductVendorMapService {
|
||||
|
||||
@Override
|
||||
public Optional<ProductVendorMap> getDefaultVendorByPartNumber(String partNumber) {
|
||||
var wrapper = new LambdaQueryWrapper<ProductVendorMap>()
|
||||
.eq(ProductVendorMap::getPartNumber, partNumber)
|
||||
// 按最后采购时间倒序,获取最近的供应商
|
||||
.orderByDesc(ProductVendorMap::getProcureDate)
|
||||
.last("LIMIT 1");
|
||||
return Optional.ofNullable(getOne(wrapper));
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
</resultMap>
|
||||
|
||||
<select id="getKeyAccountSelectList" resultType="com.niuan.erp.common.base.BaseSelectDto">
|
||||
SELECT Id, KeyAccountName FROM keyaccount WHERE Status = 1
|
||||
SELECT Id, KeyAccountName FROM keyaccount WHERE Status = 0
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user