9.18王德鹏/1

This commit is contained in:
2025-09-18 14:22:04 +08:00
parent a8dcee7296
commit 335e21347b
90 changed files with 1618 additions and 1346 deletions

View File

@@ -71,3 +71,6 @@ public class AttachmentController {

View File

@@ -30,3 +30,6 @@ public class AttachmentPlaceholderProperties {

View File

@@ -19,3 +19,6 @@ public class AppDefaultsProperties {

View File

@@ -0,0 +1,118 @@
package com.example.demo.order;
import com.example.demo.common.AppDefaultsProperties;
import com.example.demo.order.dto.OrderDtos;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class OrderController {
private final OrderService orderService;
private final AppDefaultsProperties defaults;
public OrderController(OrderService orderService, AppDefaultsProperties defaults) {
this.orderService = orderService;
this.defaults = defaults;
}
@PostMapping("/orders")
public ResponseEntity<?> createOrder(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestHeader(name = "X-User-Id", required = false) Long userId,
@RequestBody OrderDtos.CreateOrderRequest req) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Long uid = (userId == null ? defaults.getUserId() : userId);
return ResponseEntity.ok(orderService.create(sid, uid, req));
}
@PostMapping("/payments/{biz}")
public ResponseEntity<?> createPayments(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestHeader(name = "X-User-Id", required = false) Long userId,
@PathVariable("biz") String biz,
@RequestBody java.util.List<OrderDtos.PaymentItem> req) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Long uid = (userId == null ? defaults.getUserId() : userId);
return ResponseEntity.ok(orderService.createPayments(sid, uid, req, biz));
}
@PostMapping("/orders/{id}/void")
public ResponseEntity<?> voidOrder(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestHeader(name = "X-User-Id", required = false) Long userId,
@PathVariable("id") Long id,
@RequestParam("type") String type) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Long uid = (userId == null ? defaults.getUserId() : userId);
orderService.voidOrder(sid, uid, id, type);
return ResponseEntity.ok().build();
}
@GetMapping("/orders")
public ResponseEntity<?> list(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestParam(name = "biz", required = false) String biz,
@RequestParam(name = "type", required = false) String type,
@RequestParam(name = "kw", required = false) String kw,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "20") int size,
@RequestParam(name = "startDate", required = false) String startDate,
@RequestParam(name = "endDate", required = false) String endDate) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
return ResponseEntity.ok(orderService.list(sid, biz, type, kw, Math.max(0, page-1), size, startDate, endDate));
}
// 兼容前端直接调用 /api/purchase-orders
@GetMapping("/purchase-orders")
public ResponseEntity<?> purchaseOrders(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "kw", required = false) String kw,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "20") int size,
@RequestParam(name = "startDate", required = false) String startDate,
@RequestParam(name = "endDate", required = false) String endDate) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
String type = ("returned".equalsIgnoreCase(status) ? "purchase.return" : "purchase.in");
return ResponseEntity.ok(orderService.list(sid, "purchase", type, kw, Math.max(0, page-1), size, startDate, endDate));
}
@GetMapping("/payments")
public ResponseEntity<?> listPayments(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestParam(name = "direction", required = false) String direction,
@RequestParam(name = "bizType", required = false) String bizType,
@RequestParam(name = "accountId", required = false) Long accountId,
@RequestParam(name = "kw", required = false) String kw,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "20") int size,
@RequestParam(name = "startDate", required = false) String startDate,
@RequestParam(name = "endDate", required = false) String endDate) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
return ResponseEntity.ok(orderService.listPayments(sid, direction, bizType, accountId, kw, Math.max(0, page-1), size, startDate, endDate));
}
@GetMapping("/other-transactions")
public ResponseEntity<?> listOtherTransactions(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestParam(name = "type", required = false) String type,
@RequestParam(name = "accountId", required = false) Long accountId,
@RequestParam(name = "kw", required = false) String kw,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "20") int size,
@RequestParam(name = "startDate", required = false) String startDate,
@RequestParam(name = "endDate", required = false) String endDate) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
return ResponseEntity.ok(orderService.listOtherTransactions(sid, type, accountId, kw, Math.max(0, page-1), size, startDate, endDate));
}
@GetMapping("/inventories/logs")
public ResponseEntity<?> listInventoryLogs(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestParam(name = "productId", required = false) Long productId,
@RequestParam(name = "reason", required = false) String reason,
@RequestParam(name = "kw", required = false) String kw,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "20") int size,
@RequestParam(name = "startDate", required = false) String startDate,
@RequestParam(name = "endDate", required = false) String endDate) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
return ResponseEntity.ok(orderService.listInventoryLogs(sid, productId, reason, kw, Math.max(0, page-1), size, startDate, endDate));
}
}

View File

@@ -0,0 +1,24 @@
package com.example.demo.order;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 简单的进程内单号生成器:前缀 + yyyyMMdd + 4位流水
* 说明:演示用,生产建议使用数据库序列或 Redis 自增确保多实例唯一。
*/
public class OrderNumberGenerator {
private static final ConcurrentHashMap<String, AtomicInteger> dateCounters = new ConcurrentHashMap<>();
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
public static String next(String prefix) {
String day = LocalDateTime.now().format(DATE);
String key = prefix + day;
int seq = dateCounters.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet();
return prefix + day + String.format("%04d", seq);
}
}

View File

@@ -0,0 +1,353 @@
package com.example.demo.order;
import com.example.demo.order.dto.OrderDtos;
import com.example.demo.product.entity.Inventory;
import com.example.demo.product.repo.InventoryRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
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;
@Service
public class OrderService {
private final InventoryRepository inventoryRepository;
private final JdbcTemplate jdbcTemplate;
public OrderService(InventoryRepository inventoryRepository,
JdbcTemplate jdbcTemplate) {
this.inventoryRepository = inventoryRepository;
this.jdbcTemplate = jdbcTemplate;
}
@Transactional
public Object create(Long shopId, Long userId, OrderDtos.CreateOrderRequest req) {
String type = req.type == null ? "" : req.type;
boolean isSaleOut = "sale.out".equals(type) || "out".equals(type) || "sale".equals(type);
boolean isPurchaseIn = "purchase.in".equals(type) || "in".equals(type);
boolean isSaleReturn = "sale.return".equals(type);
boolean isPurchaseReturn = "purchase.return".equals(type);
boolean isSaleCollect = "sale.collect".equals(type);
boolean isPurchasePay = "purchase.pay".equals(type);
if (isSaleCollect || isPurchasePay) {
java.util.List<OrderDtos.PaymentItem> payments = req.payments == null ? java.util.List.of() : req.payments;
return createPayments(shopId, userId, payments, isSaleCollect ? "sale" : "purchase");
}
if (!(isSaleOut || isPurchaseIn || isSaleReturn || isPurchaseReturn)) throw new IllegalArgumentException("不支持的type");
if (req.items == null || req.items.isEmpty()) throw new IllegalArgumentException("明细为空");
// 后端重算金额
final BigDecimal[] totalRef = new BigDecimal[]{BigDecimal.ZERO};
for (OrderDtos.Item it : req.items) {
BigDecimal qty = n(it.quantity);
BigDecimal price = n(it.unitPrice);
BigDecimal dr = n(it.discountRate);
BigDecimal line = qty.multiply(price).multiply(BigDecimal.ONE.subtract(dr.divide(new BigDecimal("100"))));
totalRef[0] = totalRef[0].add(scale2(line));
}
// 库存变动(保存即 approved
LocalDateTime now = nowUtc();
for (OrderDtos.Item it : req.items) {
Long pid = it.productId;
Inventory inv = inventoryRepository.findById(pid).orElseGet(Inventory::new);
inv.setProductId(pid);
inv.setShopId(shopId);
inv.setUserId(userId);
BigDecimal cur = n(inv.getQuantity());
BigDecimal delta = BigDecimal.ZERO;
if (isSaleOut) delta = n(it.quantity).negate();
if (isPurchaseIn) delta = n(it.quantity);
if (isSaleReturn) delta = n(it.quantity); // 退货入库
if (isPurchaseReturn) delta = n(it.quantity).negate(); // 退货出库
BigDecimal next = cur.add(delta);
if (isSaleOut && next.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalStateException("库存不足");
}
inv.setQuantity(next);
inv.setUpdatedAt(now);
inventoryRepository.save(inv);
// 写入库存流水(可选金额)
String imSql = "INSERT INTO inventory_movements (shop_id,user_id,product_id,source_type,source_id,qty_delta,amount_delta,reason,tx_time,remark,created_at) VALUES (?,?,?,?,?,?,?,?,?,NULL,NOW())";
String sourceType = isSaleOut ? "sale" : (isPurchaseIn ? "purchase" : (isSaleReturn ? "sale_return" : "purchase_return"));
jdbcTemplate.update(imSql, shopId, userId, pid, sourceType, null, delta, null, null, java.sql.Timestamp.from(now.atZone(java.time.ZoneOffset.UTC).toInstant()));
}
String prefix = isSaleOut || isSaleReturn ? (isSaleReturn ? "SR" : "SO") : (isPurchaseReturn ? "PR" : "PO");
String orderNo = OrderNumberGenerator.next(prefix);
// 持久化订单头与明细(简化使用 JDBC
String headTable = isSaleOut ? "sales_orders" : (isPurchaseIn ? "purchase_orders" : (isSaleReturn ? "sales_return_orders" : "purchase_return_orders"));
String itemTable = isSaleOut ? "sales_order_items" : (isPurchaseIn ? "purchase_order_items" : (isSaleReturn ? "sales_return_order_items" : "purchase_return_order_items"));
// insert head
KeyHolder kh = new GeneratedKeyHolder();
String headSql = "INSERT INTO " + headTable + " (shop_id,user_id,customer_id,supplier_id,order_no,order_time,status,amount,paid_amount,remark,created_at,updated_at) " +
"VALUES (?,?,?,?,?,?, 'approved', ?, 0, ?, NOW(), NOW())";
Long customerId = req.customerId;
Long supplierId = req.supplierId;
jdbcTemplate.update(con -> {
java.sql.PreparedStatement ps = con.prepareStatement(headSql, new String[]{"id"});
ps.setLong(1, shopId);
ps.setLong(2, userId);
if (headTable.startsWith("sales")) {
ps.setObject(3, customerId, java.sql.Types.BIGINT);
ps.setObject(4, null);
} else if (headTable.startsWith("purchase")) {
ps.setObject(3, null);
ps.setObject(4, supplierId, java.sql.Types.BIGINT);
} else {
ps.setObject(3, null);
ps.setObject(4, null);
}
ps.setString(5, orderNo);
ps.setTimestamp(6, java.sql.Timestamp.from(now.atZone(java.time.ZoneOffset.UTC).toInstant()));
ps.setBigDecimal(7, scale2(totalRef[0]));
ps.setString(8, req.remark);
return ps;
}, kh);
Number orderKey = kh.getKey();
Long orderId = (orderKey == null ? null : orderKey.longValue());
// insert items
String itemSql = "INSERT INTO " + itemTable + " (order_id,product_id,quantity,unit_price,discount_rate,amount) VALUES (?,?,?,?,?,?)";
for (OrderDtos.Item it : req.items) {
BigDecimal qty = n(it.quantity);
BigDecimal price = n(it.unitPrice);
BigDecimal dr = n(it.discountRate);
BigDecimal line = scale2(qty.multiply(price).multiply(BigDecimal.ONE.subtract(dr.divide(new BigDecimal("100")))));
jdbcTemplate.update(itemSql, orderId, it.productId, qty, price, dr, line);
}
return new OrderDtos.CreateOrderResponse(orderId, orderNo);
}
@Transactional
public OrderDtos.CreatePaymentsResponse createPayments(Long shopId, Long userId, java.util.List<OrderDtos.PaymentItem> req, String bizType) {
ensureDefaultAccounts(shopId, userId);
List<Long> ids = new ArrayList<>();
if (req == null) return new OrderDtos.CreatePaymentsResponse(ids);
String direction = "sale".equals(bizType) ? "in" : "out";
for (OrderDtos.PaymentItem p : req) {
Long accountId = resolveAccountId(shopId, userId, p.method);
KeyHolder kh = new GeneratedKeyHolder();
String sql = "INSERT INTO payments (shop_id,user_id,biz_type,biz_id,account_id,direction,amount,pay_time,remark,created_at) " +
"VALUES (?,?,?,?,?,?,?,NOW(),NULL,NOW())";
jdbcTemplate.update(con -> {
java.sql.PreparedStatement ps = con.prepareStatement(sql, new String[]{"id"});
ps.setLong(1, shopId);
ps.setLong(2, userId);
ps.setString(3, bizType);
if (p.orderId == null) ps.setNull(4, java.sql.Types.BIGINT); else ps.setLong(4, p.orderId);
ps.setLong(5, accountId);
ps.setString(6, direction);
ps.setBigDecimal(7, n(p.amount));
return ps;
}, kh);
Number payKey = kh.getKey();
Long pid = (payKey == null ? null : payKey.longValue());
if (pid != null) ids.add(pid);
// 若挂单,累加已付
if (p.orderId != null) {
String table = "sale".equals(bizType) ? "sales_orders" : "purchase_orders";
jdbcTemplate.update("UPDATE " + table + " SET paid_amount = paid_amount + ? WHERE id = ?", n(p.amount), p.orderId);
}
}
return new OrderDtos.CreatePaymentsResponse(ids);
}
@Transactional
public void voidOrder(Long shopId, Long userId, Long id, String type) {
// type: sale.out / purchase.in / sale.return / purchase.return
String headTable;
String itemTable;
boolean revertIncrease; // true 表示作废时库存应减少原先增加false 表示应增加(原先减少)
if ("sale.out".equals(type)) { headTable = "sales_orders"; itemTable = "sales_order_items"; revertIncrease = false; }
else if ("purchase.in".equals(type)) { headTable = "purchase_orders"; itemTable = "purchase_order_items"; revertIncrease = true; }
else if ("sale.return".equals(type)) { headTable = "sales_return_orders"; itemTable = "sales_return_order_items"; revertIncrease = true; }
else if ("purchase.return".equals(type)) { headTable = "purchase_return_orders"; itemTable = "purchase_return_order_items"; revertIncrease = false; }
else throw new IllegalArgumentException("不支持的type");
// 查询明细
List<java.util.Map<String,Object>> rows = jdbcTemplate.queryForList("SELECT product_id, quantity FROM " + itemTable + " WHERE order_id = ?", id);
// 回滚库存
LocalDateTime now = nowUtc();
for (java.util.Map<String,Object> r : rows) {
Long pid = ((Number)r.get("product_id")).longValue();
java.math.BigDecimal qty = new java.math.BigDecimal(r.get("quantity").toString());
Inventory inv = inventoryRepository.findById(pid).orElseGet(Inventory::new);
inv.setProductId(pid);
inv.setShopId(shopId);
inv.setUserId(userId);
java.math.BigDecimal delta = revertIncrease ? qty.negate() : qty; // 与创建时相反
inv.setQuantity(n(inv.getQuantity()).add(delta));
inv.setUpdatedAt(now);
inventoryRepository.save(inv);
}
// 更新状态
jdbcTemplate.update("UPDATE " + headTable + " SET status='void' WHERE id = ?", id);
}
public java.util.Map<String,Object> list(Long shopId, String biz, String type, String kw, int page, int size, String startDate, String endDate) {
String headTable;
if ("sale".equals(biz)) {
headTable = ("sale.return".equals(type) ? "sales_return_orders" : "sales_orders");
} else if ("purchase".equals(biz)) {
headTable = ("purchase.return".equals(type) ? "purchase_orders" : "purchase_orders");
} else {
// 若未传,默认销售出货
headTable = "sales_orders";
}
StringBuilder sql = new StringBuilder("SELECT id, order_no, order_time, amount FROM " + headTable + " WHERE shop_id=?");
java.util.List<Object> ps = new java.util.ArrayList<>();
ps.add(shopId);
if ("purchase".equals(biz) && "purchase.return".equals(type)) {
sql.append(" AND status='returned'");
}
if (kw != null && !kw.isBlank()) {
sql.append(" AND (order_no LIKE ?)");
ps.add('%' + kw + '%');
}
if (startDate != null && !startDate.isBlank()) { sql.append(" AND order_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sql.append(" AND order_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
sql.append(" ORDER BY order_time DESC LIMIT ? OFFSET ?");
ps.add(size);
ps.add(page * size);
java.util.List<java.util.Map<String,Object>> list = jdbcTemplate.queryForList(sql.toString(), ps.toArray());
// 汇总
StringBuilder sumSql = new StringBuilder("SELECT COALESCE(SUM(amount),0) FROM " + headTable + " WHERE shop_id=?");
java.util.List<Object> sumPs = new java.util.ArrayList<>();
sumPs.add(shopId);
if ("purchase".equals(biz) && "purchase.return".equals(type)) { sumSql.append(" AND status='returned'"); }
if (kw != null && !kw.isBlank()) { sumSql.append(" AND (order_no LIKE ?)"); sumPs.add('%' + kw + '%'); }
if (startDate != null && !startDate.isBlank()) { sumSql.append(" AND order_time>=?"); sumPs.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sumSql.append(" AND order_time<=?"); sumPs.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
java.math.BigDecimal total = jdbcTemplate.queryForObject(sumSql.toString(), java.math.BigDecimal.class, sumPs.toArray());
java.util.Map<String,Object> resp = new java.util.HashMap<>();
resp.put("list", list);
resp.put("totalAmount", total == null ? java.math.BigDecimal.ZERO : total);
return resp;
}
public java.util.Map<String,Object> listPayments(Long shopId, String direction, String bizType, Long accountId, String kw, int page, int size, String startDate, String endDate) {
StringBuilder sql = new StringBuilder("SELECT id, biz_type, account_id, direction, amount, pay_time FROM payments WHERE shop_id=?");
java.util.List<Object> ps = new java.util.ArrayList<>();
ps.add(shopId);
if (direction != null && !direction.isBlank()) { sql.append(" AND direction=?"); ps.add(direction); }
if (bizType != null && !bizType.isBlank()) { sql.append(" AND biz_type=?"); ps.add(bizType); }
if (accountId != null) { sql.append(" AND account_id=?"); ps.add(accountId); }
if (kw != null && !kw.isBlank()) { sql.append(" AND (CAST(id AS CHAR) LIKE ?)"); ps.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sql.append(" AND pay_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sql.append(" AND pay_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
sql.append(" ORDER BY pay_time DESC LIMIT ? OFFSET ?");
ps.add(size); ps.add(page * size);
java.util.List<java.util.Map<String,Object>> list = jdbcTemplate.queryForList(sql.toString(), ps.toArray());
StringBuilder sumSql = new StringBuilder("SELECT COALESCE(SUM(amount),0) FROM payments WHERE shop_id=?");
java.util.List<Object> sumPs = new java.util.ArrayList<>(); sumPs.add(shopId);
if (direction != null && !direction.isBlank()) { sumSql.append(" AND direction=?"); sumPs.add(direction); }
if (bizType != null && !bizType.isBlank()) { sumSql.append(" AND biz_type=?"); sumPs.add(bizType); }
if (accountId != null) { sumSql.append(" AND account_id=?"); sumPs.add(accountId); }
if (kw != null && !kw.isBlank()) { sumSql.append(" AND (CAST(id AS CHAR) LIKE ?)"); sumPs.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sumSql.append(" AND pay_time>=?"); sumPs.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sumSql.append(" AND pay_time<=?"); sumPs.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
java.math.BigDecimal total = jdbcTemplate.queryForObject(sumSql.toString(), java.math.BigDecimal.class, sumPs.toArray());
java.util.Map<String,Object> resp = new java.util.HashMap<>(); resp.put("list", list); resp.put("totalAmount", total == null ? java.math.BigDecimal.ZERO : total); return resp;
}
public java.util.Map<String,Object> listOtherTransactions(Long shopId, String type, Long accountId, String kw, int page, int size, String startDate, String endDate) {
StringBuilder sql = new StringBuilder("SELECT id, `type`, account_id, amount, tx_time, remark FROM other_transactions WHERE shop_id=?");
java.util.List<Object> ps = new java.util.ArrayList<>(); ps.add(shopId);
if (type != null && !type.isBlank()) { sql.append(" AND `type`=?"); ps.add(type); }
if (accountId != null) { sql.append(" AND account_id=?"); ps.add(accountId); }
if (kw != null && !kw.isBlank()) { sql.append(" AND (remark LIKE ? OR category LIKE ?)"); ps.add('%'+kw+'%'); ps.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sql.append(" AND tx_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sql.append(" AND tx_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
sql.append(" ORDER BY tx_time DESC LIMIT ? OFFSET ?"); ps.add(size); ps.add(page * size);
java.util.List<java.util.Map<String,Object>> list = jdbcTemplate.queryForList(sql.toString(), ps.toArray());
StringBuilder sumSql = new StringBuilder("SELECT COALESCE(SUM(amount),0) FROM other_transactions WHERE shop_id=?");
java.util.List<Object> sumPs = new java.util.ArrayList<>(); sumPs.add(shopId);
if (type != null && !type.isBlank()) { sumSql.append(" AND `type`=?"); sumPs.add(type); }
if (accountId != null) { sumSql.append(" AND account_id=?"); sumPs.add(accountId); }
if (kw != null && !kw.isBlank()) { sumSql.append(" AND (remark LIKE ? OR category LIKE ?)"); sumPs.add('%'+kw+'%'); sumPs.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sumSql.append(" AND tx_time>=?"); sumPs.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sumSql.append(" AND tx_time<=?"); sumPs.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
java.math.BigDecimal total = jdbcTemplate.queryForObject(sumSql.toString(), java.math.BigDecimal.class, sumPs.toArray());
java.util.Map<String,Object> resp = new java.util.HashMap<>(); resp.put("list", list); resp.put("totalAmount", total == null ? java.math.BigDecimal.ZERO : total); return resp;
}
public java.util.Map<String,Object> listInventoryLogs(Long shopId, Long productId, String reason, String kw, int page, int size, String startDate, String endDate) {
StringBuilder sql = new StringBuilder("SELECT id, product_id, qty_delta, amount_delta, reason, tx_time FROM inventory_movements WHERE shop_id=?");
java.util.List<Object> ps = new java.util.ArrayList<>(); ps.add(shopId);
if (productId != null) { sql.append(" AND product_id=?"); ps.add(productId); }
if (reason != null && !reason.isBlank()) { sql.append(" AND reason=?"); ps.add(reason); }
if (kw != null && !kw.isBlank()) { sql.append(" AND (remark LIKE ? OR source_type LIKE ?)"); ps.add('%'+kw+'%'); ps.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sql.append(" AND tx_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sql.append(" AND tx_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
sql.append(" ORDER BY tx_time DESC LIMIT ? OFFSET ?"); ps.add(size); ps.add(page * size);
java.util.List<java.util.Map<String,Object>> list = jdbcTemplate.queryForList(sql.toString(), ps.toArray());
StringBuilder sumSql = new StringBuilder("SELECT COALESCE(SUM(COALESCE(amount_delta,0)),0) FROM inventory_movements WHERE shop_id=?");
java.util.List<Object> sumPs = new java.util.ArrayList<>(); sumPs.add(shopId);
if (productId != null) { sumSql.append(" AND product_id=?"); sumPs.add(productId); }
if (reason != null && !reason.isBlank()) { sumSql.append(" AND reason=?"); sumPs.add(reason); }
if (kw != null && !kw.isBlank()) { sumSql.append(" AND (remark LIKE ? OR source_type LIKE ?)"); sumPs.add('%'+kw+'%'); sumPs.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sumSql.append(" AND tx_time>=?"); sumPs.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sumSql.append(" AND tx_time<=?"); sumPs.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
java.math.BigDecimal total = jdbcTemplate.queryForObject(sumSql.toString(), java.math.BigDecimal.class, sumPs.toArray());
java.util.Map<String,Object> resp = new java.util.HashMap<>(); resp.put("list", list); resp.put("totalAmount", total == null ? java.math.BigDecimal.ZERO : total); return resp;
}
private static BigDecimal n(BigDecimal v) { return v == null ? BigDecimal.ZERO : v; }
private static BigDecimal scale2(BigDecimal v) { return v.setScale(2, java.math.RoundingMode.HALF_UP); }
private static LocalDateTime nowUtc() { return LocalDateTime.now(java.time.Clock.systemUTC()); }
private void ensureDefaultAccounts(Long shopId, Long userId) {
// 为 cash/bank/wechat 分别确保存在一条账户记录;按 type→name 顺序检查,避免同名唯一冲突
ensureAccount(shopId, userId, "cash", "现金");
ensureAccount(shopId, userId, "bank", "银行存款");
ensureAccount(shopId, userId, "wechat", "微信");
}
private void ensureAccount(Long shopId, Long userId, String type, String name) {
List<Long> byType = jdbcTemplate.query("SELECT id FROM accounts WHERE shop_id=? AND type=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, type);
if (!byType.isEmpty()) return;
List<Long> byName = jdbcTemplate.query("SELECT id FROM accounts WHERE shop_id=? AND name=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, name);
if (!byName.isEmpty()) return; // 已有同名则直接复用,无需再插
jdbcTemplate.update("INSERT INTO accounts (shop_id,user_id,name,type,balance,status,created_at,updated_at) VALUES (?,?,?,?,0,1,NOW(),NOW())",
shopId, userId, name, type);
}
private Long resolveAccountId(Long shopId, Long userId, String method) {
String type = "cash";
if ("bank".equalsIgnoreCase(method)) type = "bank";
if ("wechat".equalsIgnoreCase(method)) type = "wechat";
String name = "现金";
if ("bank".equals(type)) name = "银行存款"; else if ("wechat".equals(type)) name = "微信";
// 先按 type 查
List<Long> byType = jdbcTemplate.query("SELECT id FROM accounts WHERE shop_id=? AND type=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, type);
if (!byType.isEmpty()) return byType.get(0);
// 再按 name 查,避免同名唯一冲突
List<Long> byName = jdbcTemplate.query("SELECT id FROM accounts WHERE shop_id=? AND name=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, name);
if (!byName.isEmpty()) return byName.get(0);
// 都没有再插入
jdbcTemplate.update("INSERT INTO accounts (shop_id,user_id,name,type,balance,status,created_at,updated_at) VALUES (?,?,?,?,0,1,NOW(),NOW())",
shopId, userId, name, type);
// 插入后按 type 读取
List<Long> recheck = jdbcTemplate.query("SELECT id FROM accounts WHERE shop_id=? AND type=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, type);
if (!recheck.isEmpty()) return recheck.get(0);
throw new IllegalStateException("账户映射失败: " + method);
}
}

View File

@@ -0,0 +1,44 @@
package com.example.demo.order.dto;
import java.math.BigDecimal;
import java.util.List;
public class OrderDtos {
public static class CreateOrderRequest {
public String type; // sale.out / sale.return / sale.collect / purchase.in / purchase.return / purchase.pay
public String orderTime; // ISO8601 或 yyyy-MM-dd
public Long customerId; // 可空
public Long supplierId; // 可空
public List<Item> items; // 出入库时必填
public List<PaymentItem> payments; // 收款/付款时必填
public BigDecimal amount; // 前端提供,后端将重算覆盖
public String remark;
}
public static class Item {
public Long productId;
public BigDecimal quantity;
public BigDecimal unitPrice;
public BigDecimal discountRate; // 可空,缺省 0
}
public static class PaymentItem {
public String method; // cash/bank/wechat
public BigDecimal amount;
public Long orderId; // 可选:若挂单则带上
}
public static class CreateOrderResponse {
public Long id;
public String orderNo;
public CreateOrderResponse(Long id, String orderNo) { this.id = id; this.orderNo = orderNo; }
}
public static class CreatePaymentsResponse {
public java.util.List<Long> paymentIds;
public CreatePaymentsResponse(java.util.List<Long> ids) { this.paymentIds = ids; }
}
}

View File

@@ -31,7 +31,6 @@ public class ProductDtos {
public BigDecimal stock;
public BigDecimal purchasePrice;
public BigDecimal retailPrice;
public BigDecimal distributionPrice;
public BigDecimal wholesalePrice;
public BigDecimal bigClientPrice;
public List<Image> images;
@@ -61,7 +60,6 @@ public class ProductDtos {
public static class Prices {
public BigDecimal purchasePrice;
public BigDecimal retailPrice;
public BigDecimal distributionPrice;
public BigDecimal wholesalePrice;
public BigDecimal bigClientPrice;
}
@@ -70,3 +68,6 @@ public class ProductDtos {

View File

@@ -39,3 +39,6 @@ public class Inventory {

View File

@@ -98,3 +98,6 @@ public class Product {

View File

@@ -56,3 +56,6 @@ public class ProductCategory {

View File

@@ -41,3 +41,6 @@ public class ProductImage {

View File

@@ -59,3 +59,6 @@ public class ProductPrice {

View File

@@ -46,3 +46,6 @@ public class ProductUnit {

View File

@@ -15,3 +15,6 @@ public interface CategoryRepository extends JpaRepository<ProductCategory, Long>

View File

@@ -9,3 +9,6 @@ public interface InventoryRepository extends JpaRepository<Inventory, Long> {

View File

@@ -13,3 +13,6 @@ public interface ProductImageRepository extends JpaRepository<ProductImage, Long

View File

@@ -9,3 +9,6 @@ public interface ProductPriceRepository extends JpaRepository<ProductPrice, Long

View File

@@ -23,3 +23,6 @@ public interface ProductRepository extends JpaRepository<Product, Long> {

View File

@@ -14,3 +14,6 @@ public interface UnitRepository extends JpaRepository<com.example.demo.product.e

View File

@@ -10,7 +10,9 @@ 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.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -27,35 +29,66 @@ public class ProductService {
private final ProductPriceRepository priceRepository;
private final InventoryRepository inventoryRepository;
private final ProductImageRepository imageRepository;
private final JdbcTemplate jdbcTemplate;
public ProductService(ProductRepository productRepository,
ProductPriceRepository priceRepository,
InventoryRepository inventoryRepository,
ProductImageRepository imageRepository) {
ProductImageRepository imageRepository,
JdbcTemplate jdbcTemplate) {
this.productRepository = productRepository;
this.priceRepository = priceRepository;
this.inventoryRepository = inventoryRepository;
this.imageRepository = imageRepository;
this.jdbcTemplate = jdbcTemplate;
}
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;
});
try {
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();
inventoryRepository.findById(prod.getId()).ifPresent(inv -> it.stock = inv.getQuantity());
priceRepository.findById(prod.getId()).ifPresent(pr -> it.retailPrice = pr.getRetailPrice());
List<ProductImage> imgs = imageRepository.findByProductIdOrderBySortOrderAscIdAsc(prod.getId());
it.cover = imgs.isEmpty() ? null : imgs.get(0).getUrl();
return it;
});
} catch (Exception e) {
// 安全回退为 JDBC 查询,保障功能可用
StringBuilder sql = new StringBuilder("SELECT p.id,p.name,p.brand,p.model,p.spec,\n" +
"(SELECT i.quantity FROM inventories i WHERE i.product_id=p.id) AS stock,\n" +
"(SELECT pr.retail_price FROM product_prices pr WHERE pr.product_id=p.id) AS retail_price,\n" +
"(SELECT img.url FROM product_images img WHERE img.product_id=p.id ORDER BY img.sort_order, img.id LIMIT 1) AS cover\n" +
"FROM products p WHERE p.shop_id=? AND p.deleted_at IS NULL");
List<Object> ps = new ArrayList<>();
ps.add(shopId);
if (kw != null && !kw.isBlank()) { sql.append(" AND (p.name LIKE ? OR p.brand LIKE ? OR p.model LIKE ? OR p.spec LIKE ? OR p.barcode LIKE ?)");
String like = "%" + kw + "%"; ps.add(like); ps.add(like); ps.add(like); ps.add(like); ps.add(like); }
if (categoryId != null) { sql.append(" AND p.category_id=?"); ps.add(categoryId); }
sql.append(" ORDER BY p.id DESC LIMIT ? OFFSET ?");
ps.add(size); ps.add(page * size);
List<ProductDtos.ProductListItem> list = jdbcTemplate.query(sql.toString(), (rs,rn) -> {
ProductDtos.ProductListItem it = new ProductDtos.ProductListItem();
it.id = rs.getLong("id");
it.name = rs.getString("name");
it.brand = rs.getString("brand");
it.model = rs.getString("model");
it.spec = rs.getString("spec");
java.math.BigDecimal st = (java.math.BigDecimal) rs.getObject("stock");
it.stock = st;
java.math.BigDecimal rp = (java.math.BigDecimal) rs.getObject("retail_price");
it.retailPrice = rp;
it.cover = rs.getString("cover");
return it;
}, ps.toArray());
return new PageImpl<>(list, PageRequest.of(page, size), list.size());
}
}
public Optional<ProductDtos.ProductDetail> findDetail(Long id) {
@@ -78,7 +111,6 @@ public class ProductService {
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();
});
@@ -171,12 +203,6 @@ public class ProductService {
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);

View File

@@ -1,9 +1,14 @@
spring.application.name=demo
# 数据源配置(通过环境变量注入,避免硬编码)
spring.datasource.url=${SPRING_DATASOURCE_URL}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
# 正确的配置
# 格式为: jdbc:mysql://<主机名>:<端口号>/<数据库名>?参数
# 默认附带 MySQL 8 推荐参数,避免握手/时区/编码问题
spring.datasource.url=${DB_URL:jdbc:mysql://mysql.tonaspace.com:3306/partsinquiry?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8}
# 用户名和密码直接写值
spring.datasource.username=${DB_USER:root}
spring.datasource.password=${DB_PASSWORD:TONA1234}
# JPA 基本配置
spring.jpa.hibernate.ddl-auto=none