diff --git a/.gitignore b/.gitignore index c2065bc..078733b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ HELP.md .gradle build/ +gradle-repository !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ diff --git a/docs/jwt-redis-usage.md b/docs/jwt-redis-usage.md new file mode 100644 index 0000000..dbff1e3 --- /dev/null +++ b/docs/jwt-redis-usage.md @@ -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 +// 设置缓存 + void set(String key, T value); + +// 设置缓存并设置过期时间 + void set(String key, T value, long timeout, TimeUnit unit); + +// 获取缓存 + T get(String key); + +// 删除缓存 +Boolean delete(String key); + +// 判断key是否存在 +Boolean hasKey(String key); +``` + +#### Hash操作 +```java +// 设置字段值 + void hSet(String key, String field, T value); + +// 获取字段值 + T hGet(String key, String field); + +// 获取所有字段和值 + Map hGetAll(String key); + +// 删除字段 +Long hDelete(String key, Object... fields); +``` + +#### Set操作 +```java +// 添加成员 + Long sAdd(String key, T... members); + +// 获取所有成员 + Set sMembers(String key); + +// 删除成员 + 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(); +}; +``` diff --git a/src/main/java/com/niuan/erp/common/cache/CacheService.java b/src/main/java/com/niuan/erp/common/cache/CacheService.java new file mode 100644 index 0000000..00fdeed --- /dev/null +++ b/src/main/java/com/niuan/erp/common/cache/CacheService.java @@ -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 { + + /** + * 设置缓存 + */ + void set(String key, T value); + + /** + * 设置缓存并设置过期时间 + */ + void set(String key, T value, long timeout, TimeUnit unit); + + /** + * 获取缓存 + */ + T get(String key); + + /** + * 删除缓存 + */ + Boolean delete(String key); + + /** + * 批量删除缓存 + */ + Long delete(Collection keys); + + /** + * 判断key是否存在 + */ + Boolean hasKey(String key); + + /** + * 设置过期时间 + */ + Boolean expire(String key, long timeout, TimeUnit unit); + + /** + * 获取过期时间 + */ + Long getExpire(String key, TimeUnit unit); + + /** + * 获取匹配的所有key + */ + Set keys(String pattern); + + /** + * Hash操作:设置字段值 + */ + void hSet(String key, String field, T value); + + /** + * Hash操作:获取字段值 + */ + T hGet(String key, String field); + + /** + * Hash操作:获取所有字段和值 + */ + Map hGetAll(String key); + + /** + * Hash操作:删除字段 + */ + Long hDelete(String key, Object... fields); + + /** + * Hash操作:判断字段是否存在 + */ + Boolean hHasKey(String key, String field); + + /** + * List操作:左侧推入 + */ + Long lPush(String key, T value); + + /** + * List操作:右侧弹出 + */ + T rPop(String key); + + /** + * List操作:获取列表范围 + */ + List lRange(String key, long start, long end); + + /** + * Set操作:添加成员 + */ + Long sAdd(String key, T... members); + + /** + * Set操作:获取所有成员 + */ + Set sMembers(String key); + + /** + * Set操作:删除成员 + */ + Long sRemove(String key, T... members); + + /** + * Set操作:判断是否是成员 + */ + Boolean sIsMember(String key, T member); + + /** + * 自增操作 + */ + Long increment(String key); + + /** + * 自增指定值 + */ + Long increment(String key, long delta); +} diff --git a/src/main/java/com/niuan/erp/common/cache/RedisCacheServiceImpl.java b/src/main/java/com/niuan/erp/common/cache/RedisCacheServiceImpl.java new file mode 100644 index 0000000..3a3f49b --- /dev/null +++ b/src/main/java/com/niuan/erp/common/cache/RedisCacheServiceImpl.java @@ -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 redisTemplate; + + @Override + public void set(String key, T value) { + redisTemplate.opsForValue().set(key, value); + } + + @Override + public void set(String key, T value, long timeout, TimeUnit unit) { + redisTemplate.opsForValue().set(key, value, timeout, unit); + } + + @SuppressWarnings("unchecked") + @Override + public 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 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 keys(String pattern) { + return redisTemplate.keys(pattern); + } + + @Override + public void hSet(String key, String field, T value) { + redisTemplate.opsForHash().put(key, field, value); + } + + @SuppressWarnings("unchecked") + @Override + public T hGet(String key, String field) { + return (T) redisTemplate.opsForHash().get(key, field); + } + + @SuppressWarnings("unchecked") + @Override + public Map hGetAll(String key) { + Map entries = redisTemplate.opsForHash().entries(key); + Map result = new java.util.HashMap<>(); + for (Map.Entry 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 Long lPush(String key, T value) { + return redisTemplate.opsForList().leftPush(key, value); + } + + @SuppressWarnings("unchecked") + @Override + public T rPop(String key) { + return (T) redisTemplate.opsForList().rightPop(key); + } + + @SuppressWarnings("unchecked") + @Override + public List lRange(String key, long start, long end) { + return (List) redisTemplate.opsForList().range(key, start, end); + } + + @Override + public Long sAdd(String key, T... members) { + return redisTemplate.opsForSet().add(key, (Object[]) members); + } + + @SuppressWarnings("unchecked") + @Override + public Set sMembers(String key) { + return (Set) redisTemplate.opsForSet().members(key); + } + + @Override + public Long sRemove(String key, T... members) { + return redisTemplate.opsForSet().remove(key, (Object[]) members); + } + + @Override + public 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); + } +} diff --git a/src/main/java/com/niuan/erp/common/config/RedisConfig.java b/src/main/java/com/niuan/erp/common/config/RedisConfig.java new file mode 100644 index 0000000..ff32d2a --- /dev/null +++ b/src/main/java/com/niuan/erp/common/config/RedisConfig.java @@ -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 redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate 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 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; + } +} diff --git a/src/main/java/com/niuan/erp/common/config/SessionConfig.java b/src/main/java/com/niuan/erp/common/config/SessionConfig.java new file mode 100644 index 0000000..67cecf5 --- /dev/null +++ b/src/main/java/com/niuan/erp/common/config/SessionConfig.java @@ -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); + } +} diff --git a/src/main/java/com/niuan/erp/common/handler/CustomerTenantHandler.java b/src/main/java/com/niuan/erp/common/handler/CustomerTenantHandler.java index 75f4bd7..0bb3b4a 100644 --- a/src/main/java/com/niuan/erp/common/handler/CustomerTenantHandler.java +++ b/src/main/java/com/niuan/erp/common/handler/CustomerTenantHandler.java @@ -23,6 +23,7 @@ public class CustomerTenantHandler implements TenantLineHandler { customerTables.add("storage_list"); customerTables.add("bom_list"); customerTables.add("vendor"); + customerTables.add("keyaccount"); } @Override diff --git a/src/main/java/com/niuan/erp/common/security/JwtAuthenticationFilter.java b/src/main/java/com/niuan/erp/common/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..1f681a8 --- /dev/null +++ b/src/main/java/com/niuan/erp/common/security/JwtAuthenticationFilter.java @@ -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"); + } +} diff --git a/src/main/java/com/niuan/erp/common/security/JwtProperties.java b/src/main/java/com/niuan/erp/common/security/JwtProperties.java new file mode 100644 index 0000000..b86785b --- /dev/null +++ b/src/main/java/com/niuan/erp/common/security/JwtProperties.java @@ -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"; +} diff --git a/src/main/java/com/niuan/erp/common/security/JwtTokenProvider.java b/src/main/java/com/niuan/erp/common/security/JwtTokenProvider.java new file mode 100644 index 0000000..552cbb3 --- /dev/null +++ b/src/main/java/com/niuan/erp/common/security/JwtTokenProvider.java @@ -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 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 claims = new HashMap<>(); + claims.put("userId", loginUser.getUser().getId()); + claims.put("tokenType", "refresh"); + + return buildToken(claims, jwtProperties.getRefreshExpiration()); + } + + /** + * 构建令牌 + */ + private String buildToken(Map 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; + } +} diff --git a/src/main/java/com/niuan/erp/common/security/SessionKickService.java b/src/main/java/com/niuan/erp/common/security/SessionKickService.java new file mode 100644 index 0000000..a790698 --- /dev/null +++ b/src/main/java/com/niuan/erp/common/security/SessionKickService.java @@ -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 allPrincipals = sessionRegistry.getAllPrincipals(); + for (Object principal : allPrincipals) { + if (principal instanceof LoginUser loginUser) { + if (loginUser.getUser().getId().equals(userId)) { + // 找到匹配的用户,踢掉所有Session + List 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); + } + } +} diff --git a/src/main/java/com/niuan/erp/common/security/TokenService.java b/src/main/java/com/niuan/erp/common/security/TokenService.java new file mode 100644 index 0000000..9df861d --- /dev/null +++ b/src/main/java/com/niuan/erp/common/security/TokenService.java @@ -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 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 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) { + } +} diff --git a/src/main/java/com/niuan/erp/common/security/UserTokenService.java b/src/main/java/com/niuan/erp/common/security/UserTokenService.java new file mode 100644 index 0000000..d9c2c7b --- /dev/null +++ b/src/main/java/com/niuan/erp/common/security/UserTokenService.java @@ -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); + } +} diff --git a/src/main/java/com/niuan/erp/module/api/controller/ApiDemoController.java b/src/main/java/com/niuan/erp/module/api/controller/ApiDemoController.java new file mode 100644 index 0000000..3c3141f --- /dev/null +++ b/src/main/java/com/niuan/erp/module/api/controller/ApiDemoController.java @@ -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> refreshToken( + @Parameter(description = "刷新令牌") @RequestParam String refreshToken) { + // 实际使用时需要加载用户信息 + // TokenService.TokenPair tokenPair = tokenService.refreshTokens(refreshToken, loginUser); + + Map result = new HashMap<>(); + result.put("message", "刷新令牌示例 - 实际使用时需要传入用户信息"); + return BaseResult.successWithData(result); + } + + /** + * 演示:将令牌加入黑名单(登出) + */ + @PostMapping("/logout") + @Operation(summary = "登出", description = "将当前令牌加入黑名单") + public BaseResult logout( + @Parameter(description = "访问令牌") @RequestParam String token) { + tokenService.addToBlacklist(token); + return BaseResult.success(); + } + + /** + * 演示:检查令牌是否在黑名单中 + */ + @GetMapping("/check-blacklist") + @Operation(summary = "检查令牌黑名单", description = "检查指定令牌是否在黑名单中") + public BaseResult> checkBlacklist( + @Parameter(description = "访问令牌") @RequestParam String token) { + boolean isBlacklisted = tokenService.isTokenBlacklisted(token); + Map result = new HashMap<>(); + result.put("isBlacklisted", isBlacklisted); + return BaseResult.successWithData(result); + } + + /** + * 演示:Redis缓存基本操作 + */ + @PostMapping("/cache/set") + @Operation(summary = "设置缓存", description = "将数据存入Redis缓存") + public BaseResult 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> getCache( + @Parameter(description = "缓存键") @RequestParam String key) { + Object value = cacheService.get(key); + Map 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> deleteCache( + @Parameter(description = "缓存键") @RequestParam String key) { + Boolean deleted = cacheService.delete(key); + Map 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 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> getHashCache( + @Parameter(description = "Hash键") @RequestParam String key, + @Parameter(description = "字段名") @RequestParam String field) { + Object value = cacheService.hGet(key, field); + Map 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> getUserTokens( + @Parameter(description = "用户ID") @RequestParam Long userId) { + Set tokens = tokenService.getUserTokens(userId); + Map 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 kickUser( + @Parameter(description = "用户ID") @RequestParam Long userId) { + tokenService.removeAllUserTokens(userId); + return BaseResult.success(); + } +} diff --git a/src/main/java/com/niuan/erp/module/api/controller/MiniProgramAuthController.java b/src/main/java/com/niuan/erp/module/api/controller/MiniProgramAuthController.java new file mode 100644 index 0000000..b6d9e63 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/api/controller/MiniProgramAuthController.java @@ -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 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 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 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 + ) { + } +} diff --git a/src/main/java/com/niuan/erp/module/open/controller/OpenController.java b/src/main/java/com/niuan/erp/module/open/controller/OpenController.java new file mode 100644 index 0000000..f55a6b6 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/open/controller/OpenController.java @@ -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 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) { + } +} diff --git a/src/main/java/com/niuan/erp/module/production/controller/dto/DeviceOutstockItem.java b/src/main/java/com/niuan/erp/module/production/controller/dto/DeviceOutstockItem.java new file mode 100644 index 0000000..3e05c3f --- /dev/null +++ b/src/main/java/com/niuan/erp/module/production/controller/dto/DeviceOutstockItem.java @@ -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 +) {} \ No newline at end of file diff --git a/src/main/java/com/niuan/erp/module/production/controller/dto/DeviceOutstockRequest.java b/src/main/java/com/niuan/erp/module/production/controller/dto/DeviceOutstockRequest.java new file mode 100644 index 0000000..ad54b7d --- /dev/null +++ b/src/main/java/com/niuan/erp/module/production/controller/dto/DeviceOutstockRequest.java @@ -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 outstockList +) {} \ No newline at end of file diff --git a/src/main/java/com/niuan/erp/module/production/controller/dto/DeviceShipmentDto.java b/src/main/java/com/niuan/erp/module/production/controller/dto/DeviceShipmentDto.java new file mode 100644 index 0000000..b8160c0 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/production/controller/dto/DeviceShipmentDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/production/controller/dto/FinishedProductReceiptAddDto.java b/src/main/java/com/niuan/erp/module/production/controller/dto/FinishedProductReceiptAddDto.java new file mode 100644 index 0000000..60bef39 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/production/controller/dto/FinishedProductReceiptAddDto.java @@ -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 deviceItems +) {} diff --git a/src/main/java/com/niuan/erp/module/production/controller/dto/FinishedProductShipmentAddDto.java b/src/main/java/com/niuan/erp/module/production/controller/dto/FinishedProductShipmentAddDto.java new file mode 100644 index 0000000..cf18056 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/production/controller/dto/FinishedProductShipmentAddDto.java @@ -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 shipmentItems +) {} diff --git a/src/main/java/com/niuan/erp/module/production/controller/dto/FinishedProductShipmentItemDto.java b/src/main/java/com/niuan/erp/module/production/controller/dto/FinishedProductShipmentItemDto.java new file mode 100644 index 0000000..d429998 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/production/controller/dto/FinishedProductShipmentItemDto.java @@ -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) {} diff --git a/src/main/java/com/niuan/erp/module/production/controller/dto/ProductionReturnItemDto.java b/src/main/java/com/niuan/erp/module/production/controller/dto/ProductionReturnItemDto.java new file mode 100644 index 0000000..e169f09 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/production/controller/dto/ProductionReturnItemDto.java @@ -0,0 +1,7 @@ +package com.niuan.erp.module.production.controller.dto; + +public record ProductionReturnItemDto( + String partNumber, + String productSpecs, + Integer returnQty +) {} \ No newline at end of file diff --git a/src/main/java/com/niuan/erp/module/production/enums/OutStockType.java b/src/main/java/com/niuan/erp/module/production/enums/OutStockType.java new file mode 100644 index 0000000..19e6e02 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/production/enums/OutStockType.java @@ -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 { + 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; + } +} diff --git a/src/main/java/com/niuan/erp/module/purchase/controller/dto/GeneratePurchaseOrderDto.java b/src/main/java/com/niuan/erp/module/purchase/controller/dto/GeneratePurchaseOrderDto.java new file mode 100644 index 0000000..5889113 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/purchase/controller/dto/GeneratePurchaseOrderDto.java @@ -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 selectedItems +) {} \ No newline at end of file diff --git a/src/main/java/com/niuan/erp/module/purchase/controller/dto/PartVendorMappingDto.java b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PartVendorMappingDto.java new file mode 100644 index 0000000..162cfb3 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PartVendorMappingDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderAddDto.java b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderAddDto.java new file mode 100644 index 0000000..1b60b9a --- /dev/null +++ b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderAddDto.java @@ -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 items +) {} diff --git a/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderInboundDto.java b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderInboundDto.java new file mode 100644 index 0000000..476ddf6 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderInboundDto.java @@ -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 items +) {} diff --git a/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderInboundItemDto.java b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderInboundItemDto.java new file mode 100644 index 0000000..82da49b --- /dev/null +++ b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderInboundItemDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderItemAddDto.java b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderItemAddDto.java new file mode 100644 index 0000000..3844dc4 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderItemAddDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderQrCodeDto.java b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderQrCodeDto.java new file mode 100644 index 0000000..4cb646a --- /dev/null +++ b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchaseOrderQrCodeDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchasePlanItemsWithVendorSuggestionsDto.java b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchasePlanItemsWithVendorSuggestionsDto.java new file mode 100644 index 0000000..3ac649f --- /dev/null +++ b/src/main/java/com/niuan/erp/module/purchase/controller/dto/PurchasePlanItemsWithVendorSuggestionsDto.java @@ -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 items, + + @Schema(description = "所有供应商列表") + List allVendors, + + @Schema(description = "物料供应商映射关系") + List partVendorMappings +) {} diff --git a/src/main/java/com/niuan/erp/module/purchase/controller/dto/VendorInfoDto.java b/src/main/java/com/niuan/erp/module/purchase/controller/dto/VendorInfoDto.java new file mode 100644 index 0000000..6270c0d --- /dev/null +++ b/src/main/java/com/niuan/erp/module/purchase/controller/dto/VendorInfoDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/purchase/controller/dto/VendorSuggestionDto.java b/src/main/java/com/niuan/erp/module/purchase/controller/dto/VendorSuggestionDto.java new file mode 100644 index 0000000..0102b94 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/purchase/controller/dto/VendorSuggestionDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceAddDto.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceAddDto.java new file mode 100644 index 0000000..4c8712a --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceAddDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceQueryDto.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceQueryDto.java new file mode 100644 index 0000000..1262fff --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceQueryDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceResultDto.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceResultDto.java new file mode 100644 index 0000000..0037ca9 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceResultDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceValidateItem.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceValidateItem.java new file mode 100644 index 0000000..db8ab4d --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceValidateItem.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceValidateRequest.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceValidateRequest.java new file mode 100644 index 0000000..6bba5c0 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceValidateRequest.java @@ -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 devices +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceValidateResponse.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceValidateResponse.java new file mode 100644 index 0000000..a0c2483 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/DeviceValidateResponse.java @@ -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 duplicateProductSns, + @Schema(description = "重复的MAC地址列表") + List duplicateMacs, + @Schema(description = "重复的序列号列表") + List duplicateSerialNums, + @Schema(description = "错误消息") + String message +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/RepairRecordQueryDto.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/RepairRecordQueryDto.java new file mode 100644 index 0000000..85899ad --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/RepairRecordQueryDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/RepairRecordResultDto.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/RepairRecordResultDto.java new file mode 100644 index 0000000..ad65f92 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/RepairRecordResultDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderAddDto.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderAddDto.java new file mode 100644 index 0000000..444f0d3 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderAddDto.java @@ -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 saleOrderItems +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderDto.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderDto.java index 37003e9..3fb6ebf 100644 --- a/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderDto.java +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderDto.java @@ -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, diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderItemAddDto.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderItemAddDto.java new file mode 100644 index 0000000..28bc19a --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderItemAddDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderItemUpdateDto.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderItemUpdateDto.java new file mode 100644 index 0000000..f3c11d9 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderItemUpdateDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderUpdateDto.java b/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderUpdateDto.java new file mode 100644 index 0000000..b6dc712 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sale/controller/dto/SaleOrderUpdateDto.java @@ -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 saleOrderItems +) {} diff --git a/src/main/java/com/niuan/erp/module/sale/converter/SaleOrderConverter.java b/src/main/java/com/niuan/erp/module/sale/converter/SaleOrderConverter.java index 32d1ab8..31247f7 100644 --- a/src/main/java/com/niuan/erp/module/sale/converter/SaleOrderConverter.java +++ b/src/main/java/com/niuan/erp/module/sale/converter/SaleOrderConverter.java @@ -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") diff --git a/src/main/java/com/niuan/erp/module/sale/service/impl/SaleOrderServiceImpl.java b/src/main/java/com/niuan/erp/module/sale/service/impl/SaleOrderServiceImpl.java index ff1fd65..9bdb407 100644 --- a/src/main/java/com/niuan/erp/module/sale/service/impl/SaleOrderServiceImpl.java +++ b/src/main/java/com/niuan/erp/module/sale/service/impl/SaleOrderServiceImpl.java @@ -263,6 +263,7 @@ public class SaleOrderServiceImpl extends ServiceImpl } 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 } entity.setFormStatus(FormStatus.NO_APPROVE); + entity.setStatus(0); entity.setUpdateDate(LocalDateTime.now()); entity.setUpdateUserId(SecurityUtils.getUserId()); entity.setUpdateUserName(SecurityUtils.getUserName()); diff --git a/src/main/java/com/niuan/erp/module/sys/controller/SysPermissionController.java b/src/main/java/com/niuan/erp/module/sys/controller/SysPermissionController.java new file mode 100644 index 0000000..585524b --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sys/controller/SysPermissionController.java @@ -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> getSysPermissionPage(BasePageReqParams dto, SysPermissionDto searchParams) { + var wrapper = new LambdaQueryWrapper(); + 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> getPermissionTree() { + return BaseResult.successWithData(sysPermissionService.getPermissionTree()); + } + + @GetMapping("/getAllPermissionTree") + public BaseResult> 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(); + } +} diff --git a/src/main/java/com/niuan/erp/module/sys/controller/dto/SysPermissionDto.java b/src/main/java/com/niuan/erp/module/sys/controller/dto/SysPermissionDto.java new file mode 100644 index 0000000..e7c1b33 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sys/controller/dto/SysPermissionDto.java @@ -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) {} \ No newline at end of file diff --git a/src/main/java/com/niuan/erp/module/sys/controller/dto/SysUserSearchDto.java b/src/main/java/com/niuan/erp/module/sys/controller/dto/SysUserSearchDto.java new file mode 100644 index 0000000..52ef9d0 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sys/controller/dto/SysUserSearchDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/sys/controller/dto/SysUserUpdateDto.java b/src/main/java/com/niuan/erp/module/sys/controller/dto/SysUserUpdateDto.java new file mode 100644 index 0000000..57a0c15 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sys/controller/dto/SysUserUpdateDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/sys/converter/SysPermissionConverter.java b/src/main/java/com/niuan/erp/module/sys/converter/SysPermissionConverter.java new file mode 100644 index 0000000..33f308b --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sys/converter/SysPermissionConverter.java @@ -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 toDtoList(List entities); + + default PermissionType map(Integer status) { + return status == null ? null : PermissionType.fromCode(status); + } + + default Integer map(PermissionType status) { + return status == null ? null : status.getCode(); + } +} \ No newline at end of file diff --git a/src/main/java/com/niuan/erp/module/sys/entity/RolePermission.java b/src/main/java/com/niuan/erp/module/sys/entity/RolePermission.java new file mode 100644 index 0000000..fbdb136 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sys/entity/RolePermission.java @@ -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; + +/** + *

+ * 角色-权限关联表,用的是原有框架的 yy_sysrole + *

+ * + * @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; +} diff --git a/src/main/java/com/niuan/erp/module/sys/entity/SysPermission.java b/src/main/java/com/niuan/erp/module/sys/entity/SysPermission.java new file mode 100644 index 0000000..eda88d1 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sys/entity/SysPermission.java @@ -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; + +/** + *

+ * 系统权限表 + *

+ * + * @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; +} diff --git a/src/main/java/com/niuan/erp/module/sys/mapper/RolePermissionMapper.java b/src/main/java/com/niuan/erp/module/sys/mapper/RolePermissionMapper.java new file mode 100644 index 0000000..de02a68 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sys/mapper/RolePermissionMapper.java @@ -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; + +/** + *

+ * 角色-权限关联表,用的是原有框架的 yy_sysrole Mapper 接口 + *

+ * + * @author + * @since 2026-02-12 + */ +public interface RolePermissionMapper extends BaseMapper { + +} diff --git a/src/main/java/com/niuan/erp/module/sys/mapper/SysPermissionMapper.java b/src/main/java/com/niuan/erp/module/sys/mapper/SysPermissionMapper.java new file mode 100644 index 0000000..c1699cf --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sys/mapper/SysPermissionMapper.java @@ -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; + +/** + *

+ * 系统权限表 Mapper 接口 + *

+ * + * @author + * @since 2026-02-12 + */ +public interface SysPermissionMapper extends BaseMapper { + + List selectByUserId(Long userId); + + List selectPermissionIdByRoleId(Long roleId); + +} diff --git a/src/main/java/com/niuan/erp/module/sys/service/SysPermissionService.java b/src/main/java/com/niuan/erp/module/sys/service/SysPermissionService.java new file mode 100644 index 0000000..d4f1629 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/sys/service/SysPermissionService.java @@ -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 getSysPermissionPage(BasePageReqParams pageParams, LambdaQueryWrapper wrapper); + + void addSysPermission(SysPermissionDto dto); + + void updateSysPermission(SysPermissionDto dto); + + void deleteSysPermission(long id); + + void deleteBatch(List ids); + + void setStatus(Long id, Integer status); + + List getPermissionTree(); + + List getAllPermissionTree(); + +} diff --git a/src/main/java/com/niuan/erp/module/sys/service/impl/KeyAccountServiceImpl.java b/src/main/java/com/niuan/erp/module/sys/service/impl/KeyAccountServiceImpl.java index aea3c47..fcc31e8 100644 --- a/src/main/java/com/niuan/erp/module/sys/service/impl/KeyAccountServiceImpl.java +++ b/src/main/java/com/niuan/erp/module/sys/service/impl/KeyAccountServiceImpl.java @@ -46,7 +46,7 @@ public class KeyAccountServiceImpl extends ServiceImpl getKeyAccountTree() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(KeyAccount::getStatus, 1); + wrapper.eq(KeyAccount::getStatus, 0); wrapper.orderByAsc(KeyAccount::getLevelPath); List keyAccounts = this.baseMapper.selectList(wrapper); @@ -77,7 +77,7 @@ public class KeyAccountServiceImpl extends ServiceImpl implements SysPermissionService { + + + private final SysPermissionConverter sysPermissionConverter; + + private final TokenService tokenService; + + private final SysUserService sysUserService; + + @Override + public IPage getSysPermissionPage(BasePageReqParams pageParams, LambdaQueryWrapper wrapper) { + IPage 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 userIds = sysUserService.getUserIdsByPermissionId(entity.getId()); + if (userIds != null && !userIds.isEmpty()) { + userIds.forEach(tokenService::removeAllUserTokens); + } + } + + @Override + public void deleteSysPermission(long id) { + // 权限删除前,踢掉所有拥有该权限的用户 + List userIds = sysUserService.getUserIdsByPermissionId(id); + if (userIds != null && !userIds.isEmpty()) { + userIds.forEach(tokenService::removeAllUserTokens); + } + this.baseMapper.deleteById(id); + } + + @Override + public void deleteBatch(List ids) { + if (ids != null && !ids.isEmpty()) { + // 批量删除前,踢掉所有拥有这些权限的用户 + for (Long permissionId : ids) { + List 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 userIds = sysUserService.getUserIdsByPermissionId(id); + if (userIds != null && !userIds.isEmpty()) { + userIds.forEach(tokenService::removeAllUserTokens); + } + } + + @Override + public List getPermissionTree() { + // 查询所有权限 + List allPermissions = this.baseMapper.selectList( + new LambdaQueryWrapper() + .orderByAsc(SysPermission::getSort) + ); + // 构建树形结构,只到倒数第二层 + return buildMenuTree(allPermissions, 0L); + } + + @Override + public List getAllPermissionTree() { + // 查询所有权限(包括菜单和按钮) + List allPermissions = this.baseMapper.selectList( + new LambdaQueryWrapper() + .orderByAsc(SysPermission::getSort) + ); + // 构建完整树形结构 + return buildTree(allPermissions, 0L); + } + + /** + * 递归构建菜单权限树 + * @param permissions 所有权限列表 + * @param parentId 父节点ID + * @return 树形结构 + */ + private List buildMenuTree(List permissions, Long parentId) { + if (permissions == null || permissions.isEmpty()) return List.of(); + + var result = new ArrayList(); + var permissionMap = new HashMap(); + + 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 buildTree(List permissions, Long parentId) { + if (permissions == null || permissions.isEmpty()) return List.of(); + + var result = new ArrayList(); + var permissionMap = new HashMap(); + + permissions.forEach(permission -> { + handlePermission(parentId, result, permissionMap, permission); + }); + + // 检查这些子节点是否还有子节点(即是否是倒数第二层) + return result; + } + + private void handlePermission(Long parentId, ArrayList result, HashMap 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(); + list.add(node); + permissionMap.get(permission.getParentId()) + .setChildren(list); + } else { + child.add(node); + } + } else { + var list = new ArrayList(); + 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); + } + } + +} diff --git a/src/main/java/com/niuan/erp/module/warehouse/controller/dto/InventoryCountAddDto.java b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/InventoryCountAddDto.java new file mode 100644 index 0000000..b610b5e --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/InventoryCountAddDto.java @@ -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 countItems +) {} diff --git a/src/main/java/com/niuan/erp/module/warehouse/controller/dto/InventoryCountItemAddDto.java b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/InventoryCountItemAddDto.java new file mode 100644 index 0000000..55c953b --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/InventoryCountItemAddDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/warehouse/controller/dto/ProductVendorMapAddDto.java b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/ProductVendorMapAddDto.java new file mode 100644 index 0000000..b11b2b1 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/ProductVendorMapAddDto.java @@ -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 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) {} +} diff --git a/src/main/java/com/niuan/erp/module/warehouse/controller/dto/ProductVendorMapDto.java b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/ProductVendorMapDto.java new file mode 100644 index 0000000..d299906 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/ProductVendorMapDto.java @@ -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) {} diff --git a/src/main/java/com/niuan/erp/module/warehouse/controller/dto/StockCheckDto.java b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/StockCheckDto.java new file mode 100644 index 0000000..ed5707a --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/StockCheckDto.java @@ -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 items +) { + @JsonInclude(JsonInclude.Include.NON_NULL) + public record StockCheckItem( + String partNumber, + Integer requiredCount + ) {} +} diff --git a/src/main/java/com/niuan/erp/module/warehouse/controller/dto/StockTransferOrderItemDto.java b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/StockTransferOrderItemDto.java new file mode 100644 index 0000000..040d4ee --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/StockTransferOrderItemDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/warehouse/controller/dto/WarehouseReceiptAddDto.java b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/WarehouseReceiptAddDto.java new file mode 100644 index 0000000..28ff835 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/WarehouseReceiptAddDto.java @@ -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 receiptItems +) {} diff --git a/src/main/java/com/niuan/erp/module/warehouse/controller/dto/WarehouseReceiptItemAddDto.java b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/WarehouseReceiptItemAddDto.java new file mode 100644 index 0000000..16036ac --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/WarehouseReceiptItemAddDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/warehouse/controller/dto/WarehouseReceiptItemDto.java b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/WarehouseReceiptItemDto.java new file mode 100644 index 0000000..673aeaa --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/controller/dto/WarehouseReceiptItemDto.java @@ -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 +) {} diff --git a/src/main/java/com/niuan/erp/module/warehouse/entity/ProductVendorMap.java b/src/main/java/com/niuan/erp/module/warehouse/entity/ProductVendorMap.java new file mode 100644 index 0000000..aacf660 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/entity/ProductVendorMap.java @@ -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; + +/** + *

+ * 产品供应商映射表 + *

+ * + * @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; +} \ No newline at end of file diff --git a/src/main/java/com/niuan/erp/module/warehouse/mapper/ProductVendorMapMapper.java b/src/main/java/com/niuan/erp/module/warehouse/mapper/ProductVendorMapMapper.java new file mode 100644 index 0000000..b68bad9 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/mapper/ProductVendorMapMapper.java @@ -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; + +/** + *

+ * 产品供应商映射表 Mapper 接口 + *

+ * + * @author + * @since 2026-03-03 + */ +@Mapper +public interface ProductVendorMapMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/src/main/java/com/niuan/erp/module/warehouse/service/ProductVendorMapService.java b/src/main/java/com/niuan/erp/module/warehouse/service/ProductVendorMapService.java new file mode 100644 index 0000000..0168c86 --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/service/ProductVendorMapService.java @@ -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; + +/** + *

+ * 产品供应商映射表 服务类 + *

+ * + * @author + * @since 2026-03-03 + */ +public interface ProductVendorMapService extends IService { + + /** + * 根据产品编号获取默认供应商 + * + * @param partNumber 产品编号 + * @return 产品供应商映射 + */ + Optional getDefaultVendorByPartNumber(String partNumber); +} \ No newline at end of file diff --git a/src/main/java/com/niuan/erp/module/warehouse/service/impl/ProductVendorMapServiceImpl.java b/src/main/java/com/niuan/erp/module/warehouse/service/impl/ProductVendorMapServiceImpl.java new file mode 100644 index 0000000..5f08a3c --- /dev/null +++ b/src/main/java/com/niuan/erp/module/warehouse/service/impl/ProductVendorMapServiceImpl.java @@ -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; + +/** + *

+ * 产品供应商映射表 服务实现类 + *

+ * + * @author + * @since 2026-03-03 + */ +@Service +@Transactional +public class ProductVendorMapServiceImpl extends ServiceImpl implements ProductVendorMapService { + + @Override + public Optional getDefaultVendorByPartNumber(String partNumber) { + var wrapper = new LambdaQueryWrapper() + .eq(ProductVendorMap::getPartNumber, partNumber) + // 按最后采购时间倒序,获取最近的供应商 + .orderByDesc(ProductVendorMap::getProcureDate) + .last("LIMIT 1"); + return Optional.ofNullable(getOne(wrapper)); + } +} \ No newline at end of file diff --git a/src/main/resources/mapper/sys/KeyAccountMapper.xml b/src/main/resources/mapper/sys/KeyAccountMapper.xml index 8869230..1c01f99 100644 --- a/src/main/resources/mapper/sys/KeyAccountMapper.xml +++ b/src/main/resources/mapper/sys/KeyAccountMapper.xml @@ -26,7 +26,7 @@