添加后端基础架构和API功能
- 配置.env文件支持环境变量管理 - 集成MyBatis Plus ORM框架并配置数据库连接 - 实现JWT认证和Spring Security安全配置 - 添加全局异常处理机制和统一响应结果封装 - 配置CORS跨域资源共享支持 - 实现用户认证API包括登录、注册、刷新token功能 - 开发完整的药品管理系统包含分类、药品、库存管理 - 实现出入库管理功能包括供应商、客户管理 - 添加角色权限管理和菜单配置 - 实现仪表盘统计和过期预警功能 - 配置分页查询和数据验证功能 ```
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,8 @@
|
||||
*/.vscode/
|
||||
*/public/
|
||||
projectDesigning.md
|
||||
|
||||
# 环境变量
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
19
backend/.env.example
Normal file
19
backend/.env.example
Normal file
@@ -0,0 +1,19 @@
|
||||
# ============================================================
|
||||
# 后端环境变量(IDEA 可通过 EnvFile 插件加载)
|
||||
# 或通过命令行: export $(cat .env | xargs) && ./mvnw spring-boot:run
|
||||
# ============================================================
|
||||
|
||||
# 数据库
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=medicine_manager
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=your_password_here
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=change_me_to_a_random_base64_string_at_least_256_bits
|
||||
JWT_EXPIRATION=86400000
|
||||
JWT_REFRESH_EXPIRATION=604800000
|
||||
|
||||
# 服务端口
|
||||
SERVER_PORT=8080
|
||||
@@ -69,6 +69,12 @@
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<version>4.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
|
||||
@@ -1,13 +1,44 @@
|
||||
package com.kronecker.backend;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.kronecker.backend.mapper")
|
||||
public class BackendApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
loadDotenv();
|
||||
SpringApplication.run(BackendApplication.class, args);
|
||||
}
|
||||
|
||||
private static void loadDotenv() {
|
||||
Path dotenv = Paths.get(".env");
|
||||
if (!Files.exists(dotenv)) dotenv = Paths.get("..", ".env");
|
||||
if (!Files.exists(dotenv)) return;
|
||||
|
||||
try {
|
||||
for (String line : Files.readAllLines(dotenv)) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
int eq = line.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
String key = line.substring(0, eq).trim();
|
||||
String value = line.substring(eq + 1).trim();
|
||||
if ((value.startsWith("\"") && value.endsWith("\""))
|
||||
|| (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.substring(1, value.length() - 1);
|
||||
}
|
||||
System.setProperty(key, value);
|
||||
}
|
||||
System.out.println("已加载 .env 文件: " + dotenv.toAbsolutePath());
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.kronecker.backend.common;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private final int code;
|
||||
|
||||
public BusinessException(ResultCode code) {
|
||||
super(code.getMessage());
|
||||
this.code = code.getCode();
|
||||
}
|
||||
|
||||
public BusinessException(int code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
this.code = ResultCode.BAD_REQUEST.getCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.kronecker.backend.common;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public Result<Void> handleBusinessException(BusinessException e) {
|
||||
log.warn("业务异常: {}", e.getMessage());
|
||||
return Result.error(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(BindException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Void> handleBindException(BindException e) {
|
||||
String msg = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(err -> err.getField() + ": " + err.getDefaultMessage())
|
||||
.reduce((a, b) -> a + "; " + b).orElse("参数校验失败");
|
||||
log.warn("参数校验失败: {}", msg);
|
||||
return Result.error(ResultCode.BAD_REQUEST.getCode(), msg);
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public Result<Void> handleAccessDeniedException(AccessDeniedException e) {
|
||||
log.warn("权限不足: {}", e.getMessage());
|
||||
return Result.error(ResultCode.FORBIDDEN);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Result<Void> handleException(Exception e) {
|
||||
log.error("系统异常", e);
|
||||
return Result.error(ResultCode.INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.kronecker.backend.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Result<T> {
|
||||
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public static <T> Result<T> success(T data) {
|
||||
Result<T> r = new Result<>();
|
||||
r.setCode(ResultCode.SUCCESS.getCode());
|
||||
r.setMessage(ResultCode.SUCCESS.getMessage());
|
||||
r.setData(data);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static <T> Result<T> success() {
|
||||
return success(null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(ResultCode code) {
|
||||
Result<T> r = new Result<>();
|
||||
r.setCode(code.getCode());
|
||||
r.setMessage(code.getMessage());
|
||||
return r;
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(int code, String message) {
|
||||
Result<T> r = new Result<>();
|
||||
r.setCode(code);
|
||||
r.setMessage(message);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.kronecker.backend.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ResultCode {
|
||||
|
||||
SUCCESS(200, "操作成功"),
|
||||
BAD_REQUEST(400, "参数错误"),
|
||||
UNAUTHORIZED(401, "请先登录"),
|
||||
FORBIDDEN(403, "权限不足"),
|
||||
NOT_FOUND(404, "资源不存在"),
|
||||
INTERNAL_ERROR(500, "服务器内部错误");
|
||||
|
||||
private final int code;
|
||||
private final String message;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.kronecker.backend.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class CorsConfig {
|
||||
|
||||
@Bean
|
||||
public CorsFilter corsFilter() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowedOriginPatterns(List.of("*"));
|
||||
config.setAllowedMethods(List.of("*"));
|
||||
config.setAllowedHeaders(List.of("*"));
|
||||
config.setAllowCredentials(true);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.kronecker.backend.config;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
@Bean
|
||||
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
|
||||
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
|
||||
factory.setDataSource(dataSource);
|
||||
factory.setPlugins(mybatisPlusInterceptor());
|
||||
return factory.getObject();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
|
||||
return new SqlSessionTemplate(sqlSessionFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
return new MybatisPlusInterceptor();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.kronecker.backend.config;
|
||||
|
||||
import com.kronecker.backend.security.JwtAuthenticationFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter jwtFilter;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/api/auth/login", "/api/auth/register", "/api/auth/refresh").permitAll()
|
||||
.anyRequest().authenticated())
|
||||
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.kronecker.backend.controller;
|
||||
|
||||
import com.kronecker.backend.common.Result;
|
||||
import com.kronecker.backend.dto.LoginDTO;
|
||||
import com.kronecker.backend.dto.RegisterDTO;
|
||||
import com.kronecker.backend.security.JwtTokenProvider;
|
||||
import com.kronecker.backend.service.AuthService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@PostMapping("/login")
|
||||
public Result<Map<String, Object>> login(@Valid @RequestBody LoginDTO dto) {
|
||||
return Result.success(authService.login(dto.getUsername(), dto.getPassword()));
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public Result<Void> register(@Valid @RequestBody RegisterDTO dto) {
|
||||
authService.register(dto);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PostMapping("/refresh")
|
||||
public Result<String> refresh(@RequestBody Map<String, String> body) {
|
||||
return Result.success(authService.refreshToken(body.get("refreshToken")));
|
||||
}
|
||||
|
||||
@GetMapping("/userinfo")
|
||||
public Result<Map<String, Object>> userInfo(@AuthenticationPrincipal UserDetails user) {
|
||||
return Result.success(authService.getUserInfo(Long.parseLong(user.getUsername())));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.kronecker.backend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kronecker.backend.common.Result;
|
||||
import com.kronecker.backend.entity.MedicineCategory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/categories")
|
||||
@RequiredArgsConstructor
|
||||
public class CategoryController {
|
||||
|
||||
private final IService<MedicineCategory> categoryService;
|
||||
|
||||
@GetMapping
|
||||
public Result<List<MedicineCategory>> list() {
|
||||
return Result.success(categoryService.list(
|
||||
new LambdaQueryWrapper<MedicineCategory>().orderByAsc(MedicineCategory::getSortOrder)));
|
||||
}
|
||||
|
||||
@GetMapping("/tree")
|
||||
public Result<List<MedicineCategory>> tree() {
|
||||
return Result.success(categoryService.list(
|
||||
new LambdaQueryWrapper<MedicineCategory>().orderByAsc(MedicineCategory::getSortOrder)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<Void> create(@RequestBody MedicineCategory category) {
|
||||
categoryService.save(category);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody MedicineCategory category) {
|
||||
category.setId(id);
|
||||
categoryService.updateById(category);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
categoryService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.kronecker.backend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kronecker.backend.common.Result;
|
||||
import com.kronecker.backend.entity.Customer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/customers")
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerController {
|
||||
|
||||
private final IService<Customer> customerService;
|
||||
|
||||
@GetMapping
|
||||
public Result<Page<Customer>> list(@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size,
|
||||
@RequestParam(required = false) String name) {
|
||||
var wrapper = new LambdaQueryWrapper<Customer>()
|
||||
.like(StringUtils.hasText(name), Customer::getName, name)
|
||||
.orderByDesc(Customer::getCreateTime);
|
||||
return Result.success(customerService.page(new Page<>(page, size), wrapper));
|
||||
}
|
||||
|
||||
@GetMapping("/all")
|
||||
public Result<List<Customer>> all() {
|
||||
return Result.success(customerService.list());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<Void> create(@RequestBody Customer customer) {
|
||||
customerService.save(customer);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody Customer customer) {
|
||||
customer.setId(id);
|
||||
customerService.updateById(customer);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
customerService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.kronecker.backend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.kronecker.backend.common.Result;
|
||||
import com.kronecker.backend.entity.Stock;
|
||||
import com.kronecker.backend.entity.StockInRecord;
|
||||
import com.kronecker.backend.entity.StockOutRecord;
|
||||
import com.kronecker.backend.service.DashboardService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/dashboard")
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
|
||||
@GetMapping("/statistics")
|
||||
public Result<Map<String, Object>> statistics() {
|
||||
return Result.success(dashboardService.getStatistics());
|
||||
}
|
||||
|
||||
@GetMapping("/expiry-warning")
|
||||
public Result<Page<Stock>> expiryWarning(@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size) {
|
||||
return Result.success(dashboardService.getExpiryWarning(new Page<>(page, size)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.kronecker.backend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.kronecker.backend.common.Result;
|
||||
import com.kronecker.backend.entity.Medicine;
|
||||
import com.kronecker.backend.service.MedicineService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/medicines")
|
||||
@RequiredArgsConstructor
|
||||
public class MedicineController {
|
||||
|
||||
private final MedicineService medicineService;
|
||||
|
||||
@GetMapping
|
||||
public Result<Page<Medicine>> list(@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size,
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false) Long categoryId) {
|
||||
return Result.success(medicineService.pageQuery(new Page<>(page, size), name, categoryId));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Result<Medicine> getById(@PathVariable Long id) {
|
||||
return Result.success(medicineService.getById(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<Void> create(@RequestBody Medicine medicine) {
|
||||
medicineService.save(medicine);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody Medicine medicine) {
|
||||
medicine.setId(id);
|
||||
medicineService.updateById(medicine);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
medicineService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.kronecker.backend.controller;
|
||||
|
||||
import com.kronecker.backend.common.Result;
|
||||
import com.kronecker.backend.entity.Menu;
|
||||
import com.kronecker.backend.service.MenuService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/menus")
|
||||
@RequiredArgsConstructor
|
||||
public class MenuController {
|
||||
|
||||
private final MenuService menuService;
|
||||
|
||||
@GetMapping("/tree")
|
||||
public Result<List<Menu>> tree(@AuthenticationPrincipal UserDetails user) {
|
||||
return Result.success(menuService.getMenusByUserId(Long.parseLong(user.getUsername())));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public Result<List<Menu>> list() {
|
||||
return Result.success(menuService.getTree());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<Void> create(@RequestBody Menu menu) {
|
||||
menuService.save(menu);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody Menu menu) {
|
||||
menu.setId(id);
|
||||
menuService.updateById(menu);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
menuService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.kronecker.backend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kronecker.backend.common.Result;
|
||||
import com.kronecker.backend.entity.Role;
|
||||
import com.kronecker.backend.entity.RoleMenu;
|
||||
import com.kronecker.backend.mapper.RoleMenuMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/roles")
|
||||
@RequiredArgsConstructor
|
||||
public class RoleController {
|
||||
|
||||
private final IService<Role> roleService;
|
||||
private final RoleMenuMapper roleMenuMapper;
|
||||
|
||||
@GetMapping
|
||||
public Result<List<Role>> list() {
|
||||
return Result.success(roleService.list());
|
||||
}
|
||||
|
||||
@GetMapping("/all")
|
||||
public Result<List<Role>> all() {
|
||||
return Result.success(roleService.list(
|
||||
new LambdaQueryWrapper<Role>().eq(Role::getStatus, 1)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<Void> create(@RequestBody Role role) {
|
||||
roleService.save(role);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody Role role) {
|
||||
role.setId(id);
|
||||
roleService.updateById(role);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
roleService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/menus")
|
||||
@Transactional
|
||||
public Result<Void> assignMenus(@PathVariable Long id, @RequestBody Map<String, List<Long>> body) {
|
||||
List<Long> menuIds = body.get("menuIds");
|
||||
roleMenuMapper.delete(new LambdaQueryWrapper<RoleMenu>().eq(RoleMenu::getRoleId, id));
|
||||
if (menuIds != null && !menuIds.isEmpty()) {
|
||||
for (Long menuId : menuIds) {
|
||||
RoleMenu rm = new RoleMenu();
|
||||
rm.setRoleId(id);
|
||||
rm.setMenuId(menuId);
|
||||
roleMenuMapper.insert(rm);
|
||||
}
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.kronecker.backend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kronecker.backend.common.Result;
|
||||
import com.kronecker.backend.entity.Medicine;
|
||||
import com.kronecker.backend.entity.Stock;
|
||||
import com.kronecker.backend.entity.Warehouse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/stocks")
|
||||
@RequiredArgsConstructor
|
||||
public class StockController {
|
||||
|
||||
private final IService<Stock> stockService;
|
||||
private final IService<Medicine> medicineService;
|
||||
private final IService<Warehouse> warehouseService;
|
||||
|
||||
@GetMapping
|
||||
public Result<Page<Stock>> list(@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size,
|
||||
@RequestParam(required = false) String medicineName,
|
||||
@RequestParam(required = false) String batchNo) {
|
||||
var wrapper = new LambdaQueryWrapper<Stock>()
|
||||
.like(StringUtils.hasText(batchNo), Stock::getBatchNo, batchNo)
|
||||
.orderByAsc(Stock::getExpiryDate);
|
||||
// 按药品名搜索需先查 medicine 表
|
||||
if (StringUtils.hasText(medicineName)) {
|
||||
var medIds = medicineService.list(
|
||||
new LambdaQueryWrapper<Medicine>().like(Medicine::getName, medicineName)
|
||||
).stream().map(Medicine::getId).toList();
|
||||
if (medIds.isEmpty()) return Result.success(new Page<>());
|
||||
wrapper.in(Stock::getMedicineId, medIds);
|
||||
}
|
||||
Page<Stock> result = stockService.page(new Page<>(page, size), wrapper);
|
||||
populateNames(result.getRecords());
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/warning")
|
||||
public Result<Page<Stock>> warning(@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size) {
|
||||
var wrapper = new LambdaQueryWrapper<Stock>()
|
||||
.gt(Stock::getExpiryDate, java.time.LocalDate.now())
|
||||
.orderByAsc(Stock::getExpiryDate);
|
||||
Page<Stock> result = stockService.page(new Page<>(page, size), wrapper);
|
||||
populateNames(result.getRecords());
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
private void populateNames(java.util.List<Stock> stocks) {
|
||||
if (stocks.isEmpty()) return;
|
||||
var medIds = stocks.stream().map(Stock::getMedicineId).collect(Collectors.toSet());
|
||||
var whIds = stocks.stream().map(Stock::getWarehouseId).collect(Collectors.toSet());
|
||||
Map<Long, String> medMap = medicineService.listByIds(medIds).stream()
|
||||
.collect(Collectors.toMap(Medicine::getId, Medicine::getName));
|
||||
Map<Long, String> whMap = warehouseService.listByIds(whIds).stream()
|
||||
.collect(Collectors.toMap(Warehouse::getId, Warehouse::getName));
|
||||
stocks.forEach(s -> {
|
||||
s.setMedicineName(medMap.getOrDefault(s.getMedicineId(), ""));
|
||||
s.setWarehouseName(whMap.getOrDefault(s.getWarehouseId(), ""));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.kronecker.backend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kronecker.backend.common.Result;
|
||||
import com.kronecker.backend.dto.StockInCreateDTO;
|
||||
import com.kronecker.backend.entity.*;
|
||||
import com.kronecker.backend.service.StockInService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/stock-in")
|
||||
@RequiredArgsConstructor
|
||||
public class StockInController {
|
||||
|
||||
private final IService<StockInRecord> recordService;
|
||||
private final IService<StockInDetail> detailService;
|
||||
private final IService<Supplier> supplierService;
|
||||
private final StockInService stockInService;
|
||||
|
||||
@GetMapping
|
||||
public Result<Page<StockInRecord>> list(@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size) {
|
||||
Page<StockInRecord> result = recordService.page(new Page<>(page, size),
|
||||
new LambdaQueryWrapper<StockInRecord>().orderByDesc(StockInRecord::getCreateTime));
|
||||
populateSupplierNames(result.getRecords());
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Result<StockInRecord> getById(@PathVariable Long id) {
|
||||
StockInRecord record = recordService.getById(id);
|
||||
if (record != null) {
|
||||
record.setDetails(detailService.list(
|
||||
new LambdaQueryWrapper<StockInDetail>().eq(StockInDetail::getRecordId, id)));
|
||||
var supp = supplierService.getById(record.getSupplierId());
|
||||
if (supp != null) record.setSupplierName(supp.getName());
|
||||
}
|
||||
return Result.success(record);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<Long> create(@Valid @RequestBody StockInCreateDTO dto,
|
||||
@AuthenticationPrincipal UserDetails user) {
|
||||
return Result.success(stockInService.create(dto, Long.parseLong(user.getUsername())));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/status")
|
||||
public Result<Void> audit(@PathVariable Long id, @RequestBody Map<String, Integer> body) {
|
||||
if (body.get("status") == 1) {
|
||||
stockInService.audit(id);
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private void populateSupplierNames(java.util.List<StockInRecord> records) {
|
||||
if (records.isEmpty()) return;
|
||||
var suppIds = records.stream().map(StockInRecord::getSupplierId).collect(Collectors.toSet());
|
||||
Map<Long, String> suppMap = supplierService.listByIds(suppIds).stream()
|
||||
.collect(Collectors.toMap(Supplier::getId, Supplier::getName));
|
||||
records.forEach(r -> r.setSupplierName(suppMap.getOrDefault(r.getSupplierId(), "")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.kronecker.backend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kronecker.backend.common.Result;
|
||||
import com.kronecker.backend.dto.StockOutCreateDTO;
|
||||
import com.kronecker.backend.entity.*;
|
||||
import com.kronecker.backend.service.StockOutService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/stock-out")
|
||||
@RequiredArgsConstructor
|
||||
public class StockOutController {
|
||||
|
||||
private final IService<StockOutRecord> recordService;
|
||||
private final IService<StockOutDetail> detailService;
|
||||
private final IService<Customer> customerService;
|
||||
private final StockOutService stockOutService;
|
||||
|
||||
@GetMapping
|
||||
public Result<Page<StockOutRecord>> list(@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size) {
|
||||
Page<StockOutRecord> result = recordService.page(new Page<>(page, size),
|
||||
new LambdaQueryWrapper<StockOutRecord>().orderByDesc(StockOutRecord::getCreateTime));
|
||||
populateCustomerNames(result.getRecords());
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Result<StockOutRecord> getById(@PathVariable Long id) {
|
||||
StockOutRecord record = recordService.getById(id);
|
||||
if (record != null) {
|
||||
record.setDetails(detailService.list(
|
||||
new LambdaQueryWrapper<StockOutDetail>().eq(StockOutDetail::getRecordId, id)));
|
||||
if (record.getCustomerId() != null) {
|
||||
var cust = customerService.getById(record.getCustomerId());
|
||||
if (cust != null) record.setCustomerName(cust.getName());
|
||||
}
|
||||
}
|
||||
return Result.success(record);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<Long> create(@Valid @RequestBody StockOutCreateDTO dto,
|
||||
@AuthenticationPrincipal UserDetails user) {
|
||||
return Result.success(stockOutService.create(dto, Long.parseLong(user.getUsername())));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/status")
|
||||
public Result<Void> audit(@PathVariable Long id, @RequestBody Map<String, Integer> body) {
|
||||
if (body.get("status") == 1) {
|
||||
stockOutService.audit(id);
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private void populateCustomerNames(java.util.List<StockOutRecord> records) {
|
||||
if (records.isEmpty()) return;
|
||||
var custIds = records.stream().map(StockOutRecord::getCustomerId)
|
||||
.filter(id -> id != null).collect(Collectors.toSet());
|
||||
if (custIds.isEmpty()) return;
|
||||
Map<Long, String> custMap = customerService.listByIds(custIds).stream()
|
||||
.collect(Collectors.toMap(Customer::getId, Customer::getName));
|
||||
records.forEach(r -> {
|
||||
if (r.getCustomerId() != null)
|
||||
r.setCustomerName(custMap.getOrDefault(r.getCustomerId(), ""));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.kronecker.backend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kronecker.backend.common.Result;
|
||||
import com.kronecker.backend.entity.Supplier;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/suppliers")
|
||||
@RequiredArgsConstructor
|
||||
public class SupplierController {
|
||||
|
||||
private final IService<Supplier> supplierService;
|
||||
|
||||
@GetMapping
|
||||
public Result<Page<Supplier>> list(@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size,
|
||||
@RequestParam(required = false) String name) {
|
||||
var wrapper = new LambdaQueryWrapper<Supplier>()
|
||||
.like(StringUtils.hasText(name), Supplier::getName, name)
|
||||
.orderByDesc(Supplier::getCreateTime);
|
||||
return Result.success(supplierService.page(new Page<>(page, size), wrapper));
|
||||
}
|
||||
|
||||
@GetMapping("/all")
|
||||
public Result<List<Supplier>> all() {
|
||||
return Result.success(supplierService.list(
|
||||
new LambdaQueryWrapper<Supplier>().eq(Supplier::getStatus, 1)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<Void> create(@RequestBody Supplier supplier) {
|
||||
supplierService.save(supplier);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody Supplier supplier) {
|
||||
supplier.setId(id);
|
||||
supplierService.updateById(supplier);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
supplierService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.kronecker.backend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kronecker.backend.common.BusinessException;
|
||||
import com.kronecker.backend.common.Result;
|
||||
import com.kronecker.backend.entity.User;
|
||||
import com.kronecker.backend.entity.UserRole;
|
||||
import com.kronecker.backend.mapper.UserRoleMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@RequiredArgsConstructor
|
||||
public class UserController {
|
||||
|
||||
private final IService<User> userService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
|
||||
@GetMapping
|
||||
public Result<Page<User>> list(@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(defaultValue = "10") Integer size,
|
||||
@RequestParam(required = false) String username) {
|
||||
var wrapper = new LambdaQueryWrapper<User>()
|
||||
.like(StringUtils.hasText(username), User::getUsername, username)
|
||||
.orderByDesc(User::getCreateTime)
|
||||
.select(User.class, f -> !f.getColumn().equals("password"));
|
||||
return Result.success(userService.page(new Page<>(page, size), wrapper));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Result<Void> create(@RequestBody User user) {
|
||||
if (!StringUtils.hasText(user.getPassword())) {
|
||||
throw new BusinessException("密码不能为空");
|
||||
}
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
userService.save(user);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody User user) {
|
||||
user.setId(id);
|
||||
if (StringUtils.hasText(user.getPassword())) {
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
} else {
|
||||
user.setPassword(null); // 不更新密码
|
||||
}
|
||||
userService.updateById(user);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
userService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/roles")
|
||||
@Transactional
|
||||
public Result<Void> assignRoles(@PathVariable Long id, @RequestBody Map<String, List<Long>> body) {
|
||||
List<Long> roleIds = body.get("roleIds");
|
||||
userRoleMapper.delete(new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, id));
|
||||
if (roleIds != null) {
|
||||
for (Long roleId : roleIds) {
|
||||
UserRole ur = new UserRole();
|
||||
ur.setUserId(id);
|
||||
ur.setRoleId(roleId);
|
||||
userRoleMapper.insert(ur);
|
||||
}
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.kronecker.backend.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class LoginDTO {
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PageQueryDTO {
|
||||
private Integer page = 1;
|
||||
private Integer size = 10;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.kronecker.backend.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RegisterDTO {
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Size(min = 3, max = 32, message = "用户名长度3-32位")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, max = 32, message = "密码长度6-32位")
|
||||
private String password;
|
||||
|
||||
private String realName;
|
||||
private String phone;
|
||||
private String email;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.kronecker.backend.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class StockInCreateDTO {
|
||||
@NotNull private Long supplierId;
|
||||
@NotNull private Long warehouseId;
|
||||
@NotNull private LocalDate inDate;
|
||||
private String remark;
|
||||
@NotNull private BigDecimal totalAmount;
|
||||
@NotEmpty private List<Detail> details;
|
||||
|
||||
@Data
|
||||
public static class Detail {
|
||||
@NotNull private Long medicineId;
|
||||
private String batchNo;
|
||||
@NotNull private Integer quantity;
|
||||
@NotNull private BigDecimal costPrice;
|
||||
@NotNull private BigDecimal totalPrice;
|
||||
private LocalDate productionDate;
|
||||
@NotNull private LocalDate expiryDate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.kronecker.backend.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class StockOutCreateDTO {
|
||||
private Long customerId;
|
||||
@NotNull private Long warehouseId;
|
||||
@NotNull private LocalDate outDate;
|
||||
private Integer outType = 1;
|
||||
private String remark;
|
||||
@NotNull private BigDecimal totalAmount;
|
||||
@NotEmpty private List<Detail> details;
|
||||
|
||||
@Data
|
||||
public static class Detail {
|
||||
@NotNull private Long medicineId;
|
||||
private String batchNo;
|
||||
@NotNull private Integer quantity;
|
||||
@NotNull private BigDecimal unitPrice;
|
||||
@NotNull private BigDecimal totalPrice;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("customer")
|
||||
public class Customer {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String name;
|
||||
private String contactPerson;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String address;
|
||||
private String type;
|
||||
private String remark;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("medicine")
|
||||
public class Medicine {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long categoryId;
|
||||
private String name;
|
||||
private String genericName;
|
||||
private String specification;
|
||||
private String manufacturer;
|
||||
private String approvalNumber;
|
||||
private String barcode;
|
||||
private String dosageForm;
|
||||
private String unit;
|
||||
private BigDecimal retailPrice;
|
||||
private BigDecimal wholesalePrice;
|
||||
private String storageCondition;
|
||||
private Integer shelfLife;
|
||||
private Integer warningStock;
|
||||
private String description;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("medicine_category")
|
||||
public class MedicineCategory {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long parentId;
|
||||
private String name;
|
||||
private String code;
|
||||
private Integer sortOrder;
|
||||
private String description;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
}
|
||||
31
backend/src/main/java/com/kronecker/backend/entity/Menu.java
Normal file
31
backend/src/main/java/com/kronecker/backend/entity/Menu.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@TableName("menu")
|
||||
public class Menu {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long parentId;
|
||||
private String name;
|
||||
private Integer type;
|
||||
private String path;
|
||||
private String component;
|
||||
private String icon;
|
||||
private String permissionCode;
|
||||
private Integer sortOrder;
|
||||
private Integer visible;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
|
||||
@TableField(exist = false)
|
||||
private List<Menu> children;
|
||||
}
|
||||
22
backend/src/main/java/com/kronecker/backend/entity/Role.java
Normal file
22
backend/src/main/java/com/kronecker/backend/entity/Role.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("role")
|
||||
public class Role {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String name;
|
||||
private String code;
|
||||
private String description;
|
||||
private Integer status;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("role_menu")
|
||||
public class RoleMenu {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long roleId;
|
||||
private Long menuId;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("stock")
|
||||
public class Stock {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long medicineId;
|
||||
private Long warehouseId;
|
||||
private String batchNo;
|
||||
private Integer quantity;
|
||||
private BigDecimal costPrice;
|
||||
private LocalDate productionDate;
|
||||
private LocalDate expiryDate;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
|
||||
@TableField(exist = false)
|
||||
private String medicineName;
|
||||
@TableField(exist = false)
|
||||
private String warehouseName;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("stock_in_detail")
|
||||
public class StockInDetail {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long recordId;
|
||||
private Long medicineId;
|
||||
private String batchNo;
|
||||
private Integer quantity;
|
||||
private BigDecimal costPrice;
|
||||
private BigDecimal totalPrice;
|
||||
private LocalDate productionDate;
|
||||
private LocalDate expiryDate;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
|
||||
@TableField(exist = false)
|
||||
private String medicineName;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@TableName("stock_in_record")
|
||||
public class StockInRecord {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String recordNo;
|
||||
private Long supplierId;
|
||||
private Long warehouseId;
|
||||
private BigDecimal totalAmount;
|
||||
private Long operatorId;
|
||||
private LocalDate inDate;
|
||||
private Integer status;
|
||||
private String remark;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
|
||||
@TableField(exist = false)
|
||||
private String supplierName;
|
||||
@TableField(exist = false)
|
||||
private List<StockInDetail> details;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("stock_out_detail")
|
||||
public class StockOutDetail {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long recordId;
|
||||
private Long medicineId;
|
||||
private String batchNo;
|
||||
private Integer quantity;
|
||||
private BigDecimal unitPrice;
|
||||
private BigDecimal totalPrice;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
|
||||
@TableField(exist = false)
|
||||
private String medicineName;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@TableName("stock_out_record")
|
||||
public class StockOutRecord {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String recordNo;
|
||||
private Long customerId;
|
||||
private Long warehouseId;
|
||||
private BigDecimal totalAmount;
|
||||
private Long operatorId;
|
||||
private LocalDate outDate;
|
||||
private Integer outType;
|
||||
private Integer status;
|
||||
private String remark;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
|
||||
@TableField(exist = false)
|
||||
private String customerName;
|
||||
@TableField(exist = false)
|
||||
private List<StockOutDetail> details;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("supplier")
|
||||
public class Supplier {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String name;
|
||||
private String contactPerson;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String address;
|
||||
private String bankAccount;
|
||||
private String bankName;
|
||||
private String taxId;
|
||||
private String licenseNumber;
|
||||
private Integer status;
|
||||
private String remark;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
}
|
||||
26
backend/src/main/java/com/kronecker/backend/entity/User.java
Normal file
26
backend/src/main/java/com/kronecker/backend/entity/User.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("user")
|
||||
public class User {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String username;
|
||||
private String password;
|
||||
private String realName;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String avatar;
|
||||
private Integer status;
|
||||
private LocalDateTime lastLoginTime;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("user_role")
|
||||
public class UserRole {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private Long roleId;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.kronecker.backend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("warehouse")
|
||||
public class Warehouse {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String name;
|
||||
private String code;
|
||||
private String location;
|
||||
private String manager;
|
||||
private Integer status;
|
||||
private String remark;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.Customer;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface CustomerMapper extends BaseMapper<Customer> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.MedicineCategory;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface MedicineCategoryMapper extends BaseMapper<MedicineCategory> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.Medicine;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface MedicineMapper extends BaseMapper<Medicine> {}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.Menu;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface MenuMapper extends BaseMapper<Menu> {
|
||||
|
||||
@Select("<script>" +
|
||||
"SELECT DISTINCT m.permission_code FROM menu m " +
|
||||
"INNER JOIN role_menu rm ON m.id = rm.menu_id " +
|
||||
"WHERE rm.role_id IN <foreach item='id' collection='roleIds' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"AND m.permission_code IS NOT NULL AND m.permission_code != ''" +
|
||||
"</script>")
|
||||
List<String> selectPermsByRoleIds(@Param("roleIds") List<Long> roleIds);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.Role;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface RoleMapper extends BaseMapper<Role> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.RoleMenu;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface RoleMenuMapper extends BaseMapper<RoleMenu> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.StockInDetail;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface StockInDetailMapper extends BaseMapper<StockInDetail> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.StockInRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface StockInRecordMapper extends BaseMapper<StockInRecord> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.Stock;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface StockMapper extends BaseMapper<Stock> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.StockOutDetail;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface StockOutDetailMapper extends BaseMapper<StockOutDetail> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.StockOutRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface StockOutRecordMapper extends BaseMapper<StockOutRecord> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.Supplier;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SupplierMapper extends BaseMapper<Supplier> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.User;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<User> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.UserRole;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface UserRoleMapper extends BaseMapper<UserRole> {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kronecker.backend.entity.Warehouse;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface WarehouseMapper extends BaseMapper<Warehouse> {}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.kronecker.backend.security;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
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;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final UserDetailsService userDetailsService;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String token = extractToken(request);
|
||||
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
|
||||
Long userId = jwtTokenProvider.getUserId(token);
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(userId.toString());
|
||||
var auth = new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities());
|
||||
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private String extractToken(HttpServletRequest request) {
|
||||
String bearer = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
|
||||
return bearer.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.kronecker.backend.security;
|
||||
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
|
||||
private final SecretKey key;
|
||||
private final long expiration;
|
||||
private final long refreshExpiration;
|
||||
|
||||
public JwtTokenProvider(
|
||||
@Value("${jwt.secret}") String secret,
|
||||
@Value("${jwt.expiration}") long expiration,
|
||||
@Value("${jwt.refresh-expiration}") long refreshExpiration) {
|
||||
this.key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(secret));
|
||||
this.expiration = expiration;
|
||||
this.refreshExpiration = refreshExpiration;
|
||||
}
|
||||
|
||||
public String generateToken(Long userId, String username) {
|
||||
Date now = new Date();
|
||||
return Jwts.builder()
|
||||
.subject(String.valueOf(userId))
|
||||
.claim("username", username)
|
||||
.issuedAt(now)
|
||||
.expiration(new Date(now.getTime() + expiration))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String generateRefreshToken(Long userId) {
|
||||
Date now = new Date();
|
||||
return Jwts.builder()
|
||||
.subject(String.valueOf(userId))
|
||||
.issuedAt(now)
|
||||
.expiration(new Date(now.getTime() + refreshExpiration))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public Long getUserId(String token) {
|
||||
return Long.parseLong(parseClaims(token).getSubject());
|
||||
}
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
parseClaims(token);
|
||||
return true;
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Claims parseClaims(String token) {
|
||||
return Jwts.parser().verifyWith(key).build()
|
||||
.parseSignedClaims(token).getPayload();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.kronecker.backend.security;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.kronecker.backend.entity.Menu;
|
||||
import com.kronecker.backend.entity.User;
|
||||
import com.kronecker.backend.entity.UserRole;
|
||||
import com.kronecker.backend.mapper.MenuMapper;
|
||||
import com.kronecker.backend.mapper.UserMapper;
|
||||
import com.kronecker.backend.mapper.UserRoleMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
private final MenuMapper menuMapper;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
|
||||
User user = userMapper.selectById(Long.parseLong(userId));
|
||||
if (user == null || user.getStatus() == 0) {
|
||||
throw new UsernameNotFoundException("用户不存在或已停用");
|
||||
}
|
||||
|
||||
// 查角色 → 查权限
|
||||
List<Long> roleIds = userRoleMapper.selectList(
|
||||
new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, user.getId())
|
||||
).stream().map(UserRole::getRoleId).toList();
|
||||
|
||||
List<String> permissions = new ArrayList<>();
|
||||
if (!roleIds.isEmpty()) {
|
||||
permissions = menuMapper.selectPermsByRoleIds(roleIds);
|
||||
}
|
||||
|
||||
List<SimpleGrantedAuthority> authorities = permissions.stream()
|
||||
.filter(p -> p != null && !p.isEmpty())
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new org.springframework.security.core.userdetails.User(
|
||||
user.getId().toString(), user.getPassword(), authorities);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.kronecker.backend.service;
|
||||
|
||||
import com.kronecker.backend.dto.RegisterDTO;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface AuthService {
|
||||
Map<String, Object> login(String username, String password);
|
||||
String refreshToken(String refreshToken);
|
||||
Map<String, Object> getUserInfo(Long userId);
|
||||
void register(RegisterDTO dto);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.kronecker.backend.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kronecker.backend.entity.Stock;
|
||||
import com.kronecker.backend.entity.StockInRecord;
|
||||
import com.kronecker.backend.entity.StockOutRecord;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
public interface DashboardService {
|
||||
java.util.Map<String, Object> getStatistics();
|
||||
Page<Stock> getExpiryWarning(Page<Stock> page);
|
||||
Page<StockInRecord> getRecentStockIn(Page<StockInRecord> page);
|
||||
Page<StockOutRecord> getRecentStockOut(Page<StockOutRecord> page);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kronecker.backend.entity.Medicine;
|
||||
|
||||
public interface MedicineService extends IService<Medicine> {
|
||||
Page<Medicine> pageQuery(Page<Medicine> page, String name, Long categoryId);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.kronecker.backend.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kronecker.backend.entity.Menu;
|
||||
import java.util.List;
|
||||
|
||||
public interface MenuService extends IService<Menu> {
|
||||
List<Menu> getTree();
|
||||
List<Menu> getMenusByUserId(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.service;
|
||||
|
||||
import com.kronecker.backend.dto.StockInCreateDTO;
|
||||
|
||||
public interface StockInService {
|
||||
Long create(StockInCreateDTO dto, Long operatorId);
|
||||
void audit(Long id);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kronecker.backend.service;
|
||||
|
||||
import com.kronecker.backend.dto.StockOutCreateDTO;
|
||||
|
||||
public interface StockOutService {
|
||||
Long create(StockOutCreateDTO dto, Long operatorId);
|
||||
void audit(Long id);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.kronecker.backend.common.BusinessException;
|
||||
import com.kronecker.backend.common.ResultCode;
|
||||
import com.kronecker.backend.dto.RegisterDTO;
|
||||
import com.kronecker.backend.entity.*;
|
||||
import com.kronecker.backend.mapper.*;
|
||||
import com.kronecker.backend.security.JwtTokenProvider;
|
||||
import com.kronecker.backend.service.AuthService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final MenuMapper menuMapper;
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> login(String username, String password) {
|
||||
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
|
||||
if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
|
||||
throw new BusinessException(ResultCode.UNAUTHORIZED.getCode(), "用户名或密码错误");
|
||||
}
|
||||
if (user.getStatus() == 0) {
|
||||
throw new BusinessException("账号已被停用");
|
||||
}
|
||||
user.setLastLoginTime(LocalDateTime.now());
|
||||
userMapper.updateById(user);
|
||||
|
||||
String token = jwtTokenProvider.generateToken(user.getId(), user.getUsername());
|
||||
String refreshToken = jwtTokenProvider.generateRefreshToken(user.getId());
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("token", token);
|
||||
data.put("refreshToken", refreshToken);
|
||||
data.put("userInfo", buildUserInfo(user));
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String refreshToken(String refreshToken) {
|
||||
if (!jwtTokenProvider.validateToken(refreshToken)) {
|
||||
throw new BusinessException(ResultCode.UNAUTHORIZED.getCode(), "Token已过期,请重新登录");
|
||||
}
|
||||
Long userId = jwtTokenProvider.getUserId(refreshToken);
|
||||
User user = userMapper.selectById(userId);
|
||||
return jwtTokenProvider.generateToken(user.getId(), user.getUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getUserInfo(Long userId) {
|
||||
User user = userMapper.selectById(userId);
|
||||
return buildUserInfo(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void register(RegisterDTO dto) {
|
||||
if (userMapper.selectCount(new LambdaQueryWrapper<User>().eq(User::getUsername, dto.getUsername())) > 0) {
|
||||
throw new BusinessException("用户名已存在");
|
||||
}
|
||||
User user = new User();
|
||||
user.setUsername(dto.getUsername());
|
||||
user.setPassword(passwordEncoder.encode(dto.getPassword()));
|
||||
user.setRealName(dto.getRealName() != null ? dto.getRealName() : dto.getUsername());
|
||||
user.setPhone(dto.getPhone());
|
||||
user.setEmail(dto.getEmail());
|
||||
user.setStatus(1);
|
||||
userMapper.insert(user);
|
||||
|
||||
// 分配默认角色(普通员工,role code = staff, id = 3)
|
||||
UserRole ur = new UserRole();
|
||||
ur.setUserId(user.getId());
|
||||
ur.setRoleId(3L);
|
||||
userRoleMapper.insert(ur);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildUserInfo(User user) {
|
||||
Map<String, Object> info = new HashMap<>();
|
||||
info.put("id", user.getId());
|
||||
info.put("username", user.getUsername());
|
||||
info.put("realName", user.getRealName());
|
||||
info.put("phone", user.getPhone());
|
||||
info.put("email", user.getEmail());
|
||||
info.put("avatar", user.getAvatar());
|
||||
return info;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.MedicineCategory;
|
||||
import com.kronecker.backend.mapper.MedicineCategoryMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class CategoryServiceImpl extends ServiceImpl<MedicineCategoryMapper, MedicineCategory> {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.Customer;
|
||||
import com.kronecker.backend.mapper.CustomerMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> {}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.kronecker.backend.entity.*;
|
||||
import com.kronecker.backend.mapper.*;
|
||||
import com.kronecker.backend.service.DashboardService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardServiceImpl implements DashboardService {
|
||||
|
||||
private final MedicineMapper medicineMapper;
|
||||
private final StockMapper stockMapper;
|
||||
private final StockInRecordMapper stockInRecordMapper;
|
||||
private final StockOutRecordMapper stockOutRecordMapper;
|
||||
private final SupplierMapper supplierMapper;
|
||||
private final CustomerMapper customerMapper;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getStatistics() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("medicineCount", medicineMapper.selectCount(null));
|
||||
stats.put("stockBatchCount", stockMapper.selectCount(null));
|
||||
stats.put("pendingStockIn", stockInRecordMapper.selectCount(
|
||||
new LambdaQueryWrapper<StockInRecord>().eq(StockInRecord::getStatus, 0)));
|
||||
stats.put("expiryWarningCount", stockMapper.selectCount(
|
||||
new LambdaQueryWrapper<Stock>().le(Stock::getExpiryDate, LocalDate.now().plusDays(90))));
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<Stock> getExpiryWarning(Page<Stock> page) {
|
||||
Page<Stock> result = stockMapper.selectPage(page,
|
||||
new LambdaQueryWrapper<Stock>()
|
||||
.le(Stock::getExpiryDate, LocalDate.now().plusDays(90))
|
||||
.orderByAsc(Stock::getExpiryDate));
|
||||
populateStockNames(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<StockInRecord> getRecentStockIn(Page<StockInRecord> page) {
|
||||
Page<StockInRecord> result = stockInRecordMapper.selectPage(page,
|
||||
new LambdaQueryWrapper<StockInRecord>().orderByDesc(StockInRecord::getCreateTime));
|
||||
populateInRecordNames(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<StockOutRecord> getRecentStockOut(Page<StockOutRecord> page) {
|
||||
Page<StockOutRecord> result = stockOutRecordMapper.selectPage(page,
|
||||
new LambdaQueryWrapper<StockOutRecord>().orderByDesc(StockOutRecord::getCreateTime));
|
||||
populateOutRecordNames(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
private void populateStockNames(java.util.List<Stock> stocks) {
|
||||
if (stocks.isEmpty()) return;
|
||||
var medIds = stocks.stream().map(Stock::getMedicineId).collect(java.util.stream.Collectors.toSet());
|
||||
var medMap = medicineMapper.selectBatchIds(medIds).stream()
|
||||
.collect(java.util.stream.Collectors.toMap(Medicine::getId, Medicine::getName));
|
||||
stocks.forEach(s -> s.setMedicineName(medMap.getOrDefault(s.getMedicineId(), "")));
|
||||
}
|
||||
|
||||
private void populateInRecordNames(java.util.List<StockInRecord> records) {
|
||||
if (records.isEmpty()) return;
|
||||
var suppIds = records.stream().map(StockInRecord::getSupplierId).collect(java.util.stream.Collectors.toSet());
|
||||
var suppMap = supplierMapper.selectBatchIds(suppIds).stream()
|
||||
.collect(java.util.stream.Collectors.toMap(Supplier::getId, Supplier::getName));
|
||||
records.forEach(r -> r.setSupplierName(suppMap.getOrDefault(r.getSupplierId(), "")));
|
||||
}
|
||||
|
||||
private void populateOutRecordNames(java.util.List<StockOutRecord> records) {
|
||||
if (records.isEmpty()) return;
|
||||
var custIds = records.stream().map(StockOutRecord::getCustomerId)
|
||||
.filter(id -> id != null).collect(java.util.stream.Collectors.toSet());
|
||||
if (!custIds.isEmpty()) {
|
||||
var custMap = customerMapper.selectBatchIds(custIds).stream()
|
||||
.collect(java.util.stream.Collectors.toMap(Customer::getId, Customer::getName));
|
||||
records.forEach(r -> { if (r.getCustomerId() != null) r.setCustomerName(custMap.getOrDefault(r.getCustomerId(), "")); });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.Medicine;
|
||||
import com.kronecker.backend.mapper.MedicineMapper;
|
||||
import com.kronecker.backend.service.MedicineService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@Service
|
||||
public class MedicineServiceImpl extends ServiceImpl<MedicineMapper, Medicine> implements MedicineService {
|
||||
|
||||
@Override
|
||||
public Page<Medicine> pageQuery(Page<Medicine> page, String name, Long categoryId) {
|
||||
LambdaQueryWrapper<Medicine> wrapper = new LambdaQueryWrapper<Medicine>()
|
||||
.like(StringUtils.hasText(name), Medicine::getName, name)
|
||||
.eq(categoryId != null, Medicine::getCategoryId, categoryId)
|
||||
.orderByDesc(Medicine::getCreateTime);
|
||||
return baseMapper.selectPage(page, wrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.Menu;
|
||||
import com.kronecker.backend.entity.RoleMenu;
|
||||
import com.kronecker.backend.entity.UserRole;
|
||||
import com.kronecker.backend.mapper.*;
|
||||
import com.kronecker.backend.service.MenuService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {
|
||||
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
private final RoleMenuMapper roleMenuMapper;
|
||||
private final MenuMapper menuMapper;
|
||||
|
||||
@Override
|
||||
public List<Menu> getTree() {
|
||||
List<Menu> all = menuMapper.selectList(
|
||||
new LambdaQueryWrapper<Menu>().orderByAsc(Menu::getSortOrder));
|
||||
return buildTree(all, 0L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Menu> getMenusByUserId(Long userId) {
|
||||
List<Long> roleIds = userRoleMapper.selectList(
|
||||
new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, userId)
|
||||
).stream().map(UserRole::getRoleId).toList();
|
||||
if (roleIds.isEmpty()) return Collections.emptyList();
|
||||
|
||||
Set<Long> menuIds = roleMenuMapper.selectList(
|
||||
new LambdaQueryWrapper<RoleMenu>().in(RoleMenu::getRoleId, roleIds)
|
||||
).stream().map(RoleMenu::getMenuId).collect(Collectors.toSet());
|
||||
if (menuIds.isEmpty()) return Collections.emptyList();
|
||||
|
||||
List<Menu> menus = menuMapper.selectList(
|
||||
new LambdaQueryWrapper<Menu>()
|
||||
.in(Menu::getId, menuIds)
|
||||
.orderByAsc(Menu::getSortOrder));
|
||||
return buildTree(menus, 0L);
|
||||
}
|
||||
|
||||
private List<Menu> buildTree(List<Menu> list, Long parentId) {
|
||||
return list.stream()
|
||||
.filter(m -> m.getParentId().equals(parentId))
|
||||
.peek(m -> m.setChildren(buildTree(list, m.getId())))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.Role;
|
||||
import com.kronecker.backend.mapper.RoleMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.StockInDetail;
|
||||
import com.kronecker.backend.mapper.StockInDetailMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class StockInDetailServiceImpl extends ServiceImpl<StockInDetailMapper, StockInDetail> {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.StockInRecord;
|
||||
import com.kronecker.backend.mapper.StockInRecordMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class StockInRecordServiceImpl extends ServiceImpl<StockInRecordMapper, StockInRecord> {}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.kronecker.backend.common.BusinessException;
|
||||
import com.kronecker.backend.dto.StockInCreateDTO;
|
||||
import com.kronecker.backend.entity.*;
|
||||
import com.kronecker.backend.mapper.*;
|
||||
import com.kronecker.backend.service.StockInService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StockInServiceImpl implements StockInService {
|
||||
|
||||
private final StockInRecordMapper recordMapper;
|
||||
private final StockInDetailMapper detailMapper;
|
||||
private final StockMapper stockMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long create(StockInCreateDTO dto, Long operatorId) {
|
||||
// 生成单号
|
||||
String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
long count = recordMapper.selectCount(new LambdaQueryWrapper<StockInRecord>()
|
||||
.likeRight(StockInRecord::getRecordNo, "SI" + date)) + 1;
|
||||
String recordNo = String.format("SI%s%04d", date, count);
|
||||
|
||||
StockInRecord record = new StockInRecord();
|
||||
record.setRecordNo(recordNo);
|
||||
record.setSupplierId(dto.getSupplierId());
|
||||
record.setWarehouseId(dto.getWarehouseId());
|
||||
record.setTotalAmount(dto.getTotalAmount());
|
||||
record.setOperatorId(operatorId);
|
||||
record.setInDate(dto.getInDate());
|
||||
record.setStatus(0);
|
||||
record.setRemark(dto.getRemark());
|
||||
recordMapper.insert(record);
|
||||
|
||||
for (var d : dto.getDetails()) {
|
||||
StockInDetail detail = new StockInDetail();
|
||||
detail.setRecordId(record.getId());
|
||||
detail.setMedicineId(d.getMedicineId());
|
||||
detail.setBatchNo(d.getBatchNo());
|
||||
detail.setQuantity(d.getQuantity());
|
||||
detail.setCostPrice(d.getCostPrice());
|
||||
detail.setTotalPrice(d.getTotalPrice());
|
||||
detail.setProductionDate(d.getProductionDate());
|
||||
detail.setExpiryDate(d.getExpiryDate());
|
||||
detailMapper.insert(detail);
|
||||
}
|
||||
return record.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void audit(Long id) {
|
||||
StockInRecord record = recordMapper.selectById(id);
|
||||
if (record == null || record.getStatus() != 0) {
|
||||
throw new BusinessException("入库单不存在或状态不正确");
|
||||
}
|
||||
record.setStatus(1);
|
||||
recordMapper.updateById(record);
|
||||
|
||||
// 更新库存
|
||||
var details = detailMapper.selectList(
|
||||
new LambdaQueryWrapper<StockInDetail>().eq(StockInDetail::getRecordId, id));
|
||||
for (var d : details) {
|
||||
// 查找同批号库存
|
||||
var stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>()
|
||||
.eq(Stock::getMedicineId, d.getMedicineId())
|
||||
.eq(Stock::getWarehouseId, record.getWarehouseId())
|
||||
.eq(Stock::getBatchNo, d.getBatchNo())
|
||||
.last("LIMIT 1"));
|
||||
if (stock != null) {
|
||||
stock.setQuantity(stock.getQuantity() + d.getQuantity());
|
||||
stock.setCostPrice(d.getCostPrice());
|
||||
stockMapper.updateById(stock);
|
||||
} else {
|
||||
Stock s = new Stock();
|
||||
s.setMedicineId(d.getMedicineId());
|
||||
s.setWarehouseId(record.getWarehouseId());
|
||||
s.setBatchNo(d.getBatchNo());
|
||||
s.setQuantity(d.getQuantity());
|
||||
s.setCostPrice(d.getCostPrice());
|
||||
s.setProductionDate(d.getProductionDate());
|
||||
s.setExpiryDate(d.getExpiryDate());
|
||||
stockMapper.insert(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.StockOutDetail;
|
||||
import com.kronecker.backend.mapper.StockOutDetailMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class StockOutDetailServiceImpl extends ServiceImpl<StockOutDetailMapper, StockOutDetail> {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.StockOutRecord;
|
||||
import com.kronecker.backend.mapper.StockOutRecordMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class StockOutRecordServiceImpl extends ServiceImpl<StockOutRecordMapper, StockOutRecord> {}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.kronecker.backend.common.BusinessException;
|
||||
import com.kronecker.backend.dto.StockOutCreateDTO;
|
||||
import com.kronecker.backend.entity.*;
|
||||
import com.kronecker.backend.mapper.*;
|
||||
import com.kronecker.backend.service.StockOutService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StockOutServiceImpl implements StockOutService {
|
||||
|
||||
private final StockOutRecordMapper recordMapper;
|
||||
private final StockOutDetailMapper detailMapper;
|
||||
private final StockMapper stockMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long create(StockOutCreateDTO dto, Long operatorId) {
|
||||
String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
long count = recordMapper.selectCount(new LambdaQueryWrapper<StockOutRecord>()
|
||||
.likeRight(StockOutRecord::getRecordNo, "SO" + date)) + 1;
|
||||
String recordNo = String.format("SO%s%04d", date, count);
|
||||
|
||||
StockOutRecord record = new StockOutRecord();
|
||||
record.setRecordNo(recordNo);
|
||||
record.setCustomerId(dto.getCustomerId());
|
||||
record.setWarehouseId(dto.getWarehouseId());
|
||||
record.setTotalAmount(dto.getTotalAmount());
|
||||
record.setOperatorId(operatorId);
|
||||
record.setOutDate(dto.getOutDate());
|
||||
record.setOutType(dto.getOutType() != null ? dto.getOutType() : 1);
|
||||
record.setStatus(0);
|
||||
record.setRemark(dto.getRemark());
|
||||
recordMapper.insert(record);
|
||||
|
||||
for (var d : dto.getDetails()) {
|
||||
StockOutDetail detail = new StockOutDetail();
|
||||
detail.setRecordId(record.getId());
|
||||
detail.setMedicineId(d.getMedicineId());
|
||||
detail.setBatchNo(d.getBatchNo());
|
||||
detail.setQuantity(d.getQuantity());
|
||||
detail.setUnitPrice(d.getUnitPrice());
|
||||
detail.setTotalPrice(d.getTotalPrice());
|
||||
detailMapper.insert(detail);
|
||||
}
|
||||
return record.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void audit(Long id) {
|
||||
StockOutRecord record = recordMapper.selectById(id);
|
||||
if (record == null || record.getStatus() != 0) {
|
||||
throw new BusinessException("出库单不存在或状态不正确");
|
||||
}
|
||||
var details = detailMapper.selectList(
|
||||
new LambdaQueryWrapper<StockOutDetail>().eq(StockOutDetail::getRecordId, id));
|
||||
|
||||
// 检查库存是否充足
|
||||
for (var d : details) {
|
||||
int totalStock = stockMapper.selectList(new LambdaQueryWrapper<Stock>()
|
||||
.eq(Stock::getMedicineId, d.getMedicineId())
|
||||
.eq(Stock::getWarehouseId, record.getWarehouseId())
|
||||
.eq(Stock::getBatchNo, d.getBatchNo()))
|
||||
.stream().mapToInt(Stock::getQuantity).sum();
|
||||
if (totalStock < d.getQuantity()) {
|
||||
throw new BusinessException("药品批次 " + d.getBatchNo() + " 库存不足");
|
||||
}
|
||||
}
|
||||
|
||||
// 扣减库存(FIFO: 先过期先出)
|
||||
for (var d : details) {
|
||||
int remaining = d.getQuantity();
|
||||
var stocks = stockMapper.selectList(new LambdaQueryWrapper<Stock>()
|
||||
.eq(Stock::getMedicineId, d.getMedicineId())
|
||||
.eq(Stock::getWarehouseId, record.getWarehouseId())
|
||||
.eq(Stock::getBatchNo, d.getBatchNo())
|
||||
.orderByAsc(Stock::getExpiryDate));
|
||||
for (Stock s : stocks) {
|
||||
if (remaining <= 0) break;
|
||||
int deduct = Math.min(s.getQuantity(), remaining);
|
||||
s.setQuantity(s.getQuantity() - deduct);
|
||||
stockMapper.updateById(s);
|
||||
remaining -= deduct;
|
||||
}
|
||||
}
|
||||
|
||||
record.setStatus(1);
|
||||
recordMapper.updateById(record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.Stock;
|
||||
import com.kronecker.backend.mapper.StockMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.Supplier;
|
||||
import com.kronecker.backend.mapper.SupplierMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class SupplierServiceImpl extends ServiceImpl<SupplierMapper, Supplier> {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.UserRole;
|
||||
import com.kronecker.backend.mapper.UserRoleMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.User;
|
||||
import com.kronecker.backend.mapper.UserMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UserServiceImpl extends ServiceImpl<UserMapper, User> {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kronecker.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kronecker.backend.entity.Warehouse;
|
||||
import com.kronecker.backend.mapper.WarehouseMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class WarehouseServiceImpl extends ServiceImpl<WarehouseMapper, Warehouse> {}
|
||||
32
backend/src/main/resources/application.yml
Normal file
32
backend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
server:
|
||||
port: ${SERVER_PORT:8080}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: backend
|
||||
datasource:
|
||||
url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:medicine_manager}?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
|
||||
username: ${DB_USERNAME:root}
|
||||
password: ${DB_PASSWORD:root}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: Asia/Shanghai
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
type-aliases-package: com.kronecker.backend.entity
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: auto
|
||||
logic-delete-field: isDeleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:Kzuxv0tsjN/GTO/udrRLgXj+Pr+uW1PL3PfGIXGstkQ=}
|
||||
expiration: ${JWT_EXPIRATION:86400000}
|
||||
refresh-expiration: ${JWT_REFRESH_EXPIRATION:604800000}
|
||||
@@ -411,7 +411,7 @@ INSERT INTO `role` (`id`, `name`, `code`, `description`, `status`) VALUES
|
||||
-- BCrypt 加密: $2a$10$... 共 60 字符
|
||||
-- -----------------------------------------------------------
|
||||
INSERT INTO `user` (`id`, `username`, `password`, `real_name`, `phone`, `email`, `status`) VALUES
|
||||
(1, 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5EH', '系统管理员', '13800000000', 'admin@medicine.com', 1);
|
||||
(1, 'admin', '$2b$10$8Mo2a8QMdoXKVKYxlAqsy.qg2ruYDQ1NaWbc6VArXT6NSU.vuOJC6', '系统管理员', '13800000000', 'admin@medicine.com', 1);
|
||||
|
||||
-- -----------------------------------------------------------
|
||||
-- 用户角色关联(admin 用户 → 超级管理员角色)
|
||||
@@ -541,8 +541,8 @@ INSERT INTO `role_menu` (`role_id`, `menu_id`) VALUES
|
||||
-- 仓库管理员用户(用户名: warehouse01 / 密码: admin123)
|
||||
-- -----------------------------------------------------------
|
||||
INSERT INTO `user` (`id`, `username`, `password`, `real_name`, `phone`, `email`, `status`) VALUES
|
||||
(2, 'warehouse01', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5EH', '张伟', '13800000001', 'zhangwei@medicine.com', 1),
|
||||
(3, 'staff01', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5EH', '李娜', '13800000002', 'lina@medicine.com', 1);
|
||||
(2, 'warehouse01', '$2b$10$8Mo2a8QMdoXKVKYxlAqsy.qg2ruYDQ1NaWbc6VArXT6NSU.vuOJC6', '张伟', '13800000001', 'zhangwei@medicine.com', 1),
|
||||
(3, 'staff01', '$2b$10$8Mo2a8QMdoXKVKYxlAqsy.qg2ruYDQ1NaWbc6VArXT6NSU.vuOJC6', '李娜', '13800000002', 'lina@medicine.com', 1);
|
||||
|
||||
-- -----------------------------------------------------------
|
||||
-- 用户角色关联
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.kronecker.backend;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@@ -7,6 +8,7 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
class BackendApplicationTests {
|
||||
|
||||
@Test
|
||||
@Disabled("需要先配置数据库连接,开发阶段跳过")
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
|
||||
2
frontend/.env.development
Normal file
2
frontend/.env.development
Normal file
@@ -0,0 +1,2 @@
|
||||
# 开发环境(Vite dev 模式下自动加载)
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
6
frontend/.env.example
Normal file
6
frontend/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# ============================================================
|
||||
# 前端环境变量(Vite 原生支持,复制为 .env 即可生效)
|
||||
# ============================================================
|
||||
|
||||
# API 代理目标地址
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
@@ -12,6 +12,10 @@ export function getMenusApi() {
|
||||
return request.get('/menus/tree')
|
||||
}
|
||||
|
||||
export function registerApi(data) {
|
||||
return request.post('/auth/register', data)
|
||||
}
|
||||
|
||||
export function refreshTokenApi(data) {
|
||||
return request.post('/auth/refresh', data)
|
||||
}
|
||||
|
||||
@@ -24,14 +24,17 @@ function renderMenu(menus) {
|
||||
return menus
|
||||
.filter((m) => m.type !== 3 && m.visible !== 0)
|
||||
.map((menu) => {
|
||||
if (menu.children?.length) {
|
||||
// 递归处理子节点,过滤掉 type=3 后的有效子节点
|
||||
const children = menu.children?.length ? renderMenu(menu.children) : []
|
||||
if (children.length) {
|
||||
return {
|
||||
index: menu.path || menu.name,
|
||||
title: menu.name,
|
||||
icon: menu.icon,
|
||||
children: renderMenu(menu.children),
|
||||
children,
|
||||
}
|
||||
}
|
||||
// 无有效子节点 → 渲染为可点击的叶子菜单项
|
||||
return {
|
||||
index: menu.path?.toLowerCase() || '',
|
||||
title: menu.name,
|
||||
@@ -41,6 +44,21 @@ function renderMenu(menus) {
|
||||
}
|
||||
|
||||
const menuItems = computed(() => renderMenu(userStore.menus))
|
||||
|
||||
// 所有有子菜单的父级 index,保持展开
|
||||
const openedMenus = computed(() => {
|
||||
const indices = []
|
||||
function collect(menus) {
|
||||
menus.forEach(m => {
|
||||
if (m.children?.length) {
|
||||
indices.push(m.index || m.name)
|
||||
collect(m.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
collect(userStore.menus)
|
||||
return indices
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -48,27 +66,29 @@ const menuItems = computed(() => renderMenu(userStore.menus))
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside :width="isCollapse ? '64px' : '220px'" class="aside">
|
||||
<div class="logo">
|
||||
<span v-if="!isCollapse">💊 药品管理系统</span>
|
||||
<span v-else>💊</span>
|
||||
<span v-if="!isCollapse"> 药品管理系统</span>
|
||||
<span v-else></span>
|
||||
</div>
|
||||
<el-menu
|
||||
:key="menuItems.length"
|
||||
:default-active="activeMenu"
|
||||
:default-openeds="openedMenus"
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
router
|
||||
@select="(index) => { if (index) router.push(index) }"
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#409eff"
|
||||
>
|
||||
<template v-for="item in menuItems" :key="item.index">
|
||||
<!-- 有子菜单 -->
|
||||
<el-sub-menu v-if="item.children" :index="item.index">
|
||||
<el-sub-menu v-if="item.children?.length" :index="item.index">
|
||||
<template #title>
|
||||
<el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
|
||||
<span>{{ item.title }}</span>
|
||||
</template>
|
||||
<template v-for="child in item.children" :key="child.index">
|
||||
<el-sub-menu v-if="child.children" :index="child.index">
|
||||
<el-sub-menu v-if="child.children?.length" :index="child.index">
|
||||
<template #title>{{ child.title }}</template>
|
||||
<el-menu-item
|
||||
v-for="sub in child.children"
|
||||
|
||||
@@ -10,6 +10,12 @@ const constantRoutes = [
|
||||
component: () => import('@/views/login/LoginView.vue'),
|
||||
meta: { title: '登录', noAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: () => import('@/views/login/RegisterView.vue'),
|
||||
meta: { title: '注册', noAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'MainLayout',
|
||||
@@ -48,6 +54,7 @@ export const componentMap = {
|
||||
'views/stockOut/StockOutForm': () => import('@/views/stockOut/StockOutForm.vue'),
|
||||
'views/user/UserList': () => import('@/views/user/UserList.vue'),
|
||||
'views/role/RoleList': () => import('@/views/role/RoleList.vue'),
|
||||
'views/menu/MenuList': () => import('@/views/menu/MenuList.vue'),
|
||||
}
|
||||
|
||||
// 根据后端菜单数据生成路由
|
||||
@@ -79,13 +86,12 @@ router.beforeEach(async (to, _from, next) => {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 暂时跳过登录验证(开发阶段)
|
||||
// if (!userStore.token) {
|
||||
// next('/login')
|
||||
// return
|
||||
// }
|
||||
if (!userStore.token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 动态注册菜单路由(仅首次),开发阶段使用本地 mock 菜单
|
||||
// 动态注册菜单路由(仅首次)
|
||||
if (!isMenusLoaded) {
|
||||
try {
|
||||
const menus = await userStore.getMenus()
|
||||
|
||||
@@ -25,9 +25,9 @@ const mockMenus = [
|
||||
]
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const token = ref(localStorage.getItem('token') || 'dev-token') // TODO: 开发阶段默认有 token
|
||||
const refreshToken = ref(localStorage.getItem('refreshToken') || 'dev-refresh')
|
||||
const userInfo = ref({ username: 'admin', realName: '管理员' }) // TODO: 开发阶段 mock
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const refreshToken = ref(localStorage.getItem('refreshToken') || '')
|
||||
const userInfo = ref({})
|
||||
const menus = ref([])
|
||||
const permissions = ref([])
|
||||
|
||||
@@ -35,7 +35,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
const perms = []
|
||||
function walk(list) {
|
||||
list.forEach((item) => {
|
||||
if (item.permission_code) perms.push(item.permission_code)
|
||||
const code = item.permissionCode || item.permission_code
|
||||
if (code) perms.push(code)
|
||||
if (item.children?.length) walk(item.children)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getStatistics, getExpiryWarning } from '@/api/dashboard'
|
||||
import { getStockInList } from '@/api/stockIn'
|
||||
import { getStockOutList } from '@/api/stockOut'
|
||||
import { Goods, Box, DocumentChecked, Warning } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const statistics = ref({ medicineCount: 0, stockBatchCount: 0, pendingStockIn: 0, expiryWarningCount: 0 })
|
||||
const expiryList = ref([])
|
||||
@@ -18,31 +19,30 @@ const statCards = [
|
||||
]
|
||||
|
||||
function expiryColor(date) {
|
||||
if (!date) return ''
|
||||
const days = Math.ceil((new Date(date) - new Date()) / (1000 * 60 * 60 * 24))
|
||||
if (days < 30) return 'danger'
|
||||
if (days < 90) return 'warning'
|
||||
return ''
|
||||
return 'success'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [statRes, warnRes, inRes, outRes] = await Promise.all([
|
||||
getStatistics(),
|
||||
getExpiryWarning(),
|
||||
const [statRes, warnRes, inRes, outRes] = await Promise.allSettled([
|
||||
getStatistics(), getExpiryWarning(),
|
||||
getStockInList({ page: 1, size: 5 }),
|
||||
getStockOutList({ page: 1, size: 5 }),
|
||||
])
|
||||
statistics.value = statRes.data
|
||||
expiryList.value = warnRes.data?.records || warnRes.data || []
|
||||
recentInList.value = inRes.data?.records || []
|
||||
recentOutList.value = outRes.data?.records || []
|
||||
} catch { /* 后端未启动,静默处理 */ }
|
||||
if (statRes.status === 'fulfilled') statistics.value = statRes.value.data || {}
|
||||
if (warnRes.status === 'fulfilled') expiryList.value = warnRes.value.data?.records || warnRes.value.data || []
|
||||
if (inRes.status === 'fulfilled') recentInList.value = inRes.value.data?.records || []
|
||||
if (outRes.status === 'fulfilled') recentOutList.value = outRes.value.data?.records || []
|
||||
} catch { /* 后端未启动 */ }
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stat-cards">
|
||||
<div v-for="card in statCards" :key="card.key" class="stat-card">
|
||||
<div class="stat-icon" :style="{ background: card.color }">
|
||||
@@ -55,31 +55,27 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 效期预警 -->
|
||||
<el-card class="section-card" header="效期预警(近90天)">
|
||||
<el-table :data="expiryList" size="small" empty-text="暂无预警数据">
|
||||
<el-table-column prop="medicineName" label="药品名称" />
|
||||
<el-table-column prop="batchNo" label="批号" />
|
||||
<el-card class="section-card" header="效期预警(90天内到期)">
|
||||
<el-table :data="expiryList" size="small" empty-text="暂无预警">
|
||||
<el-table-column prop="medicineName" label="药品" min-width="160" />
|
||||
<el-table-column prop="batchNo" label="批号" width="120" />
|
||||
<el-table-column prop="quantity" label="库存" width="80" />
|
||||
<el-table-column prop="expiryDate" label="有效期至" width="120">
|
||||
<el-table-column prop="expiryDate" label="有效期至" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="expiryColor(row.expiryDate || row.expiry_date)" size="small">
|
||||
{{ row.expiryDate || row.expiry_date }}
|
||||
</el-tag>
|
||||
<el-tag :type="expiryColor(row.expiryDate)" size="small">{{ row.expiryDate }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 最近记录 -->
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||||
<el-card header="最近入库">
|
||||
<template v-if="recentInList.length">
|
||||
<div v-for="item in recentInList" :key="item.id" class="record-item">
|
||||
<span>{{ item.recordNo || item.record_no }}</span>
|
||||
<span>{{ item.supplierName || '供应商' }}</span>
|
||||
<el-tag :type="item.status === 1 ? 'success' : 'warning'" size="small">
|
||||
{{ item.status === 1 ? '已入库' : item.status === 0 ? '待审核' : '已取消' }}
|
||||
<span>{{ item.recordNo }}</span>
|
||||
<span style="color:#909399;font-size:13px">{{ item.supplierName }}</span>
|
||||
<el-tag :type="item.status===1?'success':item.status===0?'warning':'info'" size="small">
|
||||
{{ item.status===1?'已入库':item.status===0?'待审核':'已取消' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
@@ -88,10 +84,10 @@ onMounted(async () => {
|
||||
<el-card header="最近出库">
|
||||
<template v-if="recentOutList.length">
|
||||
<div v-for="item in recentOutList" :key="item.id" class="record-item">
|
||||
<span>{{ item.recordNo || item.record_no }}</span>
|
||||
<span>{{ item.customerName || '客户' }}</span>
|
||||
<el-tag :type="item.status === 1 ? 'success' : 'warning'" size="small">
|
||||
{{ item.status === 1 ? '已出库' : item.status === 0 ? '待审核' : '已取消' }}
|
||||
<span>{{ item.recordNo }}</span>
|
||||
<span style="color:#909399;font-size:13px">{{ item.customerName || '-' }}</span>
|
||||
<el-tag :type="item.status===1?'success':item.status===0?'warning':'info'" size="small">
|
||||
{{ item.status===1?'已出库':item.status===0?'待审核':'已取消' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
@@ -102,19 +98,10 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.section-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-card { margin-bottom: 16px; }
|
||||
.record-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 10px 0; border-bottom: 1px solid #f0f0f0;
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -70,15 +70,13 @@ async function handleLogin() {
|
||||
<el-checkbox v-model="loginForm.remember">记住我</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
style="width: 100%"
|
||||
@click="handleLogin"
|
||||
>
|
||||
<el-button type="primary" :loading="loading" style="width:100%" @click="handleLogin">
|
||||
{{ loading ? '登录中...' : '登 录' }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button style="width:100%" @click="router.push('/register')">没有账号?去注册</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
117
frontend/src/views/login/RegisterView.vue
Normal file
117
frontend/src/views/login/RegisterView.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { registerApi } from '@/api/auth'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User, Lock, Phone, Message } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const formRef = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
realName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
})
|
||||
|
||||
const validatePass = (_rule, value, callback) => {
|
||||
if (value !== form.password) callback(new Error('两次密码不一致'))
|
||||
else callback()
|
||||
}
|
||||
|
||||
const rules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 32, message: '用户名长度3-32位', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 32, message: '密码长度6-32位', trigger: 'blur' },
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请再次输入密码', trigger: 'blur' },
|
||||
{ validator: validatePass, trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
async function handleRegister() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
try {
|
||||
await registerApi({
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
realName: form.realName,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
})
|
||||
ElMessage.success('注册成功,请登录')
|
||||
router.push('/login')
|
||||
} catch { /* error handled by interceptor */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
<h2 class="login-title">注册账号</h2>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" size="large" @keyup.enter="handleRegister">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="form.username" placeholder="用户名" :prefix-icon="User" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="realName">
|
||||
<el-input v-model="form.realName" placeholder="真实姓名(选填)" :prefix-icon="User" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password placeholder="密码" :prefix-icon="Lock" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input v-model="form.confirmPassword" type="password" show-password placeholder="确认密码" :prefix-icon="Lock" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="手机号(选填)" :prefix-icon="Phone" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="email">
|
||||
<el-input v-model="form.email" placeholder="邮箱(选填)" :prefix-icon="Message" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" style="width:100%" @click="handleRegister">
|
||||
{{ loading ? '注册中...' : '注 册' }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button style="width:100%" @click="router.push('/login')">已有账号?去登录</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.login-card {
|
||||
width: 420px;
|
||||
padding: 40px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
|
||||
}
|
||||
.login-title {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
color: #303133;
|
||||
font-size: 22px;
|
||||
}
|
||||
</style>
|
||||
153
frontend/src/views/menu/MenuList.vue
Normal file
153
frontend/src/views/menu/MenuList.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
|
||||
const tableData = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增菜单')
|
||||
const formRef = ref(null)
|
||||
const parentOptions = ref([])
|
||||
|
||||
const form = reactive({
|
||||
id: null, parentId: 0, name: '', type: 1, path: '', component: '',
|
||||
icon: '', permissionCode: '', sortOrder: 0, visible: 1,
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const res = await request.get('/menus')
|
||||
tableData.value = buildTree(res.data || [])
|
||||
parentOptions.value = flatten(res.data || [])
|
||||
} catch { /* */ }
|
||||
}
|
||||
|
||||
function buildTree(list, parentId = 0) {
|
||||
return list.filter(m => m.parentId === parentId).map(m => ({
|
||||
...m, children: buildTree(list, m.id) || undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
function flatten(list) {
|
||||
return list.map(m => ({ id: m.id, name: m.name, type: m.type }))
|
||||
}
|
||||
|
||||
function handleAdd(parentId = 0) {
|
||||
dialogTitle.value = '新增菜单'
|
||||
Object.keys(form).forEach(k => form[k] = null)
|
||||
form.id = null; form.parentId = parentId; form.type = 1; form.sortOrder = 0; form.visible = 1
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
dialogTitle.value = '编辑菜单'
|
||||
Object.assign(form, row)
|
||||
form.parentId = form.parentId || 0
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除"${row.name}"?子菜单也会被删除。`, '确认删除', { type: 'warning' })
|
||||
await request.delete(`/menus/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const data = { ...form }
|
||||
delete data.id; delete data.children
|
||||
if (form.id) {
|
||||
await request.put(`/menus/${form.id}`, data)
|
||||
ElMessage.success('编辑成功')
|
||||
} else {
|
||||
await request.post('/menus', data)
|
||||
ElMessage.success('新增成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="tool-bar">
|
||||
<el-button type="primary" v-permission="'menu:add'" :icon="Plus" @click="handleAdd(0)">新增顶级菜单</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="tableData" border stripe row-key="id" default-expand-all>
|
||||
<el-table-column prop="name" label="菜单名称" min-width="180" />
|
||||
<el-table-column prop="type" label="类型" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 1 ? '' : row.type === 2 ? 'success' : 'warning'" size="small">
|
||||
{{ { 1: '目录', 2: '菜单', 3: '按钮' }[row.type] }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路由路径" width="150" />
|
||||
<el-table-column prop="component" label="组件路径" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="icon" label="图标" width="100" />
|
||||
<el-table-column prop="permissionCode" label="权限编码" width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="sortOrder" label="排序" width="70" align="center" />
|
||||
<el-table-column prop="visible" label="可见" width="70" align="center">
|
||||
<template #default="{ row }">{{ row.visible === 1 ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="table-actions">
|
||||
<el-button size="small" v-permission="'menu:edit'" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" v-permission="'menu:add'" @click="handleAdd(row.id)">添加子级</el-button>
|
||||
<el-button size="small" type="danger" v-permission="'menu:delete'" @click="handleDelete(row)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px">
|
||||
<el-form ref="formRef" :model="form" label-width="90px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="上级菜单">
|
||||
<el-select v-model="form.parentId" placeholder="不选则为顶级" clearable style="width:100%">
|
||||
<el-option v-for="p in parentOptions.filter(x => x.id !== form.id && x.type !== 3)" :key="p.id" :label="p.name" :value="p.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="菜单类型">
|
||||
<el-select v-model="form.type" style="width:100%">
|
||||
<el-option label="目录" :value="1" />
|
||||
<el-option label="菜单" :value="2" />
|
||||
<el-option label="按钮" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="名称" required><el-input v-model="form.name" /></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="图标"><el-input v-model="form.icon" placeholder="Element Plus 图标名" /></el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.type !== 1" :span="12">
|
||||
<el-form-item label="路由路径"><el-input v-model="form.path" placeholder="type=2时填写,如 /user/list" /></el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.type !== 1" :span="12">
|
||||
<el-form-item label="组件路径"><el-input v-model="form.component" placeholder="type=2时填写" /></el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.type === 3" :span="12">
|
||||
<el-form-item label="权限编码"><el-input v-model="form.permissionCode" placeholder="如 user:add" /></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" style="width:100%" /></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="可见"><el-switch v-model="form.visible" :active-value="1" :inactive-value="0" /></el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" @click="handleSubmit">确定</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,26 +1,53 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getStockInList, auditStockIn } from '@/api/stockIn'
|
||||
import { getStockInList, getStockInById, auditStockIn } from '@/api/stockIn'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const query = reactive({ page: 1, size: 10 })
|
||||
const query = reactive({ page: 1, size: 10, recordNo: '' })
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const detailRecord = ref({})
|
||||
const detailItems = ref([])
|
||||
|
||||
function statusTag(status) {
|
||||
return { 0: 'warning', 1: 'success', 2: 'info' }[status] || 'info'
|
||||
}
|
||||
function statusText(status) {
|
||||
return { 0: '待审核', 1: '已入库', 2: '已取消' }[status] || '未知'
|
||||
const statusMap = { 0: { type: 'warning', text: '待审核' }, 1: { type: 'success', text: '已入库' }, 2: { type: 'info', text: '已取消' } }
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await getStockInList(query)
|
||||
tableData.value = r.data?.records || []
|
||||
total.value = r.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function loadData() { try { const r = await getStockInList(query); tableData.value = r.data?.records || []; total.value = r.data?.total || 0 } catch { /* */ } }
|
||||
function handleCreate() { router.push('/stock-in/add') }
|
||||
function handleView(row) { /* 查看详情弹窗 */ ElMessage.info('入库单号: ' + (row.recordNo || row.record_no)) }
|
||||
async function handleAudit(row) { await auditStockIn(row.id, { status: 1 }); ElMessage.success('审核通过,库存已更新'); loadData() }
|
||||
async function handleView(row) {
|
||||
try {
|
||||
const r = await getStockInById(row.id)
|
||||
detailRecord.value = r.data || row
|
||||
detailItems.value = r.data?.details || []
|
||||
detailVisible.value = true
|
||||
} catch { ElMessage.error('获取详情失败') }
|
||||
}
|
||||
|
||||
async function handleAudit(row) {
|
||||
await ElMessageBox.confirm(`确认审核通过入库单 ${row.recordNo}?审核后库存将自动更新。`, '审核确认', { type: 'success' })
|
||||
await auditStockIn(row.id, { status: 1 })
|
||||
ElMessage.success('审核通过,库存已更新')
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function handleReject(row) {
|
||||
await ElMessageBox.confirm(`确认驳回入库单 ${row.recordNo}?`, '驳回确认', { type: 'warning' })
|
||||
await auditStockIn(row.id, { status: 2 })
|
||||
ElMessage.success('已驳回')
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
@@ -28,30 +55,72 @@ onMounted(loadData)
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="query.recordNo" placeholder="入库单号" clearable style="width:180px" />
|
||||
<el-input v-model="query.recordNo" placeholder="入库单号" clearable style="width:200px" @keyup.enter="loadData" />
|
||||
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>
|
||||
<el-button :icon="Refresh" @click="Object.keys(query).forEach(k=>query[k]=k==='page'?1:k==='size'?10:'');loadData()">重置</el-button>
|
||||
<el-button :icon="Refresh" @click="query.recordNo='';loadData()">重置</el-button>
|
||||
</div>
|
||||
<div class="tool-bar">
|
||||
<el-button type="primary" v-permission="'stock-in:add'" :icon="Plus" @click="handleCreate">+ 新建入库单</el-button>
|
||||
<el-button type="primary" v-permission="'stock-in:add'" :icon="Plus" @click="handleCreate">新建入库单</el-button>
|
||||
</div>
|
||||
<el-table :data="tableData" border stripe>
|
||||
<el-table-column prop="recordNo" label="入库单号" width="150" />
|
||||
|
||||
<el-table :data="tableData" border stripe v-loading="loading">
|
||||
<el-table-column prop="recordNo" label="入库单号" width="160" />
|
||||
<el-table-column prop="supplierName" label="供应商" min-width="180" />
|
||||
<el-table-column prop="totalAmount" label="总金额" width="110"><template #default="{row}">¥{{row.totalAmount}}</template></el-table-column>
|
||||
<el-table-column prop="totalAmount" label="总金额" width="120">
|
||||
<template #default="{ row }">¥{{ row.totalAmount }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="inDate" label="入库日期" width="120" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{row}"><el-tag :type="statusTag(row.status)" size="small">{{statusText(row.status)}}</el-tag></template>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusMap[row.status]?.type || 'info'" size="small">
|
||||
{{ statusMap[row.status]?.text || '未知' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{row}">
|
||||
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="table-actions">
|
||||
<el-button size="small" @click="handleView(row)">查看</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="success" v-permission="'stock-in:audit'" @click="handleAudit(row)">审核</el-button>
|
||||
<template v-if="row.status === 0">
|
||||
<el-button size="small" type="success" v-permission="'stock-in:audit'" @click="handleAudit(row)">通过</el-button>
|
||||
<el-button size="small" type="danger" @click="handleReject(row)">驳回</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:16px"><el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next" @current-change="loadData" @size-change="query.page=1;loadData()" /></div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:16px">
|
||||
<el-pagination
|
||||
v-model:current-page="query.page" v-model:page-size="query.size"
|
||||
:total="total" :page-sizes="[10,20,50]"
|
||||
layout="total,sizes,prev,pager,next"
|
||||
@current-change="loadData" @size-change="query.page=1;loadData()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog v-model="detailVisible" title="入库单详情" width="750px">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="入库单号">{{ detailRecord.recordNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商">{{ detailRecord.supplierName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="入库日期">{{ detailRecord.inDate }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总金额">¥{{ detailRecord.totalAmount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusMap[detailRecord.status]?.type" size="small">{{ statusMap[detailRecord.status]?.text }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">{{ detailRecord.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<h4 style="margin:16px 0 8px">药品明细</h4>
|
||||
<el-table :data="detailItems" border size="small" max-height="300">
|
||||
<el-table-column prop="medicineName" label="药品" min-width="160" />
|
||||
<el-table-column prop="batchNo" label="批号" width="120" />
|
||||
<el-table-column prop="quantity" label="数量" width="80" />
|
||||
<el-table-column prop="costPrice" label="进价" width="90"><template #default="{row}">¥{{row.costPrice}}</template></el-table-column>
|
||||
<el-table-column prop="totalPrice" label="小计" width="100"><template #default="{row}">¥{{row.totalPrice}}</template></el-table-column>
|
||||
<el-table-column prop="expiryDate" label="有效期至" width="120" />
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,23 +1,54 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getStockOutList, auditStockOut } from '@/api/stockOut'
|
||||
import { getStockOutList, getStockOutById, auditStockOut } from '@/api/stockOut'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const query = reactive({ page: 1, size: 10 })
|
||||
const query = reactive({ page: 1, size: 10, recordNo: '' })
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const detailRecord = ref({})
|
||||
const detailItems = ref([])
|
||||
|
||||
function statusTag(s) { return { 0: 'warning', 1: 'success', 2: 'info' }[s] || 'info' }
|
||||
function statusText(s) { return { 0: '待审核', 1: '已出库', 2: '已取消' }[s] || '未知' }
|
||||
function typeText(s) { return { 1: '销售出库', 2: '领用出库', 3: '报损出库', 4: '退货出库' }[s] || '未知' }
|
||||
const statusMap = { 0: { type: 'warning', text: '待审核' }, 1: { type: 'success', text: '已出库' }, 2: { type: 'info', text: '已取消' } }
|
||||
const typeMap = { 1: '销售出库', 2: '领用出库', 3: '报损出库', 4: '退货出库' }
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await getStockOutList(query)
|
||||
tableData.value = r.data?.records || []
|
||||
total.value = r.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function loadData() { try { const r = await getStockOutList(query); tableData.value = r.data?.records || []; total.value = r.data?.total || 0 } catch { /* */ } }
|
||||
function handleCreate() { router.push('/stock-out/add') }
|
||||
function handleView(row) { ElMessage.info('出库单号: ' + (row.recordNo || row.record_no)) }
|
||||
async function handleAudit(row) { await auditStockOut(row.id, { status: 1 }); ElMessage.success('审核通过,库存已扣减'); loadData() }
|
||||
async function handleView(row) {
|
||||
try {
|
||||
const r = await getStockOutById(row.id)
|
||||
detailRecord.value = r.data || row
|
||||
detailItems.value = r.data?.details || []
|
||||
detailVisible.value = true
|
||||
} catch { ElMessage.error('获取详情失败') }
|
||||
}
|
||||
|
||||
async function handleAudit(row) {
|
||||
await ElMessageBox.confirm(`确认审核通过出库单 ${row.recordNo}?审核后库存将自动扣减。`, '审核确认', { type: 'success' })
|
||||
await auditStockOut(row.id, { status: 1 })
|
||||
ElMessage.success('审核通过,库存已扣减')
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function handleReject(row) {
|
||||
await ElMessageBox.confirm(`确认驳回出库单 ${row.recordNo}?`, '驳回确认', { type: 'warning' })
|
||||
await auditStockOut(row.id, { status: 2 })
|
||||
ElMessage.success('已驳回')
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
@@ -25,31 +56,75 @@ onMounted(loadData)
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="query.recordNo" placeholder="出库单号" clearable style="width:180px" />
|
||||
<el-input v-model="query.recordNo" placeholder="出库单号" clearable style="width:200px" @keyup.enter="loadData" />
|
||||
<el-button type="primary" :icon="Search" @click="loadData">搜索</el-button>
|
||||
<el-button :icon="Refresh" @click="Object.keys(query).forEach(k=>query[k]=k==='page'?1:k==='size'?10:'');loadData()">重置</el-button>
|
||||
<el-button :icon="Refresh" @click="query.recordNo='';loadData()">重置</el-button>
|
||||
</div>
|
||||
<div class="tool-bar">
|
||||
<el-button type="primary" v-permission="'stock-out:add'" :icon="Plus" @click="handleCreate">+ 新建出库单</el-button>
|
||||
<el-button type="primary" v-permission="'stock-out:add'" :icon="Plus" @click="handleCreate">新建出库单</el-button>
|
||||
</div>
|
||||
<el-table :data="tableData" border stripe>
|
||||
<el-table-column prop="recordNo" label="出库单号" width="150" />
|
||||
<el-table-column prop="customerName" label="客户" min-width="160" />
|
||||
<el-table-column prop="outType" label="类型" width="100"><template #default="{row}">{{typeText(row.outType)}}</template></el-table-column>
|
||||
<el-table-column prop="totalAmount" label="总金额" width="110"><template #default="{row}">¥{{row.totalAmount}}</template></el-table-column>
|
||||
|
||||
<el-table :data="tableData" border stripe v-loading="loading">
|
||||
<el-table-column prop="recordNo" label="出库单号" width="160" />
|
||||
<el-table-column prop="customerName" label="客户" min-width="160">
|
||||
<template #default="{ row }">{{ row.customerName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="outType" label="类型" width="110">
|
||||
<template #default="{ row }">{{ typeMap[row.outType] || '未知' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalAmount" label="总金额" width="120">
|
||||
<template #default="{ row }">¥{{ row.totalAmount }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="outDate" label="出库日期" width="120" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{row}"><el-tag :type="statusTag(row.status)" size="small">{{statusText(row.status)}}</el-tag></template>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusMap[row.status]?.type || 'info'" size="small">
|
||||
{{ statusMap[row.status]?.text || '未知' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{row}">
|
||||
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="table-actions">
|
||||
<el-button size="small" @click="handleView(row)">查看</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="success" v-permission="'stock-out:audit'" @click="handleAudit(row)">审核</el-button>
|
||||
<template v-if="row.status === 0">
|
||||
<el-button size="small" type="success" v-permission="'stock-out:audit'" @click="handleAudit(row)">通过</el-button>
|
||||
<el-button size="small" type="danger" @click="handleReject(row)">驳回</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:16px"><el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next" @current-change="loadData" @size-change="query.page=1;loadData()" /></div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:16px">
|
||||
<el-pagination
|
||||
v-model:current-page="query.page" v-model:page-size="query.size"
|
||||
:total="total" :page-sizes="[10,20,50]"
|
||||
layout="total,sizes,prev,pager,next"
|
||||
@current-change="loadData" @size-change="query.page=1;loadData()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="detailVisible" title="出库单详情" width="750px">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="出库单号">{{ detailRecord.recordNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户">{{ detailRecord.customerName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出库日期">{{ detailRecord.outDate }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">{{ typeMap[detailRecord.outType] }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总金额">¥{{ detailRecord.totalAmount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusMap[detailRecord.status]?.type" size="small">{{ statusMap[detailRecord.status]?.text }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<h4 style="margin:16px 0 8px">药品明细</h4>
|
||||
<el-table :data="detailItems" border size="small" max-height="300">
|
||||
<el-table-column prop="medicineName" label="药品" min-width="160" />
|
||||
<el-table-column prop="batchNo" label="批号" width="120" />
|
||||
<el-table-column prop="quantity" label="数量" width="80" />
|
||||
<el-table-column prop="unitPrice" label="单价" width="90"><template #default="{row}">¥{{row.unitPrice}}</template></el-table-column>
|
||||
<el-table-column prop="totalPrice" label="小计" width="100"><template #default="{row}">¥{{row.totalPrice}}</template></el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user