feat: 1. 新加入更改权限踢人功能。2. 使用几个 AI 工具测试工具效果。

This commit is contained in:
c
2026-03-18 15:29:22 +08:00
parent 8bb7dcca31
commit e142d099cc
75 changed files with 3524 additions and 3 deletions

1
.gitignore vendored
View File

@@ -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
View File

@@ -0,0 +1,450 @@
# JWT + Redis + Session 双认证方式使用文档
## 概述
本文档描述了ERP系统中同时支持两种认证方式的使用说明
- **Web端后台管理**传统Session方式基于Cookie
- **小程序端**JWT方式基于Token存储于Redis
两种认证方式同时启用,互不影响。
## 1. 认证方式对比
| 特性 | Web端Session | 小程序JWT |
|------|-----------------|--------------|
| 认证机制 | Cookie + Session | TokenHeader |
| 登录接口 | 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();
};
```

View 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);
}

View 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);
}
}

View 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;
}
}

View 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);
}
}

View File

@@ -23,6 +23,7 @@ public class CustomerTenantHandler implements TenantLineHandler {
customerTables.add("storage_list");
customerTables.add("bom_list");
customerTables.add("vendor");
customerTables.add("keyaccount");
}
@Override

View File

@@ -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");
}
}

View File

@@ -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";
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View 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) {
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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
) {
}
}

View File

@@ -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检查SessionWeb端
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) {
}
}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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) {}

View File

@@ -0,0 +1,7 @@
package com.niuan.erp.module.production.controller.dto;
public record ProductionReturnItemDto(
String partNumber,
String productSpecs,
Integer returnQty
) {}

View File

@@ -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;
}
}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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,

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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")

View File

@@ -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());

View File

@@ -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();
}
}

View File

@@ -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) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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();
}
}

View File

@@ -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;
}

View 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;
/**
* 父权限ID0表示根节点
*/
@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;
}

View File

@@ -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> {
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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) {}
}

View File

@@ -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) {}

View File

@@ -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
) {}
}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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;
}

View File

@@ -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> {
}

View File

@@ -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);
}

View File

@@ -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));
}
}

View File

@@ -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>