diff --git a/.gitignore b/.gitignore index f113dbf..1e46991 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ */.vscode/ */public/ -projectDesigning.md \ No newline at end of file +projectDesigning.md + +# 环境变量 +.env +.env.local +.env.*.local diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..7f32ee8 --- /dev/null +++ b/backend/.env.example @@ -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 diff --git a/backend/pom.xml b/backend/pom.xml index 68c2c2e..676ebdd 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -69,6 +69,12 @@ runtime + + org.mybatis + mybatis-spring + 4.1.0 + + cn.hutool hutool-all diff --git a/backend/src/main/java/com/kronecker/backend/BackendApplication.java b/backend/src/main/java/com/kronecker/backend/BackendApplication.java index b9e5357..7f01a79 100644 --- a/backend/src/main/java/com/kronecker/backend/BackendApplication.java +++ b/backend/src/main/java/com/kronecker/backend/BackendApplication.java @@ -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) { + } + } + } diff --git a/backend/src/main/java/com/kronecker/backend/common/BusinessException.java b/backend/src/main/java/com/kronecker/backend/common/BusinessException.java new file mode 100644 index 0000000..3518da8 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/common/BusinessException.java @@ -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(); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/common/GlobalExceptionHandler.java b/backend/src/main/java/com/kronecker/backend/common/GlobalExceptionHandler.java new file mode 100644 index 0000000..b36327d --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/common/GlobalExceptionHandler.java @@ -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 handleBusinessException(BusinessException e) { + log.warn("业务异常: {}", e.getMessage()); + return Result.error(e.getCode(), e.getMessage()); + } + + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result 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 handleAccessDeniedException(AccessDeniedException e) { + log.warn("权限不足: {}", e.getMessage()); + return Result.error(ResultCode.FORBIDDEN); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleException(Exception e) { + log.error("系统异常", e); + return Result.error(ResultCode.INTERNAL_ERROR); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/common/Result.java b/backend/src/main/java/com/kronecker/backend/common/Result.java new file mode 100644 index 0000000..88f8623 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/common/Result.java @@ -0,0 +1,37 @@ +package com.kronecker.backend.common; + +import lombok.Data; + +@Data +public class Result { + + private int code; + private String message; + private T data; + + public static Result success(T data) { + Result r = new Result<>(); + r.setCode(ResultCode.SUCCESS.getCode()); + r.setMessage(ResultCode.SUCCESS.getMessage()); + r.setData(data); + return r; + } + + public static Result success() { + return success(null); + } + + public static Result error(ResultCode code) { + Result r = new Result<>(); + r.setCode(code.getCode()); + r.setMessage(code.getMessage()); + return r; + } + + public static Result error(int code, String message) { + Result r = new Result<>(); + r.setCode(code); + r.setMessage(message); + return r; + } +} diff --git a/backend/src/main/java/com/kronecker/backend/common/ResultCode.java b/backend/src/main/java/com/kronecker/backend/common/ResultCode.java new file mode 100644 index 0000000..c213a02 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/common/ResultCode.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/config/CorsConfig.java b/backend/src/main/java/com/kronecker/backend/config/CorsConfig.java new file mode 100644 index 0000000..189ceed --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/config/CorsConfig.java @@ -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); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/config/MybatisPlusConfig.java b/backend/src/main/java/com/kronecker/backend/config/MybatisPlusConfig.java new file mode 100644 index 0000000..e741235 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/config/MybatisPlusConfig.java @@ -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(); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/config/SecurityConfig.java b/backend/src/main/java/com/kronecker/backend/config/SecurityConfig.java new file mode 100644 index 0000000..8dbfce4 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/config/SecurityConfig.java @@ -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(); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/controller/AuthController.java b/backend/src/main/java/com/kronecker/backend/controller/AuthController.java new file mode 100644 index 0000000..0c04977 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/controller/AuthController.java @@ -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> login(@Valid @RequestBody LoginDTO dto) { + return Result.success(authService.login(dto.getUsername(), dto.getPassword())); + } + + @PostMapping("/register") + public Result register(@Valid @RequestBody RegisterDTO dto) { + authService.register(dto); + return Result.success(); + } + + @PostMapping("/refresh") + public Result refresh(@RequestBody Map body) { + return Result.success(authService.refreshToken(body.get("refreshToken"))); + } + + @GetMapping("/userinfo") + public Result> userInfo(@AuthenticationPrincipal UserDetails user) { + return Result.success(authService.getUserInfo(Long.parseLong(user.getUsername()))); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/controller/CategoryController.java b/backend/src/main/java/com/kronecker/backend/controller/CategoryController.java new file mode 100644 index 0000000..da06e5c --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/controller/CategoryController.java @@ -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 categoryService; + + @GetMapping + public Result> list() { + return Result.success(categoryService.list( + new LambdaQueryWrapper().orderByAsc(MedicineCategory::getSortOrder))); + } + + @GetMapping("/tree") + public Result> tree() { + return Result.success(categoryService.list( + new LambdaQueryWrapper().orderByAsc(MedicineCategory::getSortOrder))); + } + + @PostMapping + public Result create(@RequestBody MedicineCategory category) { + categoryService.save(category); + return Result.success(); + } + + @PutMapping("/{id}") + public Result update(@PathVariable Long id, @RequestBody MedicineCategory category) { + category.setId(id); + categoryService.updateById(category); + return Result.success(); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + categoryService.removeById(id); + return Result.success(); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/controller/CustomerController.java b/backend/src/main/java/com/kronecker/backend/controller/CustomerController.java new file mode 100644 index 0000000..efec60b --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/controller/CustomerController.java @@ -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 customerService; + + @GetMapping + public Result> list(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String name) { + var wrapper = new LambdaQueryWrapper() + .like(StringUtils.hasText(name), Customer::getName, name) + .orderByDesc(Customer::getCreateTime); + return Result.success(customerService.page(new Page<>(page, size), wrapper)); + } + + @GetMapping("/all") + public Result> all() { + return Result.success(customerService.list()); + } + + @PostMapping + public Result create(@RequestBody Customer customer) { + customerService.save(customer); + return Result.success(); + } + + @PutMapping("/{id}") + public Result update(@PathVariable Long id, @RequestBody Customer customer) { + customer.setId(id); + customerService.updateById(customer); + return Result.success(); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + customerService.removeById(id); + return Result.success(); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/controller/DashboardController.java b/backend/src/main/java/com/kronecker/backend/controller/DashboardController.java new file mode 100644 index 0000000..ef38971 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/controller/DashboardController.java @@ -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> statistics() { + return Result.success(dashboardService.getStatistics()); + } + + @GetMapping("/expiry-warning") + public Result> expiryWarning(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + return Result.success(dashboardService.getExpiryWarning(new Page<>(page, size))); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/controller/MedicineController.java b/backend/src/main/java/com/kronecker/backend/controller/MedicineController.java new file mode 100644 index 0000000..37e586a --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/controller/MedicineController.java @@ -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> 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 getById(@PathVariable Long id) { + return Result.success(medicineService.getById(id)); + } + + @PostMapping + public Result create(@RequestBody Medicine medicine) { + medicineService.save(medicine); + return Result.success(); + } + + @PutMapping("/{id}") + public Result update(@PathVariable Long id, @RequestBody Medicine medicine) { + medicine.setId(id); + medicineService.updateById(medicine); + return Result.success(); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + medicineService.removeById(id); + return Result.success(); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/controller/MenuController.java b/backend/src/main/java/com/kronecker/backend/controller/MenuController.java new file mode 100644 index 0000000..4d11a3f --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/controller/MenuController.java @@ -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> tree(@AuthenticationPrincipal UserDetails user) { + return Result.success(menuService.getMenusByUserId(Long.parseLong(user.getUsername()))); + } + + @GetMapping + public Result> list() { + return Result.success(menuService.getTree()); + } + + @PostMapping + public Result create(@RequestBody Menu menu) { + menuService.save(menu); + return Result.success(); + } + + @PutMapping("/{id}") + public Result update(@PathVariable Long id, @RequestBody Menu menu) { + menu.setId(id); + menuService.updateById(menu); + return Result.success(); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + menuService.removeById(id); + return Result.success(); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/controller/RoleController.java b/backend/src/main/java/com/kronecker/backend/controller/RoleController.java new file mode 100644 index 0000000..e9cbed5 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/controller/RoleController.java @@ -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 roleService; + private final RoleMenuMapper roleMenuMapper; + + @GetMapping + public Result> list() { + return Result.success(roleService.list()); + } + + @GetMapping("/all") + public Result> all() { + return Result.success(roleService.list( + new LambdaQueryWrapper().eq(Role::getStatus, 1))); + } + + @PostMapping + public Result create(@RequestBody Role role) { + roleService.save(role); + return Result.success(); + } + + @PutMapping("/{id}") + public Result update(@PathVariable Long id, @RequestBody Role role) { + role.setId(id); + roleService.updateById(role); + return Result.success(); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + roleService.removeById(id); + return Result.success(); + } + + @PutMapping("/{id}/menus") + @Transactional + public Result assignMenus(@PathVariable Long id, @RequestBody Map> body) { + List menuIds = body.get("menuIds"); + roleMenuMapper.delete(new LambdaQueryWrapper().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(); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/controller/StockController.java b/backend/src/main/java/com/kronecker/backend/controller/StockController.java new file mode 100644 index 0000000..18adb16 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/controller/StockController.java @@ -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 stockService; + private final IService medicineService; + private final IService warehouseService; + + @GetMapping + public Result> 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() + .like(StringUtils.hasText(batchNo), Stock::getBatchNo, batchNo) + .orderByAsc(Stock::getExpiryDate); + // 按药品名搜索需先查 medicine 表 + if (StringUtils.hasText(medicineName)) { + var medIds = medicineService.list( + new LambdaQueryWrapper().like(Medicine::getName, medicineName) + ).stream().map(Medicine::getId).toList(); + if (medIds.isEmpty()) return Result.success(new Page<>()); + wrapper.in(Stock::getMedicineId, medIds); + } + Page result = stockService.page(new Page<>(page, size), wrapper); + populateNames(result.getRecords()); + return Result.success(result); + } + + @GetMapping("/warning") + public Result> warning(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + var wrapper = new LambdaQueryWrapper() + .gt(Stock::getExpiryDate, java.time.LocalDate.now()) + .orderByAsc(Stock::getExpiryDate); + Page result = stockService.page(new Page<>(page, size), wrapper); + populateNames(result.getRecords()); + return Result.success(result); + } + + private void populateNames(java.util.List 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 medMap = medicineService.listByIds(medIds).stream() + .collect(Collectors.toMap(Medicine::getId, Medicine::getName)); + Map 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(), "")); + }); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/controller/StockInController.java b/backend/src/main/java/com/kronecker/backend/controller/StockInController.java new file mode 100644 index 0000000..04a4347 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/controller/StockInController.java @@ -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 recordService; + private final IService detailService; + private final IService supplierService; + private final StockInService stockInService; + + @GetMapping + public Result> list(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + Page result = recordService.page(new Page<>(page, size), + new LambdaQueryWrapper().orderByDesc(StockInRecord::getCreateTime)); + populateSupplierNames(result.getRecords()); + return Result.success(result); + } + + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + StockInRecord record = recordService.getById(id); + if (record != null) { + record.setDetails(detailService.list( + new LambdaQueryWrapper().eq(StockInDetail::getRecordId, id))); + var supp = supplierService.getById(record.getSupplierId()); + if (supp != null) record.setSupplierName(supp.getName()); + } + return Result.success(record); + } + + @PostMapping + public Result create(@Valid @RequestBody StockInCreateDTO dto, + @AuthenticationPrincipal UserDetails user) { + return Result.success(stockInService.create(dto, Long.parseLong(user.getUsername()))); + } + + @PutMapping("/{id}/status") + public Result audit(@PathVariable Long id, @RequestBody Map body) { + if (body.get("status") == 1) { + stockInService.audit(id); + } + return Result.success(); + } + + private void populateSupplierNames(java.util.List records) { + if (records.isEmpty()) return; + var suppIds = records.stream().map(StockInRecord::getSupplierId).collect(Collectors.toSet()); + Map suppMap = supplierService.listByIds(suppIds).stream() + .collect(Collectors.toMap(Supplier::getId, Supplier::getName)); + records.forEach(r -> r.setSupplierName(suppMap.getOrDefault(r.getSupplierId(), ""))); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/controller/StockOutController.java b/backend/src/main/java/com/kronecker/backend/controller/StockOutController.java new file mode 100644 index 0000000..7d92edb --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/controller/StockOutController.java @@ -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 recordService; + private final IService detailService; + private final IService customerService; + private final StockOutService stockOutService; + + @GetMapping + public Result> list(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + Page result = recordService.page(new Page<>(page, size), + new LambdaQueryWrapper().orderByDesc(StockOutRecord::getCreateTime)); + populateCustomerNames(result.getRecords()); + return Result.success(result); + } + + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + StockOutRecord record = recordService.getById(id); + if (record != null) { + record.setDetails(detailService.list( + new LambdaQueryWrapper().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 create(@Valid @RequestBody StockOutCreateDTO dto, + @AuthenticationPrincipal UserDetails user) { + return Result.success(stockOutService.create(dto, Long.parseLong(user.getUsername()))); + } + + @PutMapping("/{id}/status") + public Result audit(@PathVariable Long id, @RequestBody Map body) { + if (body.get("status") == 1) { + stockOutService.audit(id); + } + return Result.success(); + } + + private void populateCustomerNames(java.util.List records) { + if (records.isEmpty()) return; + var custIds = records.stream().map(StockOutRecord::getCustomerId) + .filter(id -> id != null).collect(Collectors.toSet()); + if (custIds.isEmpty()) return; + Map 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(), "")); + }); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/controller/SupplierController.java b/backend/src/main/java/com/kronecker/backend/controller/SupplierController.java new file mode 100644 index 0000000..76f00e1 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/controller/SupplierController.java @@ -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 supplierService; + + @GetMapping + public Result> list(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String name) { + var wrapper = new LambdaQueryWrapper() + .like(StringUtils.hasText(name), Supplier::getName, name) + .orderByDesc(Supplier::getCreateTime); + return Result.success(supplierService.page(new Page<>(page, size), wrapper)); + } + + @GetMapping("/all") + public Result> all() { + return Result.success(supplierService.list( + new LambdaQueryWrapper().eq(Supplier::getStatus, 1))); + } + + @PostMapping + public Result create(@RequestBody Supplier supplier) { + supplierService.save(supplier); + return Result.success(); + } + + @PutMapping("/{id}") + public Result update(@PathVariable Long id, @RequestBody Supplier supplier) { + supplier.setId(id); + supplierService.updateById(supplier); + return Result.success(); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + supplierService.removeById(id); + return Result.success(); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/controller/UserController.java b/backend/src/main/java/com/kronecker/backend/controller/UserController.java new file mode 100644 index 0000000..6fc5360 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/controller/UserController.java @@ -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 userService; + private final PasswordEncoder passwordEncoder; + private final UserRoleMapper userRoleMapper; + + @GetMapping + public Result> list(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String username) { + var wrapper = new LambdaQueryWrapper() + .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 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 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 delete(@PathVariable Long id) { + userService.removeById(id); + return Result.success(); + } + + @PutMapping("/{id}/roles") + @Transactional + public Result assignRoles(@PathVariable Long id, @RequestBody Map> body) { + List roleIds = body.get("roleIds"); + userRoleMapper.delete(new LambdaQueryWrapper().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(); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/dto/LoginDTO.java b/backend/src/main/java/com/kronecker/backend/dto/LoginDTO.java new file mode 100644 index 0000000..3b57738 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/dto/LoginDTO.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/dto/PageQueryDTO.java b/backend/src/main/java/com/kronecker/backend/dto/PageQueryDTO.java new file mode 100644 index 0000000..61ca8cf --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/dto/PageQueryDTO.java @@ -0,0 +1,9 @@ +package com.kronecker.backend.dto; + +import lombok.Data; + +@Data +public class PageQueryDTO { + private Integer page = 1; + private Integer size = 10; +} diff --git a/backend/src/main/java/com/kronecker/backend/dto/RegisterDTO.java b/backend/src/main/java/com/kronecker/backend/dto/RegisterDTO.java new file mode 100644 index 0000000..a20696d --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/dto/RegisterDTO.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/dto/StockInCreateDTO.java b/backend/src/main/java/com/kronecker/backend/dto/StockInCreateDTO.java new file mode 100644 index 0000000..4cdb6cb --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/dto/StockInCreateDTO.java @@ -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 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; + } +} diff --git a/backend/src/main/java/com/kronecker/backend/dto/StockOutCreateDTO.java b/backend/src/main/java/com/kronecker/backend/dto/StockOutCreateDTO.java new file mode 100644 index 0000000..79fd357 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/dto/StockOutCreateDTO.java @@ -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 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; + } +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/Customer.java b/backend/src/main/java/com/kronecker/backend/entity/Customer.java new file mode 100644 index 0000000..78a3994 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/Customer.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/Medicine.java b/backend/src/main/java/com/kronecker/backend/entity/Medicine.java new file mode 100644 index 0000000..eaf2b25 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/Medicine.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/MedicineCategory.java b/backend/src/main/java/com/kronecker/backend/entity/MedicineCategory.java new file mode 100644 index 0000000..56bf400 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/MedicineCategory.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/Menu.java b/backend/src/main/java/com/kronecker/backend/entity/Menu.java new file mode 100644 index 0000000..a8f4a67 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/Menu.java @@ -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 children; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/Role.java b/backend/src/main/java/com/kronecker/backend/entity/Role.java new file mode 100644 index 0000000..72d1eb7 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/Role.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/RoleMenu.java b/backend/src/main/java/com/kronecker/backend/entity/RoleMenu.java new file mode 100644 index 0000000..cb8a7ed --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/RoleMenu.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/Stock.java b/backend/src/main/java/com/kronecker/backend/entity/Stock.java new file mode 100644 index 0000000..80f8435 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/Stock.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/StockInDetail.java b/backend/src/main/java/com/kronecker/backend/entity/StockInDetail.java new file mode 100644 index 0000000..9b41fde --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/StockInDetail.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/StockInRecord.java b/backend/src/main/java/com/kronecker/backend/entity/StockInRecord.java new file mode 100644 index 0000000..09e2e5a --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/StockInRecord.java @@ -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 details; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/StockOutDetail.java b/backend/src/main/java/com/kronecker/backend/entity/StockOutDetail.java new file mode 100644 index 0000000..2900f4c --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/StockOutDetail.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/StockOutRecord.java b/backend/src/main/java/com/kronecker/backend/entity/StockOutRecord.java new file mode 100644 index 0000000..37e06e3 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/StockOutRecord.java @@ -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 details; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/Supplier.java b/backend/src/main/java/com/kronecker/backend/entity/Supplier.java new file mode 100644 index 0000000..b5d465b --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/Supplier.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/User.java b/backend/src/main/java/com/kronecker/backend/entity/User.java new file mode 100644 index 0000000..1cfe56c --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/User.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/UserRole.java b/backend/src/main/java/com/kronecker/backend/entity/UserRole.java new file mode 100644 index 0000000..6967a8f --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/UserRole.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/entity/Warehouse.java b/backend/src/main/java/com/kronecker/backend/entity/Warehouse.java new file mode 100644 index 0000000..48f9725 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/entity/Warehouse.java @@ -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; +} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/CustomerMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/CustomerMapper.java new file mode 100644 index 0000000..41a2056 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/CustomerMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/MedicineCategoryMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/MedicineCategoryMapper.java new file mode 100644 index 0000000..c54746a --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/MedicineCategoryMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/MedicineMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/MedicineMapper.java new file mode 100644 index 0000000..0da1fae --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/MedicineMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/MenuMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/MenuMapper.java new file mode 100644 index 0000000..4191f05 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/MenuMapper.java @@ -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 { + + @Select("") + List selectPermsByRoleIds(@Param("roleIds") List roleIds); +} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/RoleMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/RoleMapper.java new file mode 100644 index 0000000..ddabad8 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/RoleMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/RoleMenuMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/RoleMenuMapper.java new file mode 100644 index 0000000..b0f7695 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/RoleMenuMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/StockInDetailMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/StockInDetailMapper.java new file mode 100644 index 0000000..ad2b083 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/StockInDetailMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/StockInRecordMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/StockInRecordMapper.java new file mode 100644 index 0000000..4ff6aea --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/StockInRecordMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/StockMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/StockMapper.java new file mode 100644 index 0000000..80ce07b --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/StockMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/StockOutDetailMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/StockOutDetailMapper.java new file mode 100644 index 0000000..220cb4b --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/StockOutDetailMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/StockOutRecordMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/StockOutRecordMapper.java new file mode 100644 index 0000000..81ae432 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/StockOutRecordMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/SupplierMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/SupplierMapper.java new file mode 100644 index 0000000..cd62283 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/SupplierMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/UserMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/UserMapper.java new file mode 100644 index 0000000..f391fc2 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/UserMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/UserRoleMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/UserRoleMapper.java new file mode 100644 index 0000000..ce5cc5c --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/UserRoleMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/mapper/WarehouseMapper.java b/backend/src/main/java/com/kronecker/backend/mapper/WarehouseMapper.java new file mode 100644 index 0000000..344806c --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/mapper/WarehouseMapper.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/security/JwtAuthenticationFilter.java b/backend/src/main/java/com/kronecker/backend/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..e2e0744 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/security/JwtAuthenticationFilter.java @@ -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; + } +} diff --git a/backend/src/main/java/com/kronecker/backend/security/JwtTokenProvider.java b/backend/src/main/java/com/kronecker/backend/security/JwtTokenProvider.java new file mode 100644 index 0000000..56913e2 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/security/JwtTokenProvider.java @@ -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(); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/security/UserDetailsServiceImpl.java b/backend/src/main/java/com/kronecker/backend/security/UserDetailsServiceImpl.java new file mode 100644 index 0000000..b68d3b5 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/security/UserDetailsServiceImpl.java @@ -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 roleIds = userRoleMapper.selectList( + new LambdaQueryWrapper().eq(UserRole::getUserId, user.getId()) + ).stream().map(UserRole::getRoleId).toList(); + + List permissions = new ArrayList<>(); + if (!roleIds.isEmpty()) { + permissions = menuMapper.selectPermsByRoleIds(roleIds); + } + + List 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); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/service/AuthService.java b/backend/src/main/java/com/kronecker/backend/service/AuthService.java new file mode 100644 index 0000000..1477cf9 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/AuthService.java @@ -0,0 +1,12 @@ +package com.kronecker.backend.service; + +import com.kronecker.backend.dto.RegisterDTO; + +import java.util.Map; + +public interface AuthService { + Map login(String username, String password); + String refreshToken(String refreshToken); + Map getUserInfo(Long userId); + void register(RegisterDTO dto); +} diff --git a/backend/src/main/java/com/kronecker/backend/service/DashboardService.java b/backend/src/main/java/com/kronecker/backend/service/DashboardService.java new file mode 100644 index 0000000..877a172 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/DashboardService.java @@ -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 getStatistics(); + Page getExpiryWarning(Page page); + Page getRecentStockIn(Page page); + Page getRecentStockOut(Page page); +} diff --git a/backend/src/main/java/com/kronecker/backend/service/MedicineService.java b/backend/src/main/java/com/kronecker/backend/service/MedicineService.java new file mode 100644 index 0000000..c5ada38 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/MedicineService.java @@ -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 { + Page pageQuery(Page page, String name, Long categoryId); +} diff --git a/backend/src/main/java/com/kronecker/backend/service/MenuService.java b/backend/src/main/java/com/kronecker/backend/service/MenuService.java new file mode 100644 index 0000000..3c3dadf --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/MenuService.java @@ -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 { + List getTree(); + List getMenusByUserId(Long userId); +} diff --git a/backend/src/main/java/com/kronecker/backend/service/StockInService.java b/backend/src/main/java/com/kronecker/backend/service/StockInService.java new file mode 100644 index 0000000..1a5e87d --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/StockInService.java @@ -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); +} diff --git a/backend/src/main/java/com/kronecker/backend/service/StockOutService.java b/backend/src/main/java/com/kronecker/backend/service/StockOutService.java new file mode 100644 index 0000000..6d96b52 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/StockOutService.java @@ -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); +} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/AuthServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..72d7ca0 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/AuthServiceImpl.java @@ -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 login(String username, String password) { + User user = userMapper.selectOne(new LambdaQueryWrapper().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 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 getUserInfo(Long userId) { + User user = userMapper.selectById(userId); + return buildUserInfo(user); + } + + @Override + @Transactional + public void register(RegisterDTO dto) { + if (userMapper.selectCount(new LambdaQueryWrapper().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 buildUserInfo(User user) { + Map 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; + } +} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/CategoryServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/CategoryServiceImpl.java new file mode 100644 index 0000000..ef5fcf9 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/CategoryServiceImpl.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/CustomerServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/CustomerServiceImpl.java new file mode 100644 index 0000000..5804635 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/CustomerServiceImpl.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/DashboardServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/DashboardServiceImpl.java new file mode 100644 index 0000000..4ccf57a --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/DashboardServiceImpl.java @@ -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 getStatistics() { + Map stats = new HashMap<>(); + stats.put("medicineCount", medicineMapper.selectCount(null)); + stats.put("stockBatchCount", stockMapper.selectCount(null)); + stats.put("pendingStockIn", stockInRecordMapper.selectCount( + new LambdaQueryWrapper().eq(StockInRecord::getStatus, 0))); + stats.put("expiryWarningCount", stockMapper.selectCount( + new LambdaQueryWrapper().le(Stock::getExpiryDate, LocalDate.now().plusDays(90)))); + return stats; + } + + @Override + public Page getExpiryWarning(Page page) { + Page result = stockMapper.selectPage(page, + new LambdaQueryWrapper() + .le(Stock::getExpiryDate, LocalDate.now().plusDays(90)) + .orderByAsc(Stock::getExpiryDate)); + populateStockNames(result.getRecords()); + return result; + } + + @Override + public Page getRecentStockIn(Page page) { + Page result = stockInRecordMapper.selectPage(page, + new LambdaQueryWrapper().orderByDesc(StockInRecord::getCreateTime)); + populateInRecordNames(result.getRecords()); + return result; + } + + @Override + public Page getRecentStockOut(Page page) { + Page result = stockOutRecordMapper.selectPage(page, + new LambdaQueryWrapper().orderByDesc(StockOutRecord::getCreateTime)); + populateOutRecordNames(result.getRecords()); + return result; + } + + private void populateStockNames(java.util.List 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 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 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(), "")); }); + } + } +} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/MedicineServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/MedicineServiceImpl.java new file mode 100644 index 0000000..6b72249 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/MedicineServiceImpl.java @@ -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 implements MedicineService { + + @Override + public Page pageQuery(Page page, String name, Long categoryId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .like(StringUtils.hasText(name), Medicine::getName, name) + .eq(categoryId != null, Medicine::getCategoryId, categoryId) + .orderByDesc(Medicine::getCreateTime); + return baseMapper.selectPage(page, wrapper); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/MenuServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/MenuServiceImpl.java new file mode 100644 index 0000000..15f34f1 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/MenuServiceImpl.java @@ -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 implements MenuService { + + private final UserRoleMapper userRoleMapper; + private final RoleMenuMapper roleMenuMapper; + private final MenuMapper menuMapper; + + @Override + public List getTree() { + List all = menuMapper.selectList( + new LambdaQueryWrapper().orderByAsc(Menu::getSortOrder)); + return buildTree(all, 0L); + } + + @Override + public List getMenusByUserId(Long userId) { + List roleIds = userRoleMapper.selectList( + new LambdaQueryWrapper().eq(UserRole::getUserId, userId) + ).stream().map(UserRole::getRoleId).toList(); + if (roleIds.isEmpty()) return Collections.emptyList(); + + Set menuIds = roleMenuMapper.selectList( + new LambdaQueryWrapper().in(RoleMenu::getRoleId, roleIds) + ).stream().map(RoleMenu::getMenuId).collect(Collectors.toSet()); + if (menuIds.isEmpty()) return Collections.emptyList(); + + List menus = menuMapper.selectList( + new LambdaQueryWrapper() + .in(Menu::getId, menuIds) + .orderByAsc(Menu::getSortOrder)); + return buildTree(menus, 0L); + } + + private List buildTree(List list, Long parentId) { + return list.stream() + .filter(m -> m.getParentId().equals(parentId)) + .peek(m -> m.setChildren(buildTree(list, m.getId()))) + .collect(Collectors.toList()); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/RoleServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/RoleServiceImpl.java new file mode 100644 index 0000000..e233e97 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/RoleServiceImpl.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/StockInDetailServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/StockInDetailServiceImpl.java new file mode 100644 index 0000000..161b917 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/StockInDetailServiceImpl.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/StockInRecordServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/StockInRecordServiceImpl.java new file mode 100644 index 0000000..df3e0d6 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/StockInRecordServiceImpl.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/StockInServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/StockInServiceImpl.java new file mode 100644 index 0000000..51b9dbd --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/StockInServiceImpl.java @@ -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() + .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().eq(StockInDetail::getRecordId, id)); + for (var d : details) { + // 查找同批号库存 + var stock = stockMapper.selectOne(new LambdaQueryWrapper() + .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); + } + } + } +} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/StockOutDetailServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/StockOutDetailServiceImpl.java new file mode 100644 index 0000000..e4a5531 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/StockOutDetailServiceImpl.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/StockOutRecordServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/StockOutRecordServiceImpl.java new file mode 100644 index 0000000..3a0f7a2 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/StockOutRecordServiceImpl.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/StockOutServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/StockOutServiceImpl.java new file mode 100644 index 0000000..7399b41 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/StockOutServiceImpl.java @@ -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() + .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().eq(StockOutDetail::getRecordId, id)); + + // 检查库存是否充足 + for (var d : details) { + int totalStock = stockMapper.selectList(new LambdaQueryWrapper() + .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() + .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); + } +} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/StockServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/StockServiceImpl.java new file mode 100644 index 0000000..b87ad30 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/StockServiceImpl.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/SupplierServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/SupplierServiceImpl.java new file mode 100644 index 0000000..3ebb16d --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/SupplierServiceImpl.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/UserRoleServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/UserRoleServiceImpl.java new file mode 100644 index 0000000..a788d32 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/UserRoleServiceImpl.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/UserServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..7f79268 --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/UserServiceImpl.java @@ -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 {} diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/WarehouseServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/WarehouseServiceImpl.java new file mode 100644 index 0000000..012341b --- /dev/null +++ b/backend/src/main/java/com/kronecker/backend/service/impl/WarehouseServiceImpl.java @@ -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 {} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..c425553 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -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} diff --git a/backend/src/main/resources/database/init.sql b/backend/src/main/resources/database/init.sql index 892f71e..c57bd22 100644 --- a/backend/src/main/resources/database/init.sql +++ b/backend/src/main/resources/database/init.sql @@ -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); -- ----------------------------------------------------------- -- 用户角色关联 diff --git a/backend/src/test/java/com/kronecker/backend/BackendApplicationTests.java b/backend/src/test/java/com/kronecker/backend/BackendApplicationTests.java index dc6ba45..79dc886 100644 --- a/backend/src/test/java/com/kronecker/backend/BackendApplicationTests.java +++ b/backend/src/test/java/com/kronecker/backend/BackendApplicationTests.java @@ -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() { } diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..0e00f14 --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1,2 @@ +# 开发环境(Vite dev 模式下自动加载) +VITE_API_BASE_URL=http://localhost:8080 diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..f9d0c0b --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,6 @@ +# ============================================================ +# 前端环境变量(Vite 原生支持,复制为 .env 即可生效) +# ============================================================ + +# API 代理目标地址 +VITE_API_BASE_URL=http://localhost:8080 diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js index d4b00be..5008ecf 100644 --- a/frontend/src/api/auth.js +++ b/frontend/src/api/auth.js @@ -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) } diff --git a/frontend/src/layout/MainLayout.vue b/frontend/src/layout/MainLayout.vue index 8dec66b..2f2b622 100644 --- a/frontend/src/layout/MainLayout.vue +++ b/frontend/src/layout/MainLayout.vue @@ -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 +})