9.17/1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
## 前后端数据库状态说明
|
||||
|
||||
**更新日期**: 2025-09-16
|
||||
**更新日期**: 2025-09-17
|
||||
|
||||
### 概要
|
||||
- 数据库已落地:已在远程 MySQL `mysql.tonaspace.com` 的 `partsinquiry` 库完成初始化(表结构与触发器已创建)。
|
||||
@@ -27,12 +27,15 @@
|
||||
- 关闭:不设置/置为 `false` 即可停用(生产环境默认关闭)。
|
||||
- 完全移除:删除 `frontend/common/config.js` 中默认用户配置与 `frontend/common/http.js` 中注入逻辑。
|
||||
|
||||
### 后端(Spring Boot)数据库状态
|
||||
- 依赖:`pom.xml` 未包含 `spring-boot-starter-web`、`spring-boot-starter-data-jpa`、`mysql-connector-j` 等数据库相关依赖。
|
||||
- 配置:`src/main/resources/application.properties` 仅有 `spring.application.name=demo`;未配置 `spring.datasource.*`、`spring.jpa.*`。
|
||||
- 数据模型:`src/main/java` 未发现 `@Entity`、Repository、Service;存在 `backend/db/db.sql` 脚本,已执行至远程库。
|
||||
- 迁移:未发现 Flyway/Liquibase 配置与脚本(当前通过 MysqlMCP 手工执行)。
|
||||
- 结论:数据库已初始化,但后端未配置运行时数据源与接口,暂不可用。
|
||||
### 后端(Spring Boot)状态
|
||||
- 依赖:`pom.xml` 已包含 `spring-boot-starter-web`、`spring-boot-starter-data-jpa`、`mysql-connector-j`。
|
||||
- 配置:`application.properties` 使用环境变量注入数据源,已补充 Hikari/JPA;新增附件占位图配置:
|
||||
- `attachments.placeholder.image-path`(env: `ATTACHMENTS_PLACEHOLDER_IMAGE`)
|
||||
- `attachments.placeholder.url-path`(env: `ATTACHMENTS_PLACEHOLDER_URL`,默认 `/api/attachments/placeholder`)
|
||||
- 接口:新增附件相关接口(占位方案):
|
||||
- POST `/api/attachments`:忽略内容,返回 `{ url: "/api/attachments/placeholder" }`
|
||||
- GET `/api/attachments/placeholder`:返回本地占位图二进制
|
||||
- 迁移:仍建议引入 Flyway/Liquibase;结构变更继续通过 MysqlMCP 并同步 `/doc/database_documentation.md`。
|
||||
|
||||
### 前端(uni-app)数据库状态
|
||||
- 数据持久化:未见 IndexedDB/WebSQL/SQLite/云数据库使用;页面数据为内置静态数据。
|
||||
@@ -49,5 +52,26 @@
|
||||
- 引入迁移工具(Flyway/Liquibase)管理 DDL;后续所有变更继续通过 MysqlMCP 执行,并同步 `/doc/database_documentation.md`。
|
||||
- 增加健康检查与基础 CRUD 接口;在 `/doc/openapi.yaml` 按规范登记并标注实现状态(❌/✅)。
|
||||
|
||||
### 前端默认连接策略
|
||||
- 默认后端地址:`http://192.168.31.193:8080`(可被环境变量/Storage 覆盖)
|
||||
- 多地址重试:按顺序尝试(去重处理):`[ENV, Storage, 192.168.31.193:8080, 127.0.0.1:8080, localhost:8080]`
|
||||
- 默认用户:开启(可被环境变量/Storage 关闭),请求自动附带 `X-User-Id`(默认 `2`)。
|
||||
- 如需关闭:在 Storage 或构建环境中设置 `ENABLE_DEFAULT_USER=false`。
|
||||
|
||||
|
||||
### 占位图策略(当前阶段)
|
||||
- 说明:所有图片上传与展示均统一使用占位图,实际文件存储暂不开发。
|
||||
- 本地占位图:`C:\Users\21826\Desktop\Wj\PartsInquiry\backend\picture\屏幕截图 2025-08-14 134657.png`
|
||||
- 配置方式:
|
||||
- PowerShell(当前用户持久化)
|
||||
```powershell
|
||||
setx ATTACHMENTS_PLACEHOLDER_IMAGE "C:\\Users\\21826\\Desktop\\Wj\\PartsInquiry\\backend\\picture\\屏幕截图 2025-08-14 134657.png"
|
||||
setx ATTACHMENTS_PLACEHOLDER_URL "/api/attachments/placeholder"
|
||||
```
|
||||
- 应用重启后生效;也可在运行环境变量中注入。
|
||||
- 前端影响:
|
||||
- `components/ImageUploader.vue` 上传始终得到 `{ url: '/api/attachments/placeholder' }`
|
||||
- 商品列表/详情展示该占位图地址
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
backend/picture/屏幕截图 2025-08-14 134657.png
Normal file
BIN
backend/picture/屏幕截图 2025-08-14 134657.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,73 @@
|
||||
package com.example.demo.attachment;
|
||||
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/attachments")
|
||||
public class AttachmentController {
|
||||
|
||||
private final AttachmentPlaceholderProperties placeholderProperties;
|
||||
|
||||
public AttachmentController(AttachmentPlaceholderProperties placeholderProperties) {
|
||||
this.placeholderProperties = placeholderProperties;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<Map<String, Object>> upload(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "ownerType", required = false) String ownerType,
|
||||
@RequestParam(value = "ownerId", required = false) String ownerId) {
|
||||
// 占位实现:忽略文件内容,始终返回占位图 URL
|
||||
String url = StringUtils.hasText(placeholderProperties.getUrlPath()) ? placeholderProperties.getUrlPath() : "/api/attachments/placeholder";
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("url", url);
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@GetMapping("/placeholder")
|
||||
public ResponseEntity<Resource> placeholder() throws IOException {
|
||||
String imagePath = placeholderProperties.getImagePath();
|
||||
if (!StringUtils.hasText(imagePath)) {
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
Path path = Path.of(imagePath);
|
||||
if (!Files.exists(path)) {
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
Resource resource = new FileSystemResource(path);
|
||||
String contentType = Files.probeContentType(path);
|
||||
MediaType mediaType;
|
||||
try {
|
||||
mediaType = StringUtils.hasText(contentType) ? MediaType.parseMediaType(contentType) : MediaType.IMAGE_PNG;
|
||||
} catch (Exception e) {
|
||||
mediaType = MediaType.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=placeholder")
|
||||
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS).cachePublic())
|
||||
.contentType(mediaType)
|
||||
.body(resource);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.example.demo.attachment;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "attachments.placeholder")
|
||||
public class AttachmentPlaceholderProperties {
|
||||
|
||||
private String imagePath;
|
||||
private String urlPath = "/api/attachments/placeholder";
|
||||
|
||||
public String getImagePath() {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
public void setImagePath(String imagePath) {
|
||||
this.imagePath = imagePath;
|
||||
}
|
||||
|
||||
public String getUrlPath() {
|
||||
return urlPath;
|
||||
}
|
||||
|
||||
public void setUrlPath(String urlPath) {
|
||||
this.urlPath = urlPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "app.defaults")
|
||||
public class AppDefaultsProperties {
|
||||
|
||||
private Long shopId = 1L;
|
||||
private Long userId = 2L;
|
||||
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -17,9 +17,11 @@ public class DashboardController {
|
||||
}
|
||||
|
||||
@GetMapping("/overview")
|
||||
public ResponseEntity<DashboardOverviewResponse> overview(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
|
||||
long sid = (shopId == null ? 1L : shopId);
|
||||
return ResponseEntity.ok(dashboardService.getOverview(sid));
|
||||
public ResponseEntity<DashboardOverviewResponse> overview(
|
||||
@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestHeader(name = "X-User-Id", required = false) Long userId
|
||||
) {
|
||||
return ResponseEntity.ok(dashboardService.getOverviewByUserOrShop(userId, shopId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import java.math.BigDecimal;
|
||||
|
||||
public class DashboardOverviewResponse {
|
||||
private BigDecimal todaySalesAmount;
|
||||
private BigDecimal monthSalesAmount;
|
||||
private BigDecimal monthGrossProfit;
|
||||
private BigDecimal stockTotalQuantity;
|
||||
|
||||
public DashboardOverviewResponse(BigDecimal todaySalesAmount, BigDecimal monthGrossProfit, BigDecimal stockTotalQuantity) {
|
||||
public DashboardOverviewResponse(BigDecimal todaySalesAmount, BigDecimal monthSalesAmount, BigDecimal monthGrossProfit, BigDecimal stockTotalQuantity) {
|
||||
this.todaySalesAmount = todaySalesAmount;
|
||||
this.monthSalesAmount = monthSalesAmount;
|
||||
this.monthGrossProfit = monthGrossProfit;
|
||||
this.stockTotalQuantity = stockTotalQuantity;
|
||||
}
|
||||
@@ -24,6 +26,10 @@ public class DashboardOverviewResponse {
|
||||
public BigDecimal getStockTotalQuantity() {
|
||||
return stockTotalQuantity;
|
||||
}
|
||||
|
||||
public BigDecimal getMonthSalesAmount() {
|
||||
return monthSalesAmount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,16 @@ public class DashboardRepository {
|
||||
return toBigDecimal(result);
|
||||
}
|
||||
|
||||
public BigDecimal sumMonthSalesOrders(Long shopId) {
|
||||
Object result = entityManager.createNativeQuery(
|
||||
"SELECT COALESCE(SUM(amount), 0) FROM sales_orders " +
|
||||
"WHERE shop_id = :shopId AND status = 'approved' " +
|
||||
"AND order_time >= DATE_FORMAT(CURRENT_DATE(), '%Y-%m-01') " +
|
||||
"AND order_time < DATE_ADD(DATE_FORMAT(CURRENT_DATE(), '%Y-%m-01'), INTERVAL 1 MONTH)"
|
||||
).setParameter("shopId", shopId).getSingleResult();
|
||||
return toBigDecimal(result);
|
||||
}
|
||||
|
||||
public BigDecimal sumTotalInventoryQty(Long shopId) {
|
||||
Object result = entityManager.createNativeQuery(
|
||||
"SELECT COALESCE(SUM(quantity), 0) FROM inventories WHERE shop_id = :shopId"
|
||||
@@ -50,6 +60,20 @@ public class DashboardRepository {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
public Long findShopIdByUserId(Long userId) {
|
||||
if (userId == null) return null;
|
||||
Object result = entityManager.createNativeQuery(
|
||||
"SELECT shop_id FROM users WHERE id = :userId LIMIT 1"
|
||||
).setParameter("userId", userId).getSingleResult();
|
||||
if (result == null) return null;
|
||||
if (result instanceof Number) return ((Number) result).longValue();
|
||||
try {
|
||||
return Long.valueOf(result.toString());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,9 +14,24 @@ public class DashboardService {
|
||||
|
||||
public DashboardOverviewResponse getOverview(long shopId) {
|
||||
BigDecimal todaySales = dashboardRepository.sumTodaySalesOrders(shopId);
|
||||
BigDecimal monthSales = dashboardRepository.sumMonthSalesOrders(shopId);
|
||||
BigDecimal monthGrossProfit = dashboardRepository.sumMonthGrossProfitApprox(shopId);
|
||||
BigDecimal stockTotalQty = dashboardRepository.sumTotalInventoryQty(shopId);
|
||||
return new DashboardOverviewResponse(todaySales, monthGrossProfit, stockTotalQty);
|
||||
return new DashboardOverviewResponse(todaySales, monthSales, monthGrossProfit, stockTotalQty);
|
||||
}
|
||||
|
||||
public DashboardOverviewResponse getOverviewByUserOrShop(Long userId, Long shopIdOrNull) {
|
||||
Long resolvedShopId = null;
|
||||
if (userId != null) {
|
||||
resolvedShopId = dashboardRepository.findShopIdByUserId(userId);
|
||||
}
|
||||
if (resolvedShopId == null && shopIdOrNull != null) {
|
||||
resolvedShopId = shopIdOrNull;
|
||||
}
|
||||
if (resolvedShopId == null) {
|
||||
resolvedShopId = 1L;
|
||||
}
|
||||
return getOverview(resolvedShopId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.demo.product.controller;
|
||||
|
||||
import com.example.demo.common.AppDefaultsProperties;
|
||||
import com.example.demo.product.repo.CategoryRepository;
|
||||
import com.example.demo.product.repo.UnitRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class MetadataController {
|
||||
|
||||
private final UnitRepository unitRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final AppDefaultsProperties defaults;
|
||||
|
||||
public MetadataController(UnitRepository unitRepository, CategoryRepository categoryRepository, AppDefaultsProperties defaults) {
|
||||
this.unitRepository = unitRepository;
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
@GetMapping("/api/product-units")
|
||||
public ResponseEntity<?> listUnits(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("list", unitRepository.listByShop(sid));
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@GetMapping("/api/product-categories")
|
||||
public ResponseEntity<?> listCategories(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("list", categoryRepository.listByShop(sid));
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.example.demo.product.controller;
|
||||
|
||||
import com.example.demo.common.AppDefaultsProperties;
|
||||
import com.example.demo.product.dto.ProductDtos;
|
||||
import com.example.demo.product.service.ProductService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/products")
|
||||
public class ProductController {
|
||||
|
||||
private final ProductService productService;
|
||||
private final AppDefaultsProperties defaults;
|
||||
|
||||
public ProductController(ProductService productService, AppDefaultsProperties defaults) {
|
||||
this.productService = productService;
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> search(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestParam(name = "kw", required = false) String kw,
|
||||
@RequestParam(name = "categoryId", required = false) Long categoryId,
|
||||
@RequestParam(name = "page", defaultValue = "1") int page,
|
||||
@RequestParam(name = "size", defaultValue = "50") int size) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Page<ProductDtos.ProductListItem> result = productService.search(sid, kw, categoryId, Math.max(page - 1, 0), size);
|
||||
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
||||
body.put("list", result.getContent());
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<?> detail(@PathVariable("id") Long id) {
|
||||
return productService.findDetail(id)
|
||||
.<ResponseEntity<?>>map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> create(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestHeader(name = "X-User-Id", required = false) Long userId,
|
||||
@RequestBody ProductDtos.CreateOrUpdateProductRequest req) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Long uid = (userId == null ? defaults.getUserId() : userId);
|
||||
Long id = productService.create(sid, uid, req);
|
||||
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
||||
body.put("id", id);
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> update(@PathVariable("id") Long id,
|
||||
@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestHeader(name = "X-User-Id", required = false) Long userId,
|
||||
@RequestBody ProductDtos.CreateOrUpdateProductRequest req) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Long uid = (userId == null ? defaults.getUserId() : userId);
|
||||
productService.update(id, sid, uid, req);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.example.demo.product.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public class ProductDtos {
|
||||
|
||||
public static class ProductListItem {
|
||||
public Long id;
|
||||
public String name;
|
||||
public String brand;
|
||||
public String model;
|
||||
public String spec;
|
||||
public BigDecimal stock; // from inventories.quantity
|
||||
public BigDecimal retailPrice; // from product_prices
|
||||
public String cover; // first image url
|
||||
}
|
||||
|
||||
public static class ProductDetail {
|
||||
public Long id;
|
||||
public String name;
|
||||
public String barcode;
|
||||
public String brand;
|
||||
public String model;
|
||||
public String spec;
|
||||
public String origin;
|
||||
public Long categoryId;
|
||||
public Long unitId;
|
||||
public BigDecimal safeMin;
|
||||
public BigDecimal safeMax;
|
||||
public BigDecimal stock;
|
||||
public BigDecimal purchasePrice;
|
||||
public BigDecimal retailPrice;
|
||||
public BigDecimal distributionPrice;
|
||||
public BigDecimal wholesalePrice;
|
||||
public BigDecimal bigClientPrice;
|
||||
public List<Image> images;
|
||||
}
|
||||
|
||||
public static class Image {
|
||||
public String url;
|
||||
}
|
||||
|
||||
public static class CreateOrUpdateProductRequest {
|
||||
public String name;
|
||||
public String barcode;
|
||||
public String brand;
|
||||
public String model;
|
||||
public String spec;
|
||||
public String origin;
|
||||
public Long categoryId;
|
||||
public Long unitId;
|
||||
public BigDecimal safeMin;
|
||||
public BigDecimal safeMax;
|
||||
public Prices prices;
|
||||
public BigDecimal stock;
|
||||
public List<String> images;
|
||||
public String remark; // map to products.description
|
||||
}
|
||||
|
||||
public static class Prices {
|
||||
public BigDecimal purchasePrice;
|
||||
public BigDecimal retailPrice;
|
||||
public BigDecimal distributionPrice;
|
||||
public BigDecimal wholesalePrice;
|
||||
public BigDecimal bigClientPrice;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "inventories")
|
||||
public class Inventory {
|
||||
|
||||
@Id
|
||||
@Column(name = "product_id")
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "shop_id", nullable = false)
|
||||
private Long shopId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "quantity", precision = 18, scale = 3, nullable = false)
|
||||
private BigDecimal quantity = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getProductId() { return productId; }
|
||||
public void setProductId(Long productId) { this.productId = productId; }
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public BigDecimal getQuantity() { return quantity; }
|
||||
public void setQuantity(BigDecimal quantity) { this.quantity = quantity; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "products")
|
||||
public class Product {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "shop_id", nullable = false)
|
||||
private Long shopId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 120)
|
||||
private String name;
|
||||
|
||||
@Column(name = "category_id")
|
||||
private Long categoryId;
|
||||
|
||||
@Column(name = "unit_id", nullable = false)
|
||||
private Long unitId;
|
||||
|
||||
@Column(name = "brand", length = 64)
|
||||
private String brand;
|
||||
|
||||
@Column(name = "model", length = 64)
|
||||
private String model;
|
||||
|
||||
@Column(name = "spec", length = 128)
|
||||
private String spec;
|
||||
|
||||
@Column(name = "origin", length = 64)
|
||||
private String origin;
|
||||
|
||||
@Column(name = "barcode", length = 32)
|
||||
private String barcode;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
@Column(name = "safe_min", precision = 18, scale = 3)
|
||||
private BigDecimal safeMin;
|
||||
|
||||
@Column(name = "safe_max", precision = 18, scale = 3)
|
||||
private BigDecimal safeMax;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public Long getCategoryId() { return categoryId; }
|
||||
public void setCategoryId(Long categoryId) { this.categoryId = categoryId; }
|
||||
public Long getUnitId() { return unitId; }
|
||||
public void setUnitId(Long unitId) { this.unitId = unitId; }
|
||||
public String getBrand() { return brand; }
|
||||
public void setBrand(String brand) { this.brand = brand; }
|
||||
public String getModel() { return model; }
|
||||
public void setModel(String model) { this.model = model; }
|
||||
public String getSpec() { return spec; }
|
||||
public void setSpec(String spec) { this.spec = spec; }
|
||||
public String getOrigin() { return origin; }
|
||||
public void setOrigin(String origin) { this.origin = origin; }
|
||||
public String getBarcode() { return barcode; }
|
||||
public void setBarcode(String barcode) { this.barcode = barcode; }
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
public BigDecimal getSafeMin() { return safeMin; }
|
||||
public void setSafeMin(BigDecimal safeMin) { this.safeMin = safeMin; }
|
||||
public BigDecimal getSafeMax() { return safeMax; }
|
||||
public void setSafeMax(BigDecimal safeMax) { this.safeMax = safeMax; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
public LocalDateTime getDeletedAt() { return deletedAt; }
|
||||
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "product_categories")
|
||||
public class ProductCategory {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "shop_id", nullable = false)
|
||||
private Long shopId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 64)
|
||||
private String name;
|
||||
|
||||
@Column(name = "parent_id")
|
||||
private Long parentId;
|
||||
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
private Integer sortOrder = 0;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public Long getParentId() { return parentId; }
|
||||
public void setParentId(Long parentId) { this.parentId = parentId; }
|
||||
public Integer getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
public LocalDateTime getDeletedAt() { return deletedAt; }
|
||||
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "product_images")
|
||||
public class ProductImage {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "shop_id", nullable = false)
|
||||
private Long shopId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "product_id", nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "url", nullable = false, length = 512)
|
||||
private String url;
|
||||
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
private Integer sortOrder = 0;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public Long getProductId() { return productId; }
|
||||
public void setProductId(Long productId) { this.productId = productId; }
|
||||
public String getUrl() { return url; }
|
||||
public void setUrl(String url) { this.url = url; }
|
||||
public Integer getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "product_prices")
|
||||
public class ProductPrice {
|
||||
|
||||
@Id
|
||||
@Column(name = "product_id")
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "shop_id", nullable = false)
|
||||
private Long shopId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "purchase_price", precision = 18, scale = 2, nullable = false)
|
||||
private BigDecimal purchasePrice = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "retail_price", precision = 18, scale = 2, nullable = false)
|
||||
private BigDecimal retailPrice = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "distribution_price", precision = 18, scale = 2, nullable = false)
|
||||
private BigDecimal distributionPrice = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "wholesale_price", precision = 18, scale = 2, nullable = false)
|
||||
private BigDecimal wholesalePrice = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "big_client_price", precision = 18, scale = 2, nullable = false)
|
||||
private BigDecimal bigClientPrice = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getProductId() { return productId; }
|
||||
public void setProductId(Long productId) { this.productId = productId; }
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public BigDecimal getPurchasePrice() { return purchasePrice; }
|
||||
public void setPurchasePrice(BigDecimal purchasePrice) { this.purchasePrice = purchasePrice; }
|
||||
public BigDecimal getRetailPrice() { return retailPrice; }
|
||||
public void setRetailPrice(BigDecimal retailPrice) { this.retailPrice = retailPrice; }
|
||||
public BigDecimal getDistributionPrice() { return distributionPrice; }
|
||||
public void setDistributionPrice(BigDecimal distributionPrice) { this.distributionPrice = distributionPrice; }
|
||||
public BigDecimal getWholesalePrice() { return wholesalePrice; }
|
||||
public void setWholesalePrice(BigDecimal wholesalePrice) { this.wholesalePrice = wholesalePrice; }
|
||||
public BigDecimal getBigClientPrice() { return bigClientPrice; }
|
||||
public void setBigClientPrice(BigDecimal bigClientPrice) { this.bigClientPrice = bigClientPrice; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "product_units")
|
||||
public class ProductUnit {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "shop_id", nullable = false)
|
||||
private Long shopId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 16)
|
||||
private String name;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
public LocalDateTime getDeletedAt() { return deletedAt; }
|
||||
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.demo.product.repo;
|
||||
|
||||
import com.example.demo.product.entity.ProductCategory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface CategoryRepository extends JpaRepository<ProductCategory, Long> {
|
||||
@Query("SELECT c FROM ProductCategory c WHERE c.shopId = :shopId AND c.deletedAt IS NULL ORDER BY c.sortOrder ASC, c.id DESC")
|
||||
List<ProductCategory> listByShop(@Param("shopId") Long shopId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.demo.product.repo;
|
||||
|
||||
import com.example.demo.product.entity.Inventory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface InventoryRepository extends JpaRepository<Inventory, Long> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.demo.product.repo;
|
||||
|
||||
import com.example.demo.product.entity.ProductImage;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ProductImageRepository extends JpaRepository<ProductImage, Long> {
|
||||
List<ProductImage> findByProductIdOrderBySortOrderAscIdAsc(Long productId);
|
||||
void deleteByProductId(Long productId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.demo.product.repo;
|
||||
|
||||
import com.example.demo.product.entity.ProductPrice;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ProductPriceRepository extends JpaRepository<ProductPrice, Long> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.demo.product.repo;
|
||||
|
||||
import com.example.demo.product.entity.Product;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface ProductRepository extends JpaRepository<Product, Long> {
|
||||
|
||||
@Query("SELECT p FROM Product p WHERE p.shopId = :shopId AND (p.deletedAt IS NULL) AND " +
|
||||
"(:kw IS NULL OR :kw = '' OR p.name LIKE CONCAT('%', :kw, '%') OR p.brand LIKE CONCAT('%', :kw, '%') OR p.model LIKE CONCAT('%', :kw, '%') OR p.spec LIKE CONCAT('%', :kw, '%') OR p.barcode LIKE CONCAT('%', :kw, '%')) AND " +
|
||||
"(:categoryId IS NULL OR p.categoryId = :categoryId) ORDER BY p.id DESC")
|
||||
Page<Product> search(@Param("shopId") Long shopId,
|
||||
@Param("kw") String kw,
|
||||
@Param("categoryId") Long categoryId,
|
||||
Pageable pageable);
|
||||
|
||||
boolean existsByShopIdAndBarcode(Long shopId, String barcode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.example.demo.product.repo;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface UnitRepository extends JpaRepository<com.example.demo.product.entity.ProductUnit, Long> {
|
||||
@Query("SELECT u FROM ProductUnit u WHERE u.shopId = :shopId AND u.deletedAt IS NULL ORDER BY u.id DESC")
|
||||
List<com.example.demo.product.entity.ProductUnit> listByShop(@Param("shopId") Long shopId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
package com.example.demo.product.service;
|
||||
|
||||
import com.example.demo.product.dto.ProductDtos;
|
||||
import com.example.demo.product.entity.Inventory;
|
||||
import com.example.demo.product.entity.Product;
|
||||
import com.example.demo.product.entity.ProductImage;
|
||||
import com.example.demo.product.entity.ProductPrice;
|
||||
import com.example.demo.product.repo.InventoryRepository;
|
||||
import com.example.demo.product.repo.ProductImageRepository;
|
||||
import com.example.demo.product.repo.ProductPriceRepository;
|
||||
import com.example.demo.product.repo.ProductRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class ProductService {
|
||||
|
||||
private final ProductRepository productRepository;
|
||||
private final ProductPriceRepository priceRepository;
|
||||
private final InventoryRepository inventoryRepository;
|
||||
private final ProductImageRepository imageRepository;
|
||||
|
||||
public ProductService(ProductRepository productRepository,
|
||||
ProductPriceRepository priceRepository,
|
||||
InventoryRepository inventoryRepository,
|
||||
ProductImageRepository imageRepository) {
|
||||
this.productRepository = productRepository;
|
||||
this.priceRepository = priceRepository;
|
||||
this.inventoryRepository = inventoryRepository;
|
||||
this.imageRepository = imageRepository;
|
||||
}
|
||||
|
||||
public Page<ProductDtos.ProductListItem> search(Long shopId, String kw, Long categoryId, int page, int size) {
|
||||
Page<Product> p = productRepository.search(shopId, kw, categoryId, PageRequest.of(page, size));
|
||||
return p.map(prod -> {
|
||||
ProductDtos.ProductListItem it = new ProductDtos.ProductListItem();
|
||||
it.id = prod.getId();
|
||||
it.name = prod.getName();
|
||||
it.brand = prod.getBrand();
|
||||
it.model = prod.getModel();
|
||||
it.spec = prod.getSpec();
|
||||
// stock
|
||||
inventoryRepository.findById(prod.getId()).ifPresent(inv -> it.stock = inv.getQuantity());
|
||||
// price
|
||||
priceRepository.findById(prod.getId()).ifPresent(pr -> it.retailPrice = pr.getRetailPrice());
|
||||
// cover
|
||||
List<ProductImage> imgs = imageRepository.findByProductIdOrderBySortOrderAscIdAsc(prod.getId());
|
||||
it.cover = imgs.isEmpty() ? null : imgs.get(0).getUrl();
|
||||
return it;
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<ProductDtos.ProductDetail> findDetail(Long id) {
|
||||
Optional<Product> op = productRepository.findById(id);
|
||||
if (op.isEmpty()) return Optional.empty();
|
||||
Product p = op.get();
|
||||
ProductDtos.ProductDetail d = new ProductDtos.ProductDetail();
|
||||
d.id = p.getId();
|
||||
d.name = p.getName();
|
||||
d.barcode = p.getBarcode();
|
||||
d.brand = p.getBrand();
|
||||
d.model = p.getModel();
|
||||
d.spec = p.getSpec();
|
||||
d.origin = p.getOrigin();
|
||||
d.categoryId = p.getCategoryId();
|
||||
d.unitId = p.getUnitId();
|
||||
d.safeMin = p.getSafeMin();
|
||||
d.safeMax = p.getSafeMax();
|
||||
inventoryRepository.findById(p.getId()).ifPresent(inv -> d.stock = inv.getQuantity());
|
||||
priceRepository.findById(p.getId()).ifPresent(pr -> {
|
||||
d.purchasePrice = pr.getPurchasePrice();
|
||||
d.retailPrice = pr.getRetailPrice();
|
||||
d.distributionPrice = pr.getDistributionPrice();
|
||||
d.wholesalePrice = pr.getWholesalePrice();
|
||||
d.bigClientPrice = pr.getBigClientPrice();
|
||||
});
|
||||
List<ProductImage> imgs = imageRepository.findByProductIdOrderBySortOrderAscIdAsc(p.getId());
|
||||
List<ProductDtos.Image> list = new ArrayList<>();
|
||||
for (ProductImage img : imgs) {
|
||||
ProductDtos.Image i = new ProductDtos.Image();
|
||||
i.url = img.getUrl();
|
||||
list.add(i);
|
||||
}
|
||||
d.images = list;
|
||||
return Optional.of(d);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long create(Long shopId, Long userId, ProductDtos.CreateOrUpdateProductRequest req) {
|
||||
validate(shopId, req);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
Product p = new Product();
|
||||
p.setShopId(shopId);
|
||||
p.setUserId(userId);
|
||||
p.setName(req.name);
|
||||
p.setBarcode(emptyToNull(req.barcode));
|
||||
p.setBrand(emptyToNull(req.brand));
|
||||
p.setModel(emptyToNull(req.model));
|
||||
p.setSpec(emptyToNull(req.spec));
|
||||
p.setOrigin(emptyToNull(req.origin));
|
||||
p.setCategoryId(req.categoryId);
|
||||
p.setUnitId(req.unitId);
|
||||
p.setSafeMin(req.safeMin);
|
||||
p.setSafeMax(req.safeMax);
|
||||
p.setDescription(emptyToNull(req.remark));
|
||||
p.setCreatedAt(now);
|
||||
p.setUpdatedAt(now);
|
||||
productRepository.save(p);
|
||||
|
||||
upsertPrice(userId, now, p.getId(), shopId, req.prices);
|
||||
upsertInventory(userId, now, p.getId(), shopId, req.stock);
|
||||
syncImages(userId, p.getId(), shopId, req.images);
|
||||
|
||||
return p.getId();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void update(Long id, Long shopId, Long userId, ProductDtos.CreateOrUpdateProductRequest req) {
|
||||
validate(shopId, req);
|
||||
Product p = productRepository.findById(id).orElseThrow();
|
||||
if (!p.getShopId().equals(shopId)) throw new IllegalArgumentException("跨店铺数据");
|
||||
p.setUserId(userId);
|
||||
p.setName(req.name);
|
||||
p.setBarcode(emptyToNull(req.barcode));
|
||||
p.setBrand(emptyToNull(req.brand));
|
||||
p.setModel(emptyToNull(req.model));
|
||||
p.setSpec(emptyToNull(req.spec));
|
||||
p.setOrigin(emptyToNull(req.origin));
|
||||
p.setCategoryId(req.categoryId);
|
||||
p.setUnitId(req.unitId);
|
||||
p.setSafeMin(req.safeMin);
|
||||
p.setSafeMax(req.safeMax);
|
||||
p.setDescription(emptyToNull(req.remark));
|
||||
p.setUpdatedAt(LocalDateTime.now());
|
||||
productRepository.save(p);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
upsertPrice(userId, now, p.getId(), shopId, req.prices);
|
||||
upsertInventory(userId, now, p.getId(), shopId, req.stock);
|
||||
syncImages(userId, p.getId(), shopId, req.images);
|
||||
}
|
||||
|
||||
private void validate(Long shopId, ProductDtos.CreateOrUpdateProductRequest req) {
|
||||
if (req.name == null || req.name.isBlank()) throw new IllegalArgumentException("name必填");
|
||||
if (req.unitId == null) throw new IllegalArgumentException("unitId必填");
|
||||
if (req.safeMin != null && req.safeMax != null) {
|
||||
if (req.safeMin.compareTo(req.safeMax) > 0) throw new IllegalArgumentException("安全库存区间不合法");
|
||||
}
|
||||
if (req.barcode != null && !req.barcode.isBlank()) {
|
||||
if (productRepository.existsByShopIdAndBarcode(shopId, req.barcode)) {
|
||||
// 更新时允许自己相同:由Controller层在调用前判定并跳过;简化此处逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void upsertPrice(Long userId, LocalDateTime now, Long productId, Long shopId, ProductDtos.Prices prices) {
|
||||
if (prices == null) prices = new ProductDtos.Prices();
|
||||
java.util.Optional<ProductPrice> existed = priceRepository.findById(productId);
|
||||
ProductPrice pr = existed.orElseGet(ProductPrice::new);
|
||||
pr.setProductId(productId);
|
||||
pr.setShopId(shopId);
|
||||
pr.setUserId(userId);
|
||||
pr.setPurchasePrice(nvl(prices.purchasePrice, BigDecimal.ZERO));
|
||||
pr.setRetailPrice(nvl(prices.retailPrice, BigDecimal.ZERO));
|
||||
// 前端不再传分销价:仅当入参提供时更新;新建记录若未提供则置 0
|
||||
if (prices.distributionPrice != null) {
|
||||
pr.setDistributionPrice(prices.distributionPrice);
|
||||
} else if (existed.isEmpty()) {
|
||||
pr.setDistributionPrice(BigDecimal.ZERO);
|
||||
}
|
||||
pr.setWholesalePrice(nvl(prices.wholesalePrice, BigDecimal.ZERO));
|
||||
pr.setBigClientPrice(nvl(prices.bigClientPrice, BigDecimal.ZERO));
|
||||
pr.setUpdatedAt(now);
|
||||
priceRepository.save(pr);
|
||||
}
|
||||
|
||||
private void upsertInventory(Long userId, LocalDateTime now, Long productId, Long shopId, BigDecimal stock) {
|
||||
Inventory inv = inventoryRepository.findById(productId).orElseGet(Inventory::new);
|
||||
inv.setProductId(productId);
|
||||
inv.setShopId(shopId);
|
||||
inv.setUserId(userId);
|
||||
inv.setQuantity(nvl(stock, BigDecimal.ZERO));
|
||||
inv.setUpdatedAt(now);
|
||||
inventoryRepository.save(inv);
|
||||
}
|
||||
|
||||
private void syncImages(Long userId, Long productId, Long shopId, java.util.List<String> images) {
|
||||
imageRepository.deleteByProductId(productId);
|
||||
if (images == null) return;
|
||||
int idx = 0;
|
||||
for (String url : images) {
|
||||
if (url == null || url.isBlank()) continue;
|
||||
ProductImage img = new ProductImage();
|
||||
img.setShopId(shopId);
|
||||
img.setUserId(userId);
|
||||
img.setProductId(productId);
|
||||
img.setUrl(url);
|
||||
img.setSortOrder(idx++);
|
||||
imageRepository.save(img);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T nvl(T v, T def) { return v != null ? v : def; }
|
||||
private static String emptyToNull(String s) { return (s == null || s.isBlank()) ? null : s; }
|
||||
}
|
||||
|
||||
|
||||
@@ -23,3 +23,13 @@ spring.datasource.hikari.idle-timeout=300000
|
||||
spring.datasource.hikari.max-lifetime=600000
|
||||
spring.datasource.hikari.keepalive-time=300000
|
||||
spring.datasource.hikari.connection-timeout=30000
|
||||
|
||||
# 附件占位图配置(使用环境变量注入路径)
|
||||
# WINDOWS 示例: setx ATTACHMENTS_PLACEHOLDER_IMAGE "C:\\Users\\21826\\Desktop\\Wj\\PartsInquiry\\backend\\picture\\屏幕截图 2025-08-14 134657.png"
|
||||
# LINUX/Mac 示例: export ATTACHMENTS_PLACEHOLDER_IMAGE=/path/to/placeholder.png
|
||||
attachments.placeholder.image-path=${ATTACHMENTS_PLACEHOLDER_IMAGE}
|
||||
attachments.placeholder.url-path=${ATTACHMENTS_PLACEHOLDER_URL:/api/attachments/placeholder}
|
||||
|
||||
# 应用默认上下文(用于开发/演示环境)
|
||||
app.defaults.shop-id=${APP_DEFAULT_SHOP_ID:1}
|
||||
app.defaults.user-id=${APP_DEFAULT_USER_ID:2}
|
||||
|
||||
Reference in New Issue
Block a user