添加后端基础架构和API功能

- 配置.env文件支持环境变量管理
- 集成MyBatis Plus ORM框架并配置数据库连接
- 实现JWT认证和Spring Security安全配置
- 添加全局异常处理机制和统一响应结果封装
- 配置CORS跨域资源共享支持
- 实现用户认证API包括登录、注册、刷新token功能
- 开发完整的药品管理系统包含分类、药品、库存管理
- 实现出入库管理功能包括供应商、客户管理
- 添加角色权限管理和菜单配置
- 实现仪表盘统计和过期预警功能
- 配置分页查询和数据验证功能
```
This commit is contained in:
2026-07-06 20:04:44 +08:00
parent 15da4fe492
commit 258bae75b2
102 changed files with 3054 additions and 128 deletions

19
backend/.env.example Normal file
View File

@@ -0,0 +1,19 @@
# ============================================================
# 后端环境变量IDEA 可通过 EnvFile 插件加载)
# 或通过命令行: export $(cat .env | xargs) && ./mvnw spring-boot:run
# ============================================================
# 数据库
DB_HOST=localhost
DB_PORT=3306
DB_NAME=medicine_manager
DB_USERNAME=root
DB_PASSWORD=your_password_here
# JWT
JWT_SECRET=change_me_to_a_random_base64_string_at_least_256_bits
JWT_EXPIRATION=86400000
JWT_REFRESH_EXPIRATION=604800000
# 服务端口
SERVER_PORT=8080

View File

@@ -69,6 +69,12 @@
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
package com.kronecker.backend.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleBindException(BindException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(err -> err.getField() + ": " + err.getDefaultMessage())
.reduce((a, b) -> a + "; " + b).orElse("参数校验失败");
log.warn("参数校验失败: {}", msg);
return Result.error(ResultCode.BAD_REQUEST.getCode(), msg);
}
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public Result<Void> handleAccessDeniedException(AccessDeniedException e) {
log.warn("权限不足: {}", e.getMessage());
return Result.error(ResultCode.FORBIDDEN);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.error(ResultCode.INTERNAL_ERROR);
}
}

View File

@@ -0,0 +1,37 @@
package com.kronecker.backend.common;
import lombok.Data;
@Data
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.setCode(ResultCode.SUCCESS.getCode());
r.setMessage(ResultCode.SUCCESS.getMessage());
r.setData(data);
return r;
}
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> error(ResultCode code) {
Result<T> r = new Result<>();
r.setCode(code.getCode());
r.setMessage(code.getMessage());
return r;
}
public static <T> Result<T> error(int code, String message) {
Result<T> r = new Result<>();
r.setCode(code);
r.setMessage(message);
return r;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
package com.kronecker.backend.controller;
import com.kronecker.backend.common.Result;
import com.kronecker.backend.dto.LoginDTO;
import com.kronecker.backend.dto.RegisterDTO;
import com.kronecker.backend.security.JwtTokenProvider;
import com.kronecker.backend.service.AuthService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
private final JwtTokenProvider jwtTokenProvider;
@PostMapping("/login")
public Result<Map<String, Object>> login(@Valid @RequestBody LoginDTO dto) {
return Result.success(authService.login(dto.getUsername(), dto.getPassword()));
}
@PostMapping("/register")
public Result<Void> register(@Valid @RequestBody RegisterDTO dto) {
authService.register(dto);
return Result.success();
}
@PostMapping("/refresh")
public Result<String> refresh(@RequestBody Map<String, String> body) {
return Result.success(authService.refreshToken(body.get("refreshToken")));
}
@GetMapping("/userinfo")
public Result<Map<String, Object>> userInfo(@AuthenticationPrincipal UserDetails user) {
return Result.success(authService.getUserInfo(Long.parseLong(user.getUsername())));
}
}

View File

@@ -0,0 +1,49 @@
package com.kronecker.backend.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kronecker.backend.common.Result;
import com.kronecker.backend.entity.MedicineCategory;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/categories")
@RequiredArgsConstructor
public class CategoryController {
private final IService<MedicineCategory> categoryService;
@GetMapping
public Result<List<MedicineCategory>> list() {
return Result.success(categoryService.list(
new LambdaQueryWrapper<MedicineCategory>().orderByAsc(MedicineCategory::getSortOrder)));
}
@GetMapping("/tree")
public Result<List<MedicineCategory>> tree() {
return Result.success(categoryService.list(
new LambdaQueryWrapper<MedicineCategory>().orderByAsc(MedicineCategory::getSortOrder)));
}
@PostMapping
public Result<Void> create(@RequestBody MedicineCategory category) {
categoryService.save(category);
return Result.success();
}
@PutMapping("/{id}")
public Result<Void> update(@PathVariable Long id, @RequestBody MedicineCategory category) {
category.setId(id);
categoryService.updateById(category);
return Result.success();
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
categoryService.removeById(id);
return Result.success();
}
}

View File

@@ -0,0 +1,54 @@
package com.kronecker.backend.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kronecker.backend.common.Result;
import com.kronecker.backend.entity.Customer;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/customers")
@RequiredArgsConstructor
public class CustomerController {
private final IService<Customer> customerService;
@GetMapping
public Result<Page<Customer>> list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String name) {
var wrapper = new LambdaQueryWrapper<Customer>()
.like(StringUtils.hasText(name), Customer::getName, name)
.orderByDesc(Customer::getCreateTime);
return Result.success(customerService.page(new Page<>(page, size), wrapper));
}
@GetMapping("/all")
public Result<List<Customer>> all() {
return Result.success(customerService.list());
}
@PostMapping
public Result<Void> create(@RequestBody Customer customer) {
customerService.save(customer);
return Result.success();
}
@PutMapping("/{id}")
public Result<Void> update(@PathVariable Long id, @RequestBody Customer customer) {
customer.setId(id);
customerService.updateById(customer);
return Result.success();
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
customerService.removeById(id);
return Result.success();
}
}

View File

@@ -0,0 +1,31 @@
package com.kronecker.backend.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.kronecker.backend.common.Result;
import com.kronecker.backend.entity.Stock;
import com.kronecker.backend.entity.StockInRecord;
import com.kronecker.backend.entity.StockOutRecord;
import com.kronecker.backend.service.DashboardService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/dashboard")
@RequiredArgsConstructor
public class DashboardController {
private final DashboardService dashboardService;
@GetMapping("/statistics")
public Result<Map<String, Object>> statistics() {
return Result.success(dashboardService.getStatistics());
}
@GetMapping("/expiry-warning")
public Result<Page<Stock>> expiryWarning(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return Result.success(dashboardService.getExpiryWarning(new Page<>(page, size)));
}
}

View File

@@ -0,0 +1,48 @@
package com.kronecker.backend.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.kronecker.backend.common.Result;
import com.kronecker.backend.entity.Medicine;
import com.kronecker.backend.service.MedicineService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/medicines")
@RequiredArgsConstructor
public class MedicineController {
private final MedicineService medicineService;
@GetMapping
public Result<Page<Medicine>> list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String name,
@RequestParam(required = false) Long categoryId) {
return Result.success(medicineService.pageQuery(new Page<>(page, size), name, categoryId));
}
@GetMapping("/{id}")
public Result<Medicine> getById(@PathVariable Long id) {
return Result.success(medicineService.getById(id));
}
@PostMapping
public Result<Void> create(@RequestBody Medicine medicine) {
medicineService.save(medicine);
return Result.success();
}
@PutMapping("/{id}")
public Result<Void> update(@PathVariable Long id, @RequestBody Medicine medicine) {
medicine.setId(id);
medicineService.updateById(medicine);
return Result.success();
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
medicineService.removeById(id);
return Result.success();
}
}

View File

@@ -0,0 +1,48 @@
package com.kronecker.backend.controller;
import com.kronecker.backend.common.Result;
import com.kronecker.backend.entity.Menu;
import com.kronecker.backend.service.MenuService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/menus")
@RequiredArgsConstructor
public class MenuController {
private final MenuService menuService;
@GetMapping("/tree")
public Result<List<Menu>> tree(@AuthenticationPrincipal UserDetails user) {
return Result.success(menuService.getMenusByUserId(Long.parseLong(user.getUsername())));
}
@GetMapping
public Result<List<Menu>> list() {
return Result.success(menuService.getTree());
}
@PostMapping
public Result<Void> create(@RequestBody Menu menu) {
menuService.save(menu);
return Result.success();
}
@PutMapping("/{id}")
public Result<Void> update(@PathVariable Long id, @RequestBody Menu menu) {
menu.setId(id);
menuService.updateById(menu);
return Result.success();
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
menuService.removeById(id);
return Result.success();
}
}

View File

@@ -0,0 +1,70 @@
package com.kronecker.backend.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kronecker.backend.common.Result;
import com.kronecker.backend.entity.Role;
import com.kronecker.backend.entity.RoleMenu;
import com.kronecker.backend.mapper.RoleMenuMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/roles")
@RequiredArgsConstructor
public class RoleController {
private final IService<Role> roleService;
private final RoleMenuMapper roleMenuMapper;
@GetMapping
public Result<List<Role>> list() {
return Result.success(roleService.list());
}
@GetMapping("/all")
public Result<List<Role>> all() {
return Result.success(roleService.list(
new LambdaQueryWrapper<Role>().eq(Role::getStatus, 1)));
}
@PostMapping
public Result<Void> create(@RequestBody Role role) {
roleService.save(role);
return Result.success();
}
@PutMapping("/{id}")
public Result<Void> update(@PathVariable Long id, @RequestBody Role role) {
role.setId(id);
roleService.updateById(role);
return Result.success();
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
roleService.removeById(id);
return Result.success();
}
@PutMapping("/{id}/menus")
@Transactional
public Result<Void> assignMenus(@PathVariable Long id, @RequestBody Map<String, List<Long>> body) {
List<Long> menuIds = body.get("menuIds");
roleMenuMapper.delete(new LambdaQueryWrapper<RoleMenu>().eq(RoleMenu::getRoleId, id));
if (menuIds != null && !menuIds.isEmpty()) {
for (Long menuId : menuIds) {
RoleMenu rm = new RoleMenu();
rm.setRoleId(id);
rm.setMenuId(menuId);
roleMenuMapper.insert(rm);
}
}
return Result.success();
}
}

View File

@@ -0,0 +1,71 @@
package com.kronecker.backend.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kronecker.backend.common.Result;
import com.kronecker.backend.entity.Medicine;
import com.kronecker.backend.entity.Stock;
import com.kronecker.backend.entity.Warehouse;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/stocks")
@RequiredArgsConstructor
public class StockController {
private final IService<Stock> stockService;
private final IService<Medicine> medicineService;
private final IService<Warehouse> warehouseService;
@GetMapping
public Result<Page<Stock>> list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String medicineName,
@RequestParam(required = false) String batchNo) {
var wrapper = new LambdaQueryWrapper<Stock>()
.like(StringUtils.hasText(batchNo), Stock::getBatchNo, batchNo)
.orderByAsc(Stock::getExpiryDate);
// 按药品名搜索需先查 medicine 表
if (StringUtils.hasText(medicineName)) {
var medIds = medicineService.list(
new LambdaQueryWrapper<Medicine>().like(Medicine::getName, medicineName)
).stream().map(Medicine::getId).toList();
if (medIds.isEmpty()) return Result.success(new Page<>());
wrapper.in(Stock::getMedicineId, medIds);
}
Page<Stock> result = stockService.page(new Page<>(page, size), wrapper);
populateNames(result.getRecords());
return Result.success(result);
}
@GetMapping("/warning")
public Result<Page<Stock>> warning(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
var wrapper = new LambdaQueryWrapper<Stock>()
.gt(Stock::getExpiryDate, java.time.LocalDate.now())
.orderByAsc(Stock::getExpiryDate);
Page<Stock> result = stockService.page(new Page<>(page, size), wrapper);
populateNames(result.getRecords());
return Result.success(result);
}
private void populateNames(java.util.List<Stock> stocks) {
if (stocks.isEmpty()) return;
var medIds = stocks.stream().map(Stock::getMedicineId).collect(Collectors.toSet());
var whIds = stocks.stream().map(Stock::getWarehouseId).collect(Collectors.toSet());
Map<Long, String> medMap = medicineService.listByIds(medIds).stream()
.collect(Collectors.toMap(Medicine::getId, Medicine::getName));
Map<Long, String> whMap = warehouseService.listByIds(whIds).stream()
.collect(Collectors.toMap(Warehouse::getId, Warehouse::getName));
stocks.forEach(s -> {
s.setMedicineName(medMap.getOrDefault(s.getMedicineId(), ""));
s.setWarehouseName(whMap.getOrDefault(s.getWarehouseId(), ""));
});
}
}

View File

@@ -0,0 +1,71 @@
package com.kronecker.backend.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kronecker.backend.common.Result;
import com.kronecker.backend.dto.StockInCreateDTO;
import com.kronecker.backend.entity.*;
import com.kronecker.backend.service.StockInService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/stock-in")
@RequiredArgsConstructor
public class StockInController {
private final IService<StockInRecord> recordService;
private final IService<StockInDetail> detailService;
private final IService<Supplier> supplierService;
private final StockInService stockInService;
@GetMapping
public Result<Page<StockInRecord>> list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
Page<StockInRecord> result = recordService.page(new Page<>(page, size),
new LambdaQueryWrapper<StockInRecord>().orderByDesc(StockInRecord::getCreateTime));
populateSupplierNames(result.getRecords());
return Result.success(result);
}
@GetMapping("/{id}")
public Result<StockInRecord> getById(@PathVariable Long id) {
StockInRecord record = recordService.getById(id);
if (record != null) {
record.setDetails(detailService.list(
new LambdaQueryWrapper<StockInDetail>().eq(StockInDetail::getRecordId, id)));
var supp = supplierService.getById(record.getSupplierId());
if (supp != null) record.setSupplierName(supp.getName());
}
return Result.success(record);
}
@PostMapping
public Result<Long> create(@Valid @RequestBody StockInCreateDTO dto,
@AuthenticationPrincipal UserDetails user) {
return Result.success(stockInService.create(dto, Long.parseLong(user.getUsername())));
}
@PutMapping("/{id}/status")
public Result<Void> audit(@PathVariable Long id, @RequestBody Map<String, Integer> body) {
if (body.get("status") == 1) {
stockInService.audit(id);
}
return Result.success();
}
private void populateSupplierNames(java.util.List<StockInRecord> records) {
if (records.isEmpty()) return;
var suppIds = records.stream().map(StockInRecord::getSupplierId).collect(Collectors.toSet());
Map<Long, String> suppMap = supplierService.listByIds(suppIds).stream()
.collect(Collectors.toMap(Supplier::getId, Supplier::getName));
records.forEach(r -> r.setSupplierName(suppMap.getOrDefault(r.getSupplierId(), "")));
}
}

View File

@@ -0,0 +1,78 @@
package com.kronecker.backend.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kronecker.backend.common.Result;
import com.kronecker.backend.dto.StockOutCreateDTO;
import com.kronecker.backend.entity.*;
import com.kronecker.backend.service.StockOutService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/stock-out")
@RequiredArgsConstructor
public class StockOutController {
private final IService<StockOutRecord> recordService;
private final IService<StockOutDetail> detailService;
private final IService<Customer> customerService;
private final StockOutService stockOutService;
@GetMapping
public Result<Page<StockOutRecord>> list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
Page<StockOutRecord> result = recordService.page(new Page<>(page, size),
new LambdaQueryWrapper<StockOutRecord>().orderByDesc(StockOutRecord::getCreateTime));
populateCustomerNames(result.getRecords());
return Result.success(result);
}
@GetMapping("/{id}")
public Result<StockOutRecord> getById(@PathVariable Long id) {
StockOutRecord record = recordService.getById(id);
if (record != null) {
record.setDetails(detailService.list(
new LambdaQueryWrapper<StockOutDetail>().eq(StockOutDetail::getRecordId, id)));
if (record.getCustomerId() != null) {
var cust = customerService.getById(record.getCustomerId());
if (cust != null) record.setCustomerName(cust.getName());
}
}
return Result.success(record);
}
@PostMapping
public Result<Long> create(@Valid @RequestBody StockOutCreateDTO dto,
@AuthenticationPrincipal UserDetails user) {
return Result.success(stockOutService.create(dto, Long.parseLong(user.getUsername())));
}
@PutMapping("/{id}/status")
public Result<Void> audit(@PathVariable Long id, @RequestBody Map<String, Integer> body) {
if (body.get("status") == 1) {
stockOutService.audit(id);
}
return Result.success();
}
private void populateCustomerNames(java.util.List<StockOutRecord> records) {
if (records.isEmpty()) return;
var custIds = records.stream().map(StockOutRecord::getCustomerId)
.filter(id -> id != null).collect(Collectors.toSet());
if (custIds.isEmpty()) return;
Map<Long, String> custMap = customerService.listByIds(custIds).stream()
.collect(Collectors.toMap(Customer::getId, Customer::getName));
records.forEach(r -> {
if (r.getCustomerId() != null)
r.setCustomerName(custMap.getOrDefault(r.getCustomerId(), ""));
});
}
}

View File

@@ -0,0 +1,55 @@
package com.kronecker.backend.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kronecker.backend.common.Result;
import com.kronecker.backend.entity.Supplier;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/suppliers")
@RequiredArgsConstructor
public class SupplierController {
private final IService<Supplier> supplierService;
@GetMapping
public Result<Page<Supplier>> list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String name) {
var wrapper = new LambdaQueryWrapper<Supplier>()
.like(StringUtils.hasText(name), Supplier::getName, name)
.orderByDesc(Supplier::getCreateTime);
return Result.success(supplierService.page(new Page<>(page, size), wrapper));
}
@GetMapping("/all")
public Result<List<Supplier>> all() {
return Result.success(supplierService.list(
new LambdaQueryWrapper<Supplier>().eq(Supplier::getStatus, 1)));
}
@PostMapping
public Result<Void> create(@RequestBody Supplier supplier) {
supplierService.save(supplier);
return Result.success();
}
@PutMapping("/{id}")
public Result<Void> update(@PathVariable Long id, @RequestBody Supplier supplier) {
supplier.setId(id);
supplierService.updateById(supplier);
return Result.success();
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
supplierService.removeById(id);
return Result.success();
}
}

View File

@@ -0,0 +1,83 @@
package com.kronecker.backend.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kronecker.backend.common.BusinessException;
import com.kronecker.backend.common.Result;
import com.kronecker.backend.entity.User;
import com.kronecker.backend.entity.UserRole;
import com.kronecker.backend.mapper.UserRoleMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final IService<User> userService;
private final PasswordEncoder passwordEncoder;
private final UserRoleMapper userRoleMapper;
@GetMapping
public Result<Page<User>> list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String username) {
var wrapper = new LambdaQueryWrapper<User>()
.like(StringUtils.hasText(username), User::getUsername, username)
.orderByDesc(User::getCreateTime)
.select(User.class, f -> !f.getColumn().equals("password"));
return Result.success(userService.page(new Page<>(page, size), wrapper));
}
@PostMapping
public Result<Void> create(@RequestBody User user) {
if (!StringUtils.hasText(user.getPassword())) {
throw new BusinessException("密码不能为空");
}
user.setPassword(passwordEncoder.encode(user.getPassword()));
userService.save(user);
return Result.success();
}
@PutMapping("/{id}")
public Result<Void> update(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
if (StringUtils.hasText(user.getPassword())) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
} else {
user.setPassword(null); // 不更新密码
}
userService.updateById(user);
return Result.success();
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
userService.removeById(id);
return Result.success();
}
@PutMapping("/{id}/roles")
@Transactional
public Result<Void> assignRoles(@PathVariable Long id, @RequestBody Map<String, List<Long>> body) {
List<Long> roleIds = body.get("roleIds");
userRoleMapper.delete(new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, id));
if (roleIds != null) {
for (Long roleId : roleIds) {
UserRole ur = new UserRole();
ur.setUserId(id);
ur.setRoleId(roleId);
userRoleMapper.insert(ur);
}
}
return Result.success();
}
}

View File

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

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.dto;
import lombok.Data;
@Data
public class PageQueryDTO {
private Integer page = 1;
private Integer size = 10;
}

View File

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

View File

@@ -0,0 +1,29 @@
package com.kronecker.backend.dto;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@Data
public class StockInCreateDTO {
@NotNull private Long supplierId;
@NotNull private Long warehouseId;
@NotNull private LocalDate inDate;
private String remark;
@NotNull private BigDecimal totalAmount;
@NotEmpty private List<Detail> details;
@Data
public static class Detail {
@NotNull private Long medicineId;
private String batchNo;
@NotNull private Integer quantity;
@NotNull private BigDecimal costPrice;
@NotNull private BigDecimal totalPrice;
private LocalDate productionDate;
@NotNull private LocalDate expiryDate;
}
}

View File

@@ -0,0 +1,28 @@
package com.kronecker.backend.dto;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@Data
public class StockOutCreateDTO {
private Long customerId;
@NotNull private Long warehouseId;
@NotNull private LocalDate outDate;
private Integer outType = 1;
private String remark;
@NotNull private BigDecimal totalAmount;
@NotEmpty private List<Detail> details;
@Data
public static class Detail {
@NotNull private Long medicineId;
private String batchNo;
@NotNull private Integer quantity;
@NotNull private BigDecimal unitPrice;
@NotNull private BigDecimal totalPrice;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
package com.kronecker.backend.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
@TableName("menu")
public class Menu {
@TableId(type = IdType.AUTO)
private Long id;
private Long parentId;
private String name;
private Integer type;
private String path;
private String component;
private String icon;
private String permissionCode;
private Integer sortOrder;
private Integer visible;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer isDeleted;
@TableField(exist = false)
private List<Menu> children;
}

View File

@@ -0,0 +1,22 @@
package com.kronecker.backend.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("role")
public class Role {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String code;
private String description;
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer isDeleted;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
package com.kronecker.backend.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Data
@TableName("stock_in_record")
public class StockInRecord {
@TableId(type = IdType.AUTO)
private Long id;
private String recordNo;
private Long supplierId;
private Long warehouseId;
private BigDecimal totalAmount;
private Long operatorId;
private LocalDate inDate;
private Integer status;
private String remark;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer isDeleted;
@TableField(exist = false)
private String supplierName;
@TableField(exist = false)
private List<StockInDetail> details;
}

View File

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

View File

@@ -0,0 +1,35 @@
package com.kronecker.backend.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Data
@TableName("stock_out_record")
public class StockOutRecord {
@TableId(type = IdType.AUTO)
private Long id;
private String recordNo;
private Long customerId;
private Long warehouseId;
private BigDecimal totalAmount;
private Long operatorId;
private LocalDate outDate;
private Integer outType;
private Integer status;
private String remark;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer isDeleted;
@TableField(exist = false)
private String customerName;
@TableField(exist = false)
private List<StockOutDetail> details;
}

View File

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

View File

@@ -0,0 +1,26 @@
package com.kronecker.backend.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String realName;
private String phone;
private String email;
private String avatar;
private Integer status;
private LocalDateTime lastLoginTime;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer isDeleted;
}

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.Customer;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CustomerMapper extends BaseMapper<Customer> {}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.MedicineCategory;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MedicineCategoryMapper extends BaseMapper<MedicineCategory> {}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.Medicine;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MedicineMapper extends BaseMapper<Medicine> {}

View File

@@ -0,0 +1,21 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.Menu;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
@Select("<script>" +
"SELECT DISTINCT m.permission_code FROM menu m " +
"INNER JOIN role_menu rm ON m.id = rm.menu_id " +
"WHERE rm.role_id IN <foreach item='id' collection='roleIds' open='(' separator=',' close=')'>#{id}</foreach> " +
"AND m.permission_code IS NOT NULL AND m.permission_code != ''" +
"</script>")
List<String> selectPermsByRoleIds(@Param("roleIds") List<Long> roleIds);
}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.Role;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RoleMapper extends BaseMapper<Role> {}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.RoleMenu;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RoleMenuMapper extends BaseMapper<RoleMenu> {}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.StockInDetail;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface StockInDetailMapper extends BaseMapper<StockInDetail> {}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.StockInRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface StockInRecordMapper extends BaseMapper<StockInRecord> {}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.Stock;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface StockMapper extends BaseMapper<Stock> {}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.StockOutDetail;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface StockOutDetailMapper extends BaseMapper<StockOutDetail> {}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.StockOutRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface StockOutRecordMapper extends BaseMapper<StockOutRecord> {}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.Supplier;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SupplierMapper extends BaseMapper<Supplier> {}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.UserRole;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserRoleMapper extends BaseMapper<UserRole> {}

View File

@@ -0,0 +1,8 @@
package com.kronecker.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kronecker.backend.entity.Warehouse;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface WarehouseMapper extends BaseMapper<Warehouse> {}

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
package com.kronecker.backend.security;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.kronecker.backend.entity.Menu;
import com.kronecker.backend.entity.User;
import com.kronecker.backend.entity.UserRole;
import com.kronecker.backend.mapper.MenuMapper;
import com.kronecker.backend.mapper.UserMapper;
import com.kronecker.backend.mapper.UserRoleMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
private final UserRoleMapper userRoleMapper;
private final MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
User user = userMapper.selectById(Long.parseLong(userId));
if (user == null || user.getStatus() == 0) {
throw new UsernameNotFoundException("用户不存在或已停用");
}
// 查角色 → 查权限
List<Long> roleIds = userRoleMapper.selectList(
new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, user.getId())
).stream().map(UserRole::getRoleId).toList();
List<String> permissions = new ArrayList<>();
if (!roleIds.isEmpty()) {
permissions = menuMapper.selectPermsByRoleIds(roleIds);
}
List<SimpleGrantedAuthority> authorities = permissions.stream()
.filter(p -> p != null && !p.isEmpty())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(
user.getId().toString(), user.getPassword(), authorities);
}
}

View File

@@ -0,0 +1,12 @@
package com.kronecker.backend.service;
import com.kronecker.backend.dto.RegisterDTO;
import java.util.Map;
public interface AuthService {
Map<String, Object> login(String username, String password);
String refreshToken(String refreshToken);
Map<String, Object> getUserInfo(Long userId);
void register(RegisterDTO dto);
}

View File

@@ -0,0 +1,14 @@
package com.kronecker.backend.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kronecker.backend.entity.Stock;
import com.kronecker.backend.entity.StockInRecord;
import com.kronecker.backend.entity.StockOutRecord;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
public interface DashboardService {
java.util.Map<String, Object> getStatistics();
Page<Stock> getExpiryWarning(Page<Stock> page);
Page<StockInRecord> getRecentStockIn(Page<StockInRecord> page);
Page<StockOutRecord> getRecentStockOut(Page<StockOutRecord> page);
}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kronecker.backend.entity.Medicine;
public interface MedicineService extends IService<Medicine> {
Page<Medicine> pageQuery(Page<Medicine> page, String name, Long categoryId);
}

View File

@@ -0,0 +1,10 @@
package com.kronecker.backend.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.kronecker.backend.entity.Menu;
import java.util.List;
public interface MenuService extends IService<Menu> {
List<Menu> getTree();
List<Menu> getMenusByUserId(Long userId);
}

View File

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

View File

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

View File

@@ -0,0 +1,99 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.kronecker.backend.common.BusinessException;
import com.kronecker.backend.common.ResultCode;
import com.kronecker.backend.dto.RegisterDTO;
import com.kronecker.backend.entity.*;
import com.kronecker.backend.mapper.*;
import com.kronecker.backend.security.JwtTokenProvider;
import com.kronecker.backend.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final UserMapper userMapper;
private final MenuMapper menuMapper;
private final UserRoleMapper userRoleMapper;
private final JwtTokenProvider jwtTokenProvider;
private final PasswordEncoder passwordEncoder;
@Override
public Map<String, Object> login(String username, String password) {
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
throw new BusinessException(ResultCode.UNAUTHORIZED.getCode(), "用户名或密码错误");
}
if (user.getStatus() == 0) {
throw new BusinessException("账号已被停用");
}
user.setLastLoginTime(LocalDateTime.now());
userMapper.updateById(user);
String token = jwtTokenProvider.generateToken(user.getId(), user.getUsername());
String refreshToken = jwtTokenProvider.generateRefreshToken(user.getId());
Map<String, Object> data = new HashMap<>();
data.put("token", token);
data.put("refreshToken", refreshToken);
data.put("userInfo", buildUserInfo(user));
return data;
}
@Override
public String refreshToken(String refreshToken) {
if (!jwtTokenProvider.validateToken(refreshToken)) {
throw new BusinessException(ResultCode.UNAUTHORIZED.getCode(), "Token已过期请重新登录");
}
Long userId = jwtTokenProvider.getUserId(refreshToken);
User user = userMapper.selectById(userId);
return jwtTokenProvider.generateToken(user.getId(), user.getUsername());
}
@Override
public Map<String, Object> getUserInfo(Long userId) {
User user = userMapper.selectById(userId);
return buildUserInfo(user);
}
@Override
@Transactional
public void register(RegisterDTO dto) {
if (userMapper.selectCount(new LambdaQueryWrapper<User>().eq(User::getUsername, dto.getUsername())) > 0) {
throw new BusinessException("用户名已存在");
}
User user = new User();
user.setUsername(dto.getUsername());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
user.setRealName(dto.getRealName() != null ? dto.getRealName() : dto.getUsername());
user.setPhone(dto.getPhone());
user.setEmail(dto.getEmail());
user.setStatus(1);
userMapper.insert(user);
// 分配默认角色普通员工role code = staff, id = 3
UserRole ur = new UserRole();
ur.setUserId(user.getId());
ur.setRoleId(3L);
userRoleMapper.insert(ur);
}
private Map<String, Object> buildUserInfo(User user) {
Map<String, Object> info = new HashMap<>();
info.put("id", user.getId());
info.put("username", user.getUsername());
info.put("realName", user.getRealName());
info.put("phone", user.getPhone());
info.put("email", user.getEmail());
info.put("avatar", user.getAvatar());
return info;
}
}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.MedicineCategory;
import com.kronecker.backend.mapper.MedicineCategoryMapper;
import org.springframework.stereotype.Service;
@Service
public class CategoryServiceImpl extends ServiceImpl<MedicineCategoryMapper, MedicineCategory> {}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.Customer;
import com.kronecker.backend.mapper.CustomerMapper;
import org.springframework.stereotype.Service;
@Service
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> {}

View File

@@ -0,0 +1,89 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.kronecker.backend.entity.*;
import com.kronecker.backend.mapper.*;
import com.kronecker.backend.service.DashboardService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.*;
@Service
@RequiredArgsConstructor
public class DashboardServiceImpl implements DashboardService {
private final MedicineMapper medicineMapper;
private final StockMapper stockMapper;
private final StockInRecordMapper stockInRecordMapper;
private final StockOutRecordMapper stockOutRecordMapper;
private final SupplierMapper supplierMapper;
private final CustomerMapper customerMapper;
@Override
public Map<String, Object> getStatistics() {
Map<String, Object> stats = new HashMap<>();
stats.put("medicineCount", medicineMapper.selectCount(null));
stats.put("stockBatchCount", stockMapper.selectCount(null));
stats.put("pendingStockIn", stockInRecordMapper.selectCount(
new LambdaQueryWrapper<StockInRecord>().eq(StockInRecord::getStatus, 0)));
stats.put("expiryWarningCount", stockMapper.selectCount(
new LambdaQueryWrapper<Stock>().le(Stock::getExpiryDate, LocalDate.now().plusDays(90))));
return stats;
}
@Override
public Page<Stock> getExpiryWarning(Page<Stock> page) {
Page<Stock> result = stockMapper.selectPage(page,
new LambdaQueryWrapper<Stock>()
.le(Stock::getExpiryDate, LocalDate.now().plusDays(90))
.orderByAsc(Stock::getExpiryDate));
populateStockNames(result.getRecords());
return result;
}
@Override
public Page<StockInRecord> getRecentStockIn(Page<StockInRecord> page) {
Page<StockInRecord> result = stockInRecordMapper.selectPage(page,
new LambdaQueryWrapper<StockInRecord>().orderByDesc(StockInRecord::getCreateTime));
populateInRecordNames(result.getRecords());
return result;
}
@Override
public Page<StockOutRecord> getRecentStockOut(Page<StockOutRecord> page) {
Page<StockOutRecord> result = stockOutRecordMapper.selectPage(page,
new LambdaQueryWrapper<StockOutRecord>().orderByDesc(StockOutRecord::getCreateTime));
populateOutRecordNames(result.getRecords());
return result;
}
private void populateStockNames(java.util.List<Stock> stocks) {
if (stocks.isEmpty()) return;
var medIds = stocks.stream().map(Stock::getMedicineId).collect(java.util.stream.Collectors.toSet());
var medMap = medicineMapper.selectBatchIds(medIds).stream()
.collect(java.util.stream.Collectors.toMap(Medicine::getId, Medicine::getName));
stocks.forEach(s -> s.setMedicineName(medMap.getOrDefault(s.getMedicineId(), "")));
}
private void populateInRecordNames(java.util.List<StockInRecord> records) {
if (records.isEmpty()) return;
var suppIds = records.stream().map(StockInRecord::getSupplierId).collect(java.util.stream.Collectors.toSet());
var suppMap = supplierMapper.selectBatchIds(suppIds).stream()
.collect(java.util.stream.Collectors.toMap(Supplier::getId, Supplier::getName));
records.forEach(r -> r.setSupplierName(suppMap.getOrDefault(r.getSupplierId(), "")));
}
private void populateOutRecordNames(java.util.List<StockOutRecord> records) {
if (records.isEmpty()) return;
var custIds = records.stream().map(StockOutRecord::getCustomerId)
.filter(id -> id != null).collect(java.util.stream.Collectors.toSet());
if (!custIds.isEmpty()) {
var custMap = customerMapper.selectBatchIds(custIds).stream()
.collect(java.util.stream.Collectors.toMap(Customer::getId, Customer::getName));
records.forEach(r -> { if (r.getCustomerId() != null) r.setCustomerName(custMap.getOrDefault(r.getCustomerId(), "")); });
}
}
}

View File

@@ -0,0 +1,23 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.Medicine;
import com.kronecker.backend.mapper.MedicineMapper;
import com.kronecker.backend.service.MedicineService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@Service
public class MedicineServiceImpl extends ServiceImpl<MedicineMapper, Medicine> implements MedicineService {
@Override
public Page<Medicine> pageQuery(Page<Medicine> page, String name, Long categoryId) {
LambdaQueryWrapper<Medicine> wrapper = new LambdaQueryWrapper<Medicine>()
.like(StringUtils.hasText(name), Medicine::getName, name)
.eq(categoryId != null, Medicine::getCategoryId, categoryId)
.orderByDesc(Medicine::getCreateTime);
return baseMapper.selectPage(page, wrapper);
}
}

View File

@@ -0,0 +1,56 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.Menu;
import com.kronecker.backend.entity.RoleMenu;
import com.kronecker.backend.entity.UserRole;
import com.kronecker.backend.mapper.*;
import com.kronecker.backend.service.MenuService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {
private final UserRoleMapper userRoleMapper;
private final RoleMenuMapper roleMenuMapper;
private final MenuMapper menuMapper;
@Override
public List<Menu> getTree() {
List<Menu> all = menuMapper.selectList(
new LambdaQueryWrapper<Menu>().orderByAsc(Menu::getSortOrder));
return buildTree(all, 0L);
}
@Override
public List<Menu> getMenusByUserId(Long userId) {
List<Long> roleIds = userRoleMapper.selectList(
new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, userId)
).stream().map(UserRole::getRoleId).toList();
if (roleIds.isEmpty()) return Collections.emptyList();
Set<Long> menuIds = roleMenuMapper.selectList(
new LambdaQueryWrapper<RoleMenu>().in(RoleMenu::getRoleId, roleIds)
).stream().map(RoleMenu::getMenuId).collect(Collectors.toSet());
if (menuIds.isEmpty()) return Collections.emptyList();
List<Menu> menus = menuMapper.selectList(
new LambdaQueryWrapper<Menu>()
.in(Menu::getId, menuIds)
.orderByAsc(Menu::getSortOrder));
return buildTree(menus, 0L);
}
private List<Menu> buildTree(List<Menu> list, Long parentId) {
return list.stream()
.filter(m -> m.getParentId().equals(parentId))
.peek(m -> m.setChildren(buildTree(list, m.getId())))
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.Role;
import com.kronecker.backend.mapper.RoleMapper;
import org.springframework.stereotype.Service;
@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> {}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.StockInDetail;
import com.kronecker.backend.mapper.StockInDetailMapper;
import org.springframework.stereotype.Service;
@Service
public class StockInDetailServiceImpl extends ServiceImpl<StockInDetailMapper, StockInDetail> {}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.StockInRecord;
import com.kronecker.backend.mapper.StockInRecordMapper;
import org.springframework.stereotype.Service;
@Service
public class StockInRecordServiceImpl extends ServiceImpl<StockInRecordMapper, StockInRecord> {}

View File

@@ -0,0 +1,96 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.kronecker.backend.common.BusinessException;
import com.kronecker.backend.dto.StockInCreateDTO;
import com.kronecker.backend.entity.*;
import com.kronecker.backend.mapper.*;
import com.kronecker.backend.service.StockInService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@Service
@RequiredArgsConstructor
public class StockInServiceImpl implements StockInService {
private final StockInRecordMapper recordMapper;
private final StockInDetailMapper detailMapper;
private final StockMapper stockMapper;
@Override
@Transactional
public Long create(StockInCreateDTO dto, Long operatorId) {
// 生成单号
String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
long count = recordMapper.selectCount(new LambdaQueryWrapper<StockInRecord>()
.likeRight(StockInRecord::getRecordNo, "SI" + date)) + 1;
String recordNo = String.format("SI%s%04d", date, count);
StockInRecord record = new StockInRecord();
record.setRecordNo(recordNo);
record.setSupplierId(dto.getSupplierId());
record.setWarehouseId(dto.getWarehouseId());
record.setTotalAmount(dto.getTotalAmount());
record.setOperatorId(operatorId);
record.setInDate(dto.getInDate());
record.setStatus(0);
record.setRemark(dto.getRemark());
recordMapper.insert(record);
for (var d : dto.getDetails()) {
StockInDetail detail = new StockInDetail();
detail.setRecordId(record.getId());
detail.setMedicineId(d.getMedicineId());
detail.setBatchNo(d.getBatchNo());
detail.setQuantity(d.getQuantity());
detail.setCostPrice(d.getCostPrice());
detail.setTotalPrice(d.getTotalPrice());
detail.setProductionDate(d.getProductionDate());
detail.setExpiryDate(d.getExpiryDate());
detailMapper.insert(detail);
}
return record.getId();
}
@Override
@Transactional
public void audit(Long id) {
StockInRecord record = recordMapper.selectById(id);
if (record == null || record.getStatus() != 0) {
throw new BusinessException("入库单不存在或状态不正确");
}
record.setStatus(1);
recordMapper.updateById(record);
// 更新库存
var details = detailMapper.selectList(
new LambdaQueryWrapper<StockInDetail>().eq(StockInDetail::getRecordId, id));
for (var d : details) {
// 查找同批号库存
var stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>()
.eq(Stock::getMedicineId, d.getMedicineId())
.eq(Stock::getWarehouseId, record.getWarehouseId())
.eq(Stock::getBatchNo, d.getBatchNo())
.last("LIMIT 1"));
if (stock != null) {
stock.setQuantity(stock.getQuantity() + d.getQuantity());
stock.setCostPrice(d.getCostPrice());
stockMapper.updateById(stock);
} else {
Stock s = new Stock();
s.setMedicineId(d.getMedicineId());
s.setWarehouseId(record.getWarehouseId());
s.setBatchNo(d.getBatchNo());
s.setQuantity(d.getQuantity());
s.setCostPrice(d.getCostPrice());
s.setProductionDate(d.getProductionDate());
s.setExpiryDate(d.getExpiryDate());
stockMapper.insert(s);
}
}
}
}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.StockOutDetail;
import com.kronecker.backend.mapper.StockOutDetailMapper;
import org.springframework.stereotype.Service;
@Service
public class StockOutDetailServiceImpl extends ServiceImpl<StockOutDetailMapper, StockOutDetail> {}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.StockOutRecord;
import com.kronecker.backend.mapper.StockOutRecordMapper;
import org.springframework.stereotype.Service;
@Service
public class StockOutRecordServiceImpl extends ServiceImpl<StockOutRecordMapper, StockOutRecord> {}

View File

@@ -0,0 +1,99 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.kronecker.backend.common.BusinessException;
import com.kronecker.backend.dto.StockOutCreateDTO;
import com.kronecker.backend.entity.*;
import com.kronecker.backend.mapper.*;
import com.kronecker.backend.service.StockOutService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@Service
@RequiredArgsConstructor
public class StockOutServiceImpl implements StockOutService {
private final StockOutRecordMapper recordMapper;
private final StockOutDetailMapper detailMapper;
private final StockMapper stockMapper;
@Override
@Transactional
public Long create(StockOutCreateDTO dto, Long operatorId) {
String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
long count = recordMapper.selectCount(new LambdaQueryWrapper<StockOutRecord>()
.likeRight(StockOutRecord::getRecordNo, "SO" + date)) + 1;
String recordNo = String.format("SO%s%04d", date, count);
StockOutRecord record = new StockOutRecord();
record.setRecordNo(recordNo);
record.setCustomerId(dto.getCustomerId());
record.setWarehouseId(dto.getWarehouseId());
record.setTotalAmount(dto.getTotalAmount());
record.setOperatorId(operatorId);
record.setOutDate(dto.getOutDate());
record.setOutType(dto.getOutType() != null ? dto.getOutType() : 1);
record.setStatus(0);
record.setRemark(dto.getRemark());
recordMapper.insert(record);
for (var d : dto.getDetails()) {
StockOutDetail detail = new StockOutDetail();
detail.setRecordId(record.getId());
detail.setMedicineId(d.getMedicineId());
detail.setBatchNo(d.getBatchNo());
detail.setQuantity(d.getQuantity());
detail.setUnitPrice(d.getUnitPrice());
detail.setTotalPrice(d.getTotalPrice());
detailMapper.insert(detail);
}
return record.getId();
}
@Override
@Transactional
public void audit(Long id) {
StockOutRecord record = recordMapper.selectById(id);
if (record == null || record.getStatus() != 0) {
throw new BusinessException("出库单不存在或状态不正确");
}
var details = detailMapper.selectList(
new LambdaQueryWrapper<StockOutDetail>().eq(StockOutDetail::getRecordId, id));
// 检查库存是否充足
for (var d : details) {
int totalStock = stockMapper.selectList(new LambdaQueryWrapper<Stock>()
.eq(Stock::getMedicineId, d.getMedicineId())
.eq(Stock::getWarehouseId, record.getWarehouseId())
.eq(Stock::getBatchNo, d.getBatchNo()))
.stream().mapToInt(Stock::getQuantity).sum();
if (totalStock < d.getQuantity()) {
throw new BusinessException("药品批次 " + d.getBatchNo() + " 库存不足");
}
}
// 扣减库存FIFO: 先过期先出)
for (var d : details) {
int remaining = d.getQuantity();
var stocks = stockMapper.selectList(new LambdaQueryWrapper<Stock>()
.eq(Stock::getMedicineId, d.getMedicineId())
.eq(Stock::getWarehouseId, record.getWarehouseId())
.eq(Stock::getBatchNo, d.getBatchNo())
.orderByAsc(Stock::getExpiryDate));
for (Stock s : stocks) {
if (remaining <= 0) break;
int deduct = Math.min(s.getQuantity(), remaining);
s.setQuantity(s.getQuantity() - deduct);
stockMapper.updateById(s);
remaining -= deduct;
}
}
record.setStatus(1);
recordMapper.updateById(record);
}
}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.Stock;
import com.kronecker.backend.mapper.StockMapper;
import org.springframework.stereotype.Service;
@Service
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> {}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.Supplier;
import com.kronecker.backend.mapper.SupplierMapper;
import org.springframework.stereotype.Service;
@Service
public class SupplierServiceImpl extends ServiceImpl<SupplierMapper, Supplier> {}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.UserRole;
import com.kronecker.backend.mapper.UserRoleMapper;
import org.springframework.stereotype.Service;
@Service
public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> {}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.User;
import com.kronecker.backend.mapper.UserMapper;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> {}

View File

@@ -0,0 +1,9 @@
package com.kronecker.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.kronecker.backend.entity.Warehouse;
import com.kronecker.backend.mapper.WarehouseMapper;
import org.springframework.stereotype.Service;
@Service
public class WarehouseServiceImpl extends ServiceImpl<WarehouseMapper, Warehouse> {}

View File

@@ -0,0 +1,32 @@
server:
port: ${SERVER_PORT:8080}
spring:
application:
name: backend
datasource:
url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:medicine_manager}?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
username: ${DB_USERNAME:root}
password: ${DB_PASSWORD:root}
driver-class-name: com.mysql.cj.jdbc.Driver
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
type-aliases-package: com.kronecker.backend.entity
global-config:
db-config:
id-type: auto
logic-delete-field: isDeleted
logic-delete-value: 1
logic-not-delete-value: 0
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
jwt:
secret: ${JWT_SECRET:Kzuxv0tsjN/GTO/udrRLgXj+Pr+uW1PL3PfGIXGstkQ=}
expiration: ${JWT_EXPIRATION:86400000}
refresh-expiration: ${JWT_REFRESH_EXPIRATION:604800000}

View File

@@ -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);
-- -----------------------------------------------------------
-- 用户角色关联

View File

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