This commit is contained in:
2025-09-18 21:17:44 +08:00
parent e560e90970
commit bff3d0414d
49 changed files with 1063 additions and 90 deletions

View File

@@ -0,0 +1,43 @@
package com.example.demo.common;
import org.springframework.dao.DataAccessException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
private ResponseEntity<Map<String,Object>> badRequest(String message) {
Map<String,Object> body = new HashMap<>();
body.put("message", message);
return ResponseEntity.badRequest().body(body);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<?> handleIllegalArgument(IllegalArgumentException ex) {
return badRequest(ex.getMessage());
}
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<?> handleIllegalState(IllegalStateException ex) {
return badRequest(ex.getMessage());
}
@ExceptionHandler(DataAccessException.class)
public ResponseEntity<?> handleDataAccess(DataAccessException ex) {
return badRequest("数据库操作失败");
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleAny(Exception ex) {
Map<String,Object> body = new HashMap<>();
body.put("message", ex.getMessage() == null ? "Internal Server Error" : ex.getMessage());
return ResponseEntity.status(500).body(body);
}
}

View File

@@ -45,7 +45,11 @@ public class CustomerController {
body.put("priceLevel", c.getPriceLevel());
body.put("remark", c.getRemark());
body.put("address", c.getAddress());
body.put("receivable", new java.math.BigDecimal("0")); // 详情页如需可扩展计算
body.put("arOpening", c.getArOpening());
Long sid = (shopId == null ? null : shopId);
if (sid != null) {
body.put("receivable", customerService.computeReceivable(sid, id));
}
return ResponseEntity.ok(body);
}

View File

@@ -45,7 +45,7 @@ public class CustomerService {
public Long create(Long shopId, Long userId, CustomerDtos.CreateOrUpdateCustomerRequest req) {
Customer c = new Customer();
c.setShopId(shopId); c.setUserId(userId);
c.setName(req.name); c.setLevel(req.level); c.setPriceLevel(nullToDefaultPriceLevel(req.priceLevel));
c.setName(req.name); c.setLevel(req.level); c.setPriceLevel(cnPriceLevelOrDefault(req.priceLevel));
c.setContactName(req.contactName); c.setMobile(req.mobile); c.setPhone(req.phone); c.setAddress(req.address);
c.setStatus(1);
c.setArOpening(req.arOpening == null ? BigDecimal.ZERO : req.arOpening);
@@ -60,7 +60,7 @@ public class CustomerService {
public void update(Long id, Long shopId, Long userId, CustomerDtos.CreateOrUpdateCustomerRequest req) {
Customer c = customerRepository.findById(id).orElseThrow();
if (!c.getShopId().equals(shopId)) throw new IllegalArgumentException("跨店铺修改");
c.setName(req.name); c.setLevel(req.level); c.setPriceLevel(nullToDefaultPriceLevel(req.priceLevel));
c.setName(req.name); c.setLevel(req.level); c.setPriceLevel(cnPriceLevelOrDefault(req.priceLevel));
c.setContactName(req.contactName); c.setMobile(req.mobile); c.setPhone(req.phone); c.setAddress(req.address);
if (req.arOpening != null) c.setArOpening(req.arOpening);
c.setRemark(req.remark);
@@ -68,7 +68,20 @@ public class CustomerService {
customerRepository.save(c);
}
private String nullToDefaultPriceLevel(String v) { return (v == null || v.isBlank()) ? "retail" : v; }
private String cnPriceLevelOrDefault(String v) {
if (v == null || v.isBlank()) return "零售价";
// 兼容历史英文值
if ("retail".equalsIgnoreCase(v)) return "零售价";
if ("wholesale".equalsIgnoreCase(v) || "distribution".equalsIgnoreCase(v)) return "批发价";
if ("big_client".equalsIgnoreCase(v)) return "大单报价";
return v; // 已是中文
}
public BigDecimal computeReceivable(Long shopId, Long customerId) {
Optional<Customer> oc = customerRepository.findById(customerId);
BigDecimal opening = oc.map(Customer::getArOpening).orElse(BigDecimal.ZERO);
return calcReceivable(shopId, customerId, opening);
}
private BigDecimal calcReceivable(Long shopId, Long customerId, BigDecimal opening) {
BigDecimal open = opening == null ? BigDecimal.ZERO : opening;

View File

@@ -91,43 +91,66 @@ public class OrderService {
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
// insert head(按表结构分别处理 sales*/purchase* 的 customer_id/supplier_id 列)
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())";
boolean isSalesHead = headTable.startsWith("sales");
boolean isPurchaseHead = headTable.startsWith("purchase");
String headSql;
if (isSalesHead) {
headSql = "INSERT INTO " + headTable + " (shop_id,user_id,customer_id,order_no,order_time,status,amount,paid_amount,remark,created_at,updated_at) " +
"VALUES (?,?,?,?,?, 'approved', ?, 0, ?, NOW(), NOW())";
} else if (isPurchaseHead) {
headSql = "INSERT INTO " + headTable + " (shop_id,user_id,supplier_id,order_no,order_time,status,amount,paid_amount,remark,created_at,updated_at) " +
"VALUES (?,?,?,?,?, 'approved', ?, 0, ?, NOW(), NOW())";
} else {
// 理论不会到这里,兜底为 sales 结构
headSql = "INSERT INTO " + headTable + " (shop_id,user_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);
int idx = 1;
ps.setLong(idx++, shopId);
ps.setLong(idx++, userId);
if (isSalesHead) {
ps.setObject(idx++, customerId, java.sql.Types.BIGINT);
} else if (isPurchaseHead) {
ps.setObject(idx++, supplierId, java.sql.Types.BIGINT);
}
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);
ps.setString(idx++, orderNo);
ps.setTimestamp(idx++, java.sql.Timestamp.from(now.atZone(java.time.ZoneOffset.UTC).toInstant()));
ps.setBigDecimal(idx++, scale2(totalRef[0]));
ps.setString(idx, 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 (?,?,?,?,?,?)";
// insert items(销售类有折扣列,采购类无折扣列)
boolean itemsHasDiscount = isSaleOut || isSaleReturn;
String itemSql = itemsHasDiscount
? ("INSERT INTO " + itemTable + " (order_id,product_id,quantity,unit_price,discount_rate,amount) VALUES (?,?,?,?,?,?)")
: ("INSERT INTO " + itemTable + " (order_id,product_id,quantity,unit_price,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);
if (itemsHasDiscount) {
jdbcTemplate.update(itemSql, orderId, it.productId, qty, price, dr, line);
} else {
jdbcTemplate.update(itemSql, orderId, it.productId, qty, price, line);
}
}
// 维护供应商应付:采购入库增加,应付=+amount采购退货减少应付=-amount
if (isPurchaseHead && supplierId != null) {
java.math.BigDecimal delta = scale2(totalRef[0]);
if (isPurchaseReturn) delta = delta.negate();
adjustSupplierPayable(supplierId, delta);
}
return new OrderDtos.CreateOrderResponse(orderId, orderNo);
@@ -163,6 +186,14 @@ public class OrderService {
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);
// 采购付款联动应付:应付 -= 付款金额
if ("purchase".equals(bizType)) {
java.util.List<Long> sids = jdbcTemplate.query("SELECT supplier_id FROM purchase_orders WHERE id=?", (rs,rn)-> rs.getLong(1), p.orderId);
if (!sids.isEmpty() && sids.get(0) != null) {
adjustSupplierPayable(sids.get(0), n(p.amount).negate());
}
}
}
}
return new OrderDtos.CreatePaymentsResponse(ids);
@@ -198,6 +229,21 @@ public class OrderService {
}
// 更新状态
jdbcTemplate.update("UPDATE " + headTable + " SET status='void' WHERE id = ?", id);
// 采购单作废回滚应付
if ("purchase.out".equals(type) || "purchase.in".equals(type) || "purchase.return".equals(type)) {
boolean purchaseIn = "purchase.in".equals(type);
boolean purchaseRet = "purchase.return".equals(type);
if (purchaseIn || purchaseRet) {
java.util.List<java.util.Map<String,Object>> h = jdbcTemplate.queryForList("SELECT supplier_id, amount FROM " + headTable + " WHERE id=?", id);
if (!h.isEmpty()) {
Long sid = ((Number)h.get(0).get("supplier_id")).longValue();
java.math.BigDecimal amt = new java.math.BigDecimal(h.get(0).get("amount").toString());
java.math.BigDecimal delta = purchaseIn ? amt.negate() : amt; // 入库作废应付减少;退货作废应付增加
adjustSupplierPayable(sid, delta);
}
}
}
}
public java.util.Map<String,Object> list(Long shopId, String biz, String type, String kw, int page, int size, String startDate, String endDate) {
@@ -312,6 +358,11 @@ public class OrderService {
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 adjustSupplierPayable(Long supplierId, java.math.BigDecimal delta) {
if (supplierId == null || delta == null) return;
jdbcTemplate.update("UPDATE suppliers SET ap_payable = ap_payable + ? WHERE id = ?", delta, supplierId);
}
private void ensureDefaultAccounts(Long shopId, Long userId) {
// 为 cash/bank/wechat 分别确保存在一条账户记录;按 type→name 顺序检查,避免同名唯一冲突
ensureAccount(shopId, userId, "cash", "现金");

View File

@@ -0,0 +1,55 @@
package com.example.demo.supplier.controller;
import com.example.demo.common.AppDefaultsProperties;
import com.example.demo.supplier.dto.SupplierDtos;
import com.example.demo.supplier.service.SupplierService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/suppliers")
public class SupplierController {
private final SupplierService supplierService;
private final AppDefaultsProperties defaults;
public SupplierController(SupplierService supplierService, AppDefaultsProperties defaults) {
this.supplierService = supplierService;
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 = "debtOnly", required = false, defaultValue = "false") boolean debtOnly,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "50") int size) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
return ResponseEntity.ok(supplierService.search(sid, kw, debtOnly, Math.max(0, page-1), size));
}
@PostMapping
public ResponseEntity<?> create(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestHeader(name = "X-User-Id", required = false) Long userId,
@RequestBody SupplierDtos.CreateOrUpdateSupplierRequest req) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Long uid = (userId == null ? defaults.getUserId() : userId);
Long id = supplierService.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 SupplierDtos.CreateOrUpdateSupplierRequest req) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Long uid = (userId == null ? defaults.getUserId() : userId);
supplierService.update(id, sid, uid, req);
return ResponseEntity.ok().build();
}
}

View File

@@ -0,0 +1,49 @@
package com.example.demo.supplier.dto;
public class SupplierDtos {
public static class CreateOrUpdateSupplierRequest {
public String name;
public String contactName;
public String mobile;
public String phone;
public String address;
public java.math.BigDecimal apOpening;
public java.math.BigDecimal apPayable;
public String remark;
}
public static class SupplierListItem {
public Long id;
public String name;
public String contactName;
public String mobile;
public String phone;
public String address;
public java.math.BigDecimal apOpening;
public java.math.BigDecimal apPayable;
public String remark;
public SupplierListItem(Long id,
String name,
String contactName,
String mobile,
String phone,
String address,
java.math.BigDecimal apOpening,
java.math.BigDecimal apPayable,
String remark) {
this.id = id;
this.name = name;
this.contactName = contactName;
this.mobile = mobile;
this.phone = phone;
this.address = address;
this.apOpening = apOpening;
this.apPayable = apPayable;
this.remark = remark;
}
}
}

View File

@@ -0,0 +1,96 @@
package com.example.demo.supplier.service;
import com.example.demo.supplier.dto.SupplierDtos;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class SupplierService {
private final JdbcTemplate jdbcTemplate;
public SupplierService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Map<String, Object> search(Long shopId, String kw, boolean debtOnly, int page, int size) {
StringBuilder sql = new StringBuilder("SELECT id,name,contact_name,mobile,phone,address,ap_opening,ap_payable,remark FROM suppliers WHERE shop_id=? AND status=1");
java.util.List<Object> ps = new java.util.ArrayList<>();
ps.add(shopId);
if (kw != null && !kw.isBlank()) {
sql.append(" AND (name LIKE ? OR phone LIKE ? OR mobile LIKE ?)");
String like = '%'+kw+'%';
ps.add(like); ps.add(like); ps.add(like);
}
if (debtOnly) {
sql.append(" AND ap_payable > 0");
}
sql.append(" ORDER BY id DESC LIMIT ? OFFSET ?");
ps.add(size); ps.add(Math.max(0, page) * size);
List<Map<String,Object>> rows = jdbcTemplate.queryForList(sql.toString(), ps.toArray());
List<SupplierDtos.SupplierListItem> items = rows.stream().map(r -> new SupplierDtos.SupplierListItem(
((Number)r.get("id")).longValue(),
(String) r.get("name"),
(String) r.get("contact_name"),
(String) r.get("mobile"),
(String) r.get("phone"),
(String) r.get("address"),
toBig((java.lang.Number) r.get("ap_opening")),
toBig((java.lang.Number) r.get("ap_payable")),
(String) r.get("remark")
)).collect(Collectors.toList());
java.util.Map<String,Object> resp = new java.util.HashMap<>();
resp.put("list", items);
return resp;
}
@Transactional
public Long create(Long shopId, Long userId, SupplierDtos.CreateOrUpdateSupplierRequest req) {
BigDecimal open = nz(req.apOpening);
BigDecimal payable = nz(req.apPayable);
String sql = "INSERT INTO suppliers (shop_id,user_id,name,contact_name,mobile,phone,address,status,ap_opening,ap_payable,remark,created_at,updated_at) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,NOW(),NOW())";
org.springframework.jdbc.support.GeneratedKeyHolder kh = new org.springframework.jdbc.support.GeneratedKeyHolder();
jdbcTemplate.update(con -> {
java.sql.PreparedStatement ps = con.prepareStatement(sql, new String[]{"id"});
int i = 1;
ps.setLong(i++, shopId);
ps.setLong(i++, userId);
ps.setString(i++, req.name);
ps.setString(i++, req.contactName);
ps.setString(i++, req.mobile);
ps.setString(i++, req.phone);
ps.setString(i++, req.address);
ps.setInt(i++, 1);
ps.setBigDecimal(i++, open);
ps.setBigDecimal(i++, payable);
ps.setString(i, req.remark);
return ps;
}, kh);
Number key = kh.getKey();
return key == null ? null : key.longValue();
}
@Transactional
public void update(Long id, Long shopId, Long userId, SupplierDtos.CreateOrUpdateSupplierRequest req) {
String sql = "UPDATE suppliers SET name=?, contact_name=?, mobile=?, phone=?, address=?, ap_opening=?, ap_payable=?, remark=?, updated_at=NOW() WHERE id=? AND shop_id=?";
jdbcTemplate.update(sql, req.name, req.contactName, req.mobile, req.phone, req.address, nz(req.apOpening), nz(req.apPayable), req.remark, id, shopId);
}
public java.util.Optional<java.util.Map<String,Object>> findById(Long id) {
List<Map<String,Object>> rows = jdbcTemplate.queryForList("SELECT id,name,contact_name,mobile,phone,address,ap_opening,ap_payable,remark FROM suppliers WHERE id=?", id);
if (rows.isEmpty()) return java.util.Optional.empty();
return java.util.Optional.of(rows.get(0));
}
private static BigDecimal toBig(Number n) { return n == null ? BigDecimal.ZERO : new BigDecimal(n.toString()); }
private static BigDecimal nz(BigDecimal v) { return v == null ? BigDecimal.ZERO : v.setScale(2, java.math.RoundingMode.HALF_UP); }
}