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 +}) @@ -48,27 +66,29 @@ const menuItems = computed(() => renderMenu(userStore.menus)) - 💊 药品管理系统 - 💊 + 药品管理系统 + { if (index) router.push(index) }" background-color="#304156" text-color="#bfcbd9" active-text-color="#409eff" > - + {{ item.title }} - + {{ child.title }} import('@/views/login/LoginView.vue'), meta: { title: '登录', noAuth: true }, }, + { + path: '/register', + name: 'Register', + component: () => import('@/views/login/RegisterView.vue'), + meta: { title: '注册', noAuth: true }, + }, { path: '/', name: 'MainLayout', @@ -48,6 +54,7 @@ export const componentMap = { 'views/stockOut/StockOutForm': () => import('@/views/stockOut/StockOutForm.vue'), 'views/user/UserList': () => import('@/views/user/UserList.vue'), 'views/role/RoleList': () => import('@/views/role/RoleList.vue'), + 'views/menu/MenuList': () => import('@/views/menu/MenuList.vue'), } // 根据后端菜单数据生成路由 @@ -79,13 +86,12 @@ router.beforeEach(async (to, _from, next) => { return } - // TODO: 暂时跳过登录验证(开发阶段) - // if (!userStore.token) { - // next('/login') - // return - // } + if (!userStore.token) { + next('/login') + return + } - // 动态注册菜单路由(仅首次),开发阶段使用本地 mock 菜单 + // 动态注册菜单路由(仅首次) if (!isMenusLoaded) { try { const menus = await userStore.getMenus() diff --git a/frontend/src/stores/user.js b/frontend/src/stores/user.js index cd59c9b..eaecb97 100644 --- a/frontend/src/stores/user.js +++ b/frontend/src/stores/user.js @@ -25,9 +25,9 @@ const mockMenus = [ ] export const useUserStore = defineStore('user', () => { - const token = ref(localStorage.getItem('token') || 'dev-token') // TODO: 开发阶段默认有 token - const refreshToken = ref(localStorage.getItem('refreshToken') || 'dev-refresh') - const userInfo = ref({ username: 'admin', realName: '管理员' }) // TODO: 开发阶段 mock + const token = ref(localStorage.getItem('token') || '') + const refreshToken = ref(localStorage.getItem('refreshToken') || '') + const userInfo = ref({}) const menus = ref([]) const permissions = ref([]) @@ -35,7 +35,8 @@ export const useUserStore = defineStore('user', () => { const perms = [] function walk(list) { list.forEach((item) => { - if (item.permission_code) perms.push(item.permission_code) + const code = item.permissionCode || item.permission_code + if (code) perms.push(code) if (item.children?.length) walk(item.children) }) } diff --git a/frontend/src/views/dashboard/DashboardView.vue b/frontend/src/views/dashboard/DashboardView.vue index 7da9ef0..277e7db 100644 --- a/frontend/src/views/dashboard/DashboardView.vue +++ b/frontend/src/views/dashboard/DashboardView.vue @@ -4,6 +4,7 @@ import { getStatistics, getExpiryWarning } from '@/api/dashboard' import { getStockInList } from '@/api/stockIn' import { getStockOutList } from '@/api/stockOut' import { Goods, Box, DocumentChecked, Warning } from '@element-plus/icons-vue' +import { ElMessage } from 'element-plus' const statistics = ref({ medicineCount: 0, stockBatchCount: 0, pendingStockIn: 0, expiryWarningCount: 0 }) const expiryList = ref([]) @@ -18,31 +19,30 @@ const statCards = [ ] function expiryColor(date) { + if (!date) return '' const days = Math.ceil((new Date(date) - new Date()) / (1000 * 60 * 60 * 24)) if (days < 30) return 'danger' if (days < 90) return 'warning' - return '' + return 'success' } onMounted(async () => { try { - const [statRes, warnRes, inRes, outRes] = await Promise.all([ - getStatistics(), - getExpiryWarning(), + const [statRes, warnRes, inRes, outRes] = await Promise.allSettled([ + getStatistics(), getExpiryWarning(), getStockInList({ page: 1, size: 5 }), getStockOutList({ page: 1, size: 5 }), ]) - statistics.value = statRes.data - expiryList.value = warnRes.data?.records || warnRes.data || [] - recentInList.value = inRes.data?.records || [] - recentOutList.value = outRes.data?.records || [] - } catch { /* 后端未启动,静默处理 */ } + if (statRes.status === 'fulfilled') statistics.value = statRes.value.data || {} + if (warnRes.status === 'fulfilled') expiryList.value = warnRes.value.data?.records || warnRes.value.data || [] + if (inRes.status === 'fulfilled') recentInList.value = inRes.value.data?.records || [] + if (outRes.status === 'fulfilled') recentOutList.value = outRes.value.data?.records || [] + } catch { /* 后端未启动 */ } }) - @@ -55,31 +55,27 @@ onMounted(async () => { - - - - - + + + + - + - - {{ row.expiryDate || row.expiry_date }} - + {{ row.expiryDate }} - - + - {{ item.recordNo || item.record_no }} - {{ item.supplierName || '供应商' }} - - {{ item.status === 1 ? '已入库' : item.status === 0 ? '待审核' : '已取消' }} + {{ item.recordNo }} + {{ item.supplierName }} + + {{ item.status===1?'已入库':item.status===0?'待审核':'已取消' }} @@ -88,10 +84,10 @@ onMounted(async () => { - {{ item.recordNo || item.record_no }} - {{ item.customerName || '客户' }} - - {{ item.status === 1 ? '已出库' : item.status === 0 ? '待审核' : '已取消' }} + {{ item.recordNo }} + {{ item.customerName || '-' }} + + {{ item.status===1?'已出库':item.status===0?'待审核':'已取消' }} @@ -102,19 +98,10 @@ onMounted(async () => { diff --git a/frontend/src/views/login/LoginView.vue b/frontend/src/views/login/LoginView.vue index c90e492..2821462 100644 --- a/frontend/src/views/login/LoginView.vue +++ b/frontend/src/views/login/LoginView.vue @@ -70,15 +70,13 @@ async function handleLogin() { 记住我 - + {{ loading ? '登录中...' : '登 录' }} + + 没有账号?去注册 + diff --git a/frontend/src/views/login/RegisterView.vue b/frontend/src/views/login/RegisterView.vue new file mode 100644 index 0000000..7075c56 --- /dev/null +++ b/frontend/src/views/login/RegisterView.vue @@ -0,0 +1,117 @@ + + + + + + 注册账号 + + + + + + + + + + + + + + + + + + + + + + {{ loading ? '注册中...' : '注 册' }} + + + + 已有账号?去登录 + + + + + + + diff --git a/frontend/src/views/menu/MenuList.vue b/frontend/src/views/menu/MenuList.vue new file mode 100644 index 0000000..e3afbe6 --- /dev/null +++ b/frontend/src/views/menu/MenuList.vue @@ -0,0 +1,153 @@ + + + + + + 新增顶级菜单 + + + + + + + + {{ { 1: '目录', 2: '菜单', 3: '按钮' }[row.type] }} + + + + + + + + + + {{ row.visible === 1 ? '是' : '否' }} + + + + + 编辑 + 添加子级 + 删除 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 取消确定 + + + diff --git a/frontend/src/views/stockIn/StockInList.vue b/frontend/src/views/stockIn/StockInList.vue index 28afdcd..bdac93f 100644 --- a/frontend/src/views/stockIn/StockInList.vue +++ b/frontend/src/views/stockIn/StockInList.vue @@ -1,26 +1,53 @@ @@ -28,30 +55,72 @@ onMounted(loadData) - + 搜索 - query[k]=k==='page'?1:k==='size'?10:'');loadData()">重置 + 重置 - + 新建入库单 + 新建入库单 - - + + + - ¥{{row.totalAmount}} + + ¥{{ row.totalAmount }} + - {{statusText(row.status)}} + + + {{ statusMap[row.status]?.text || '未知' }} + + - - + + + 查看 - 审核 + + 通过 + 驳回 + - + + + + + + + + + {{ detailRecord.recordNo }} + {{ detailRecord.supplierName }} + {{ detailRecord.inDate }} + ¥{{ detailRecord.totalAmount }} + + {{ statusMap[detailRecord.status]?.text }} + + {{ detailRecord.remark || '-' }} + + 药品明细 + + + + + ¥{{row.costPrice}} + ¥{{row.totalPrice}} + + + diff --git a/frontend/src/views/stockOut/StockOutList.vue b/frontend/src/views/stockOut/StockOutList.vue index 7e73f0e..09ff0c7 100644 --- a/frontend/src/views/stockOut/StockOutList.vue +++ b/frontend/src/views/stockOut/StockOutList.vue @@ -1,23 +1,54 @@ @@ -25,31 +56,75 @@ onMounted(loadData) - + 搜索 - query[k]=k==='page'?1:k==='size'?10:'');loadData()">重置 + 重置 - + 新建出库单 + 新建出库单 - - - - {{typeText(row.outType)}} - ¥{{row.totalAmount}} + + + + + {{ row.customerName || '-' }} + + + {{ typeMap[row.outType] || '未知' }} + + + ¥{{ row.totalAmount }} + - {{statusText(row.status)}} + + + {{ statusMap[row.status]?.text || '未知' }} + + - - + + + 查看 - 审核 + + 通过 + 驳回 + - + + + + + + + + {{ detailRecord.recordNo }} + {{ detailRecord.customerName || '-' }} + {{ detailRecord.outDate }} + {{ typeMap[detailRecord.outType] }} + ¥{{ detailRecord.totalAmount }} + + {{ statusMap[detailRecord.status]?.text }} + + + 药品明细 + + + + + ¥{{row.unitPrice}} + ¥{{row.totalPrice}} + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 1445a6c..85f8817 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,22 +1,25 @@ -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' import { fileURLToPath, URL } from 'node:url' // https://vite.dev/config/ -export default defineConfig({ - plugins: [vue()], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - }, - server: { - port: 5173, - proxy: { - '/api': { - target: 'http://localhost:8080', - changeOrigin: true, +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + return { + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, - }, + server: { + port: 5173, + proxy: { + '/api': { + target: env.VITE_API_BASE_URL || 'http://localhost:8080', + changeOrigin: true, + }, + }, + }, + } }) diff --git a/readme..md b/readme..md index e69de29..0f35242 100644 --- a/readme..md +++ b/readme..md @@ -0,0 +1,27 @@ +# 医药管理系统 + +## 一、概述 + +### (一)项目概述 + +#### 1.技术栈: + +| 类别 | 技术栈 | 版本 | +| ---------- | ------------------------------ | ----------- | +| 前端 | Vue 3 + Vite | 3.5+ / 8.1+ | +| UI 与路由 | Element Plus + Vue Router | 2.x / 4.x | +| 状态与请求 | Pinia + Axios | 2.x / 1.x | +| 样式与图标 | Sass + @element-plus/icons-vue | 1.x / — | +| 后端框架 | Spring Boot (含 Validation) | 4.1.0 | +| 开发语言 | JDK | 21 | +| 持久层 | MyBatis-Plus + MySQL | 3.5+ / 8.0+ | +| 安全与认证 | Spring Security + JWT (jjwt) | 6.x / 0.12+ | +| 工具库 | Hutool | 5.x | +| 项目构建 | Maven | 3.9+ | + +## 二、前端 + +## 三、后端 + +## 四、快速开始 +