9.20/1
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package com.example.demo.account;
|
||||
|
||||
import com.example.demo.common.AppDefaultsProperties;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/accounts")
|
||||
public class AccountController {
|
||||
|
||||
private final AccountService accountService;
|
||||
private final AppDefaultsProperties defaults;
|
||||
|
||||
public AccountController(AccountService accountService, AppDefaultsProperties defaults) {
|
||||
this.accountService = accountService;
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> list(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestParam(name = "kw", required = false) String kw,
|
||||
@RequestParam(name = "status", required = false) Integer status,
|
||||
@RequestParam(name = "page", defaultValue = "1") int page,
|
||||
@RequestParam(name = "size", defaultValue = "50") int size) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
return ResponseEntity.ok(accountService.list(sid, kw, status == null ? 1 : status, 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 AccountDtos.CreateAccountRequest req) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Long uid = (userId == null ? defaults.getUserId() : userId);
|
||||
Long id = accountService.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 AccountDtos.CreateAccountRequest req) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Long uid = (userId == null ? defaults.getUserId() : userId);
|
||||
accountService.update(id, sid, uid, req);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/ledger")
|
||||
public ResponseEntity<?> ledger(@PathVariable("id") Long id,
|
||||
@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestParam(name = "startDate", required = false) String startDate,
|
||||
@RequestParam(name = "endDate", required = false) String endDate,
|
||||
@RequestParam(name = "kw", required = false) String kw,
|
||||
@RequestParam(name = "page", defaultValue = "1") int page,
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
return ResponseEntity.ok(accountService.ledger(id, sid, kw, Math.max(0, page-1), size, startDate, endDate));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.demo.account;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class AccountDtos {
|
||||
|
||||
public static class CreateAccountRequest {
|
||||
public String name;
|
||||
public String type; // cash, bank, alipay, wechat, other
|
||||
public String bankName;
|
||||
public String bankAccount;
|
||||
public BigDecimal openingBalance; // 可选,创建时作为期初
|
||||
public Integer status; // 1 启用 0 停用
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.example.demo.account;
|
||||
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class AccountService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public AccountService(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
public Object list(Long shopId, String kw, Integer status, int page, int size) {
|
||||
StringBuilder sql = new StringBuilder("SELECT id, name, type, bank_name, bank_account, balance, status FROM accounts WHERE shop_id=?");
|
||||
java.util.List<Object> ps = new java.util.ArrayList<>(); ps.add(shopId);
|
||||
if (status != null) { sql.append(" AND status=?"); ps.add(status); }
|
||||
if (kw != null && !kw.isBlank()) { sql.append(" AND (name LIKE ? OR bank_name LIKE ? OR bank_account LIKE ?)"); ps.add('%'+kw+'%'); ps.add('%'+kw+'%'); ps.add('%'+kw+'%'); }
|
||||
sql.append(" ORDER BY id DESC LIMIT ? OFFSET ?"); ps.add(size); ps.add(page * size);
|
||||
List<Map<String,Object>> list = jdbcTemplate.queryForList(sql.toString(), ps.toArray());
|
||||
Map<String,Object> body = new HashMap<>();
|
||||
body.put("list", list);
|
||||
return body;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long create(Long shopId, Long userId, AccountDtos.CreateAccountRequest req) {
|
||||
if (req == null || req.name == null || req.name.isBlank()) throw new IllegalArgumentException("账户名称必填");
|
||||
String type = (req.type == null || req.type.isBlank()) ? "cash" : req.type.toLowerCase();
|
||||
int status = req.status == null ? 1 : req.status;
|
||||
jdbcTemplate.update("INSERT INTO accounts (shop_id,user_id,name,type,bank_name,bank_account,balance,status,created_at,updated_at) VALUES (?,?,?,?,?,?,0,?,NOW(),NOW())",
|
||||
shopId, userId, req.name, type, req.bankName, req.bankAccount, status);
|
||||
Long id = jdbcTemplate.queryForObject("SELECT id FROM accounts WHERE shop_id=? AND name=? ORDER BY id DESC LIMIT 1", Long.class, shopId, req.name);
|
||||
|
||||
BigDecimal opening = req.openingBalance == null ? BigDecimal.ZERO : req.openingBalance.setScale(2, java.math.RoundingMode.HALF_UP);
|
||||
if (opening.compareTo(BigDecimal.ZERO) != 0) {
|
||||
java.sql.Timestamp now = new java.sql.Timestamp(System.currentTimeMillis());
|
||||
// other_transactions
|
||||
String otType = opening.compareTo(BigDecimal.ZERO) > 0 ? "income" : "expense";
|
||||
BigDecimal amt = opening.abs();
|
||||
jdbcTemplate.update("INSERT INTO other_transactions (shop_id,user_id,type,category,counterparty_type,counterparty_id,account_id,amount,tx_time,remark,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,NOW(),NOW())",
|
||||
shopId, userId, otType, "account_operation", null, null, id, amt, now, "期初余额");
|
||||
// payments
|
||||
String direction = opening.compareTo(BigDecimal.ZERO) > 0 ? "in" : "out";
|
||||
jdbcTemplate.update("INSERT INTO payments (shop_id,user_id,biz_type,biz_id,account_id,direction,amount,pay_time,remark,created_at) VALUES (?,?,?,?,?,?,?,?,?,NOW())",
|
||||
shopId, userId, "other", null, id, direction, amt, now, "期初余额");
|
||||
// update balance
|
||||
BigDecimal delta = opening;
|
||||
jdbcTemplate.update("UPDATE accounts SET balance = balance + ?, updated_at=NOW() WHERE id=? AND shop_id=?", delta, id, shopId);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void update(Long id, Long shopId, Long userId, AccountDtos.CreateAccountRequest req) {
|
||||
StringBuilder sql = new StringBuilder("UPDATE accounts SET updated_at=NOW()");
|
||||
java.util.List<Object> ps = new java.util.ArrayList<>();
|
||||
if (req.name != null) { sql.append(", name=?"); ps.add(req.name); }
|
||||
if (req.type != null) { sql.append(", type=?"); ps.add(req.type.toLowerCase()); }
|
||||
if (req.bankName != null) { sql.append(", bank_name=?"); ps.add(req.bankName); }
|
||||
if (req.bankAccount != null) { sql.append(", bank_account=?"); ps.add(req.bankAccount); }
|
||||
if (req.status != null) { sql.append(", status=?"); ps.add(req.status); }
|
||||
sql.append(" WHERE id=? AND shop_id=?"); ps.add(id); ps.add(shopId);
|
||||
jdbcTemplate.update(sql.toString(), ps.toArray());
|
||||
}
|
||||
|
||||
public Map<String,Object> ledger(Long accountId, Long shopId, String kw, int page, int size, String startDate, String endDate) {
|
||||
// 汇总
|
||||
String baseCond = " shop_id=? AND account_id=?";
|
||||
java.util.List<Object> basePs = new java.util.ArrayList<>(); basePs.add(shopId); basePs.add(accountId);
|
||||
java.util.function.BiFunction<String, java.util.List<Object>, java.math.BigDecimal> sum = (sql, ps) -> {
|
||||
java.math.BigDecimal v = jdbcTemplate.queryForObject(sql, java.math.BigDecimal.class, ps.toArray());
|
||||
return v == null ? java.math.BigDecimal.ZERO : v;
|
||||
};
|
||||
String dateStart = (startDate == null || startDate.isBlank()) ? null : startDate;
|
||||
String dateEnd = (endDate == null || endDate.isBlank()) ? null : endDate;
|
||||
|
||||
// opening = 截止开始日期前净额
|
||||
String payOpenSql = "SELECT COALESCE(SUM(CASE WHEN direction='in' THEN amount ELSE -amount END),0) FROM payments WHERE" + baseCond + (dateStart==null?"":" AND pay_time<?");
|
||||
java.util.List<Object> payOpenPs = new java.util.ArrayList<>(basePs);
|
||||
if (dateStart!=null) payOpenPs.add(java.sql.Timestamp.valueOf(dateStart + " 00:00:00"));
|
||||
String otOpenSql = "SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END),0) FROM other_transactions WHERE" + baseCond + (dateStart==null?"":" AND tx_time<?");
|
||||
java.util.List<Object> otOpenPs = new java.util.ArrayList<>(basePs);
|
||||
if (dateStart!=null) otOpenPs.add(java.sql.Timestamp.valueOf(dateStart + " 00:00:00"));
|
||||
java.math.BigDecimal opening = sum.apply(payOpenSql, payOpenPs).add(sum.apply(otOpenSql, otOpenPs));
|
||||
|
||||
// 区间收入/支出(含两表)
|
||||
String payRangeSql = "SELECT COALESCE(SUM(CASE WHEN direction='in' THEN amount ELSE 0 END),0), COALESCE(SUM(CASE WHEN direction='out' THEN amount ELSE 0 END),0) FROM payments WHERE" + baseCond +
|
||||
(dateStart==null?"":" AND pay_time>=?") + (dateEnd==null?"":" AND pay_time<=?");
|
||||
java.util.List<Object> payRangePs = new java.util.ArrayList<>(basePs);
|
||||
if (dateStart!=null) payRangePs.add(java.sql.Timestamp.valueOf(dateStart + " 00:00:00"));
|
||||
if (dateEnd!=null) payRangePs.add(java.sql.Timestamp.valueOf(dateEnd + " 23:59:59"));
|
||||
java.util.Map<String, Object> pr = jdbcTemplate.queryForMap(payRangeSql, payRangePs.toArray());
|
||||
java.math.BigDecimal payIn = (java.math.BigDecimal) pr.values().toArray()[0];
|
||||
java.math.BigDecimal payOut = (java.math.BigDecimal) pr.values().toArray()[1];
|
||||
|
||||
String otRangeSql = "SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END),0), COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END),0) FROM other_transactions WHERE" + baseCond +
|
||||
(dateStart==null?"":" AND tx_time>=?") + (dateEnd==null?"":" AND tx_time<=?");
|
||||
java.util.List<Object> otRangePs = new java.util.ArrayList<>(basePs);
|
||||
if (dateStart!=null) otRangePs.add(java.sql.Timestamp.valueOf(dateStart + " 00:00:00"));
|
||||
if (dateEnd!=null) otRangePs.add(java.sql.Timestamp.valueOf(dateEnd + " 23:59:59"));
|
||||
java.util.Map<String, Object> or = jdbcTemplate.queryForMap(otRangeSql, otRangePs.toArray());
|
||||
java.math.BigDecimal otIn = (java.math.BigDecimal) or.values().toArray()[0];
|
||||
java.math.BigDecimal otOut = (java.math.BigDecimal) or.values().toArray()[1];
|
||||
|
||||
java.math.BigDecimal income = payIn.add(otIn);
|
||||
java.math.BigDecimal expense = payOut.add(otOut);
|
||||
java.math.BigDecimal ending = opening.add(income).subtract(expense);
|
||||
|
||||
// 明细列表(合并两表,按时间倒序)
|
||||
String listSql = "SELECT id, biz_type AS src, pay_time AS tx_time, direction, amount, remark, biz_id, NULL AS category FROM payments WHERE" + baseCond +
|
||||
(dateStart==null?"":" AND pay_time>=?") + (dateEnd==null?"":" AND pay_time<=?") +
|
||||
" UNION ALL " +
|
||||
"SELECT id, 'other' AS src, tx_time, CASE WHEN type='income' THEN 'in' ELSE 'out' END AS direction, amount, remark, NULL AS biz_id, category FROM other_transactions WHERE" + baseCond +
|
||||
(dateStart==null?"":" AND tx_time>=?") + (dateEnd==null?"":" AND tx_time<=?") +
|
||||
" ORDER BY tx_time DESC LIMIT ? OFFSET ?";
|
||||
java.util.List<Object> lp = new java.util.ArrayList<>(basePs);
|
||||
if (dateStart!=null) { lp.add(java.sql.Timestamp.valueOf(dateStart + " 00:00:00")); }
|
||||
if (dateEnd!=null) { lp.add(java.sql.Timestamp.valueOf(dateEnd + " 23:59:59")); }
|
||||
if (dateStart!=null) { lp.add(java.sql.Timestamp.valueOf(dateStart + " 00:00:00")); }
|
||||
if (dateEnd!=null) { lp.add(java.sql.Timestamp.valueOf(dateEnd + " 23:59:59")); }
|
||||
lp.add(size); lp.add(page * size);
|
||||
List<Map<String,Object>> list = jdbcTemplate.queryForList(listSql, lp.toArray());
|
||||
|
||||
Map<String,Object> resp = new HashMap<>();
|
||||
resp.put("opening", opening);
|
||||
resp.put("income", income);
|
||||
resp.put("expense", expense);
|
||||
resp.put("ending", ending);
|
||||
resp.put("list", list);
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "app.account.defaults")
|
||||
public class AccountDefaultsProperties {
|
||||
|
||||
private String cashName = "现金";
|
||||
private String bankName = "银行存款";
|
||||
private String wechatName = "微信";
|
||||
private String alipayName = "支付宝";
|
||||
|
||||
public String getCashName() { return cashName; }
|
||||
public void setCashName(String cashName) { this.cashName = cashName; }
|
||||
public String getBankName() { return bankName; }
|
||||
public void setBankName(String bankName) { this.bankName = bankName; }
|
||||
public String getWechatName() { return wechatName; }
|
||||
public void setWechatName(String wechatName) { this.wechatName = wechatName; }
|
||||
public String getAlipayName() { return alipayName; }
|
||||
public void setAlipayName(String alipayName) { this.alipayName = alipayName; }
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,25 @@ public class AppDefaultsProperties {
|
||||
private Long shopId = 1L;
|
||||
private Long userId = 2L;
|
||||
|
||||
// 默认账户名称(可配置,避免硬编码)
|
||||
private String accountCashName = "现金";
|
||||
private String accountBankName = "银行存款";
|
||||
private String accountWechatName = "微信";
|
||||
private String accountAlipayName = "支付宝";
|
||||
|
||||
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 getAccountCashName() { return accountCashName; }
|
||||
public void setAccountCashName(String accountCashName) { this.accountCashName = accountCashName; }
|
||||
public String getAccountBankName() { return accountBankName; }
|
||||
public void setAccountBankName(String accountBankName) { this.accountBankName = accountBankName; }
|
||||
public String getAccountWechatName() { return accountWechatName; }
|
||||
public void setAccountWechatName(String accountWechatName) { this.accountWechatName = accountWechatName; }
|
||||
public String getAccountAlipayName() { return accountAlipayName; }
|
||||
public void setAccountAlipayName(String accountAlipayName) { this.accountAlipayName = accountAlipayName; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
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.Map;
|
||||
|
||||
@RestController
|
||||
public class FinanceController {
|
||||
|
||||
private final FinanceService financeService;
|
||||
private final AppDefaultsProperties defaults;
|
||||
|
||||
public FinanceController(FinanceService financeService, AppDefaultsProperties defaults) {
|
||||
this.financeService = financeService;
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
@GetMapping("/api/finance/categories")
|
||||
public ResponseEntity<?> listCategories(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Map<String, Object> body = financeService.getCategories(sid);
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.finance")
|
||||
public class FinanceDefaultsProperties {
|
||||
|
||||
// 形如 key:label, key:label 用逗号分隔
|
||||
private String incomeCategories;
|
||||
private String expenseCategories;
|
||||
|
||||
public String getIncomeCategories() { return incomeCategories; }
|
||||
public void setIncomeCategories(String incomeCategories) { this.incomeCategories = incomeCategories; }
|
||||
|
||||
public String getExpenseCategories() { return expenseCategories; }
|
||||
public void setExpenseCategories(String expenseCategories) { this.expenseCategories = expenseCategories; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class FinanceService {
|
||||
|
||||
private static final String PARAM_KEY = "finance.categories";
|
||||
|
||||
private final SystemParameterRepository systemParameterRepository;
|
||||
private final FinanceDefaultsProperties financeDefaultsProperties;
|
||||
private final javax.sql.DataSource dataSource;
|
||||
private final AppDefaultsProperties appDefaults;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public FinanceService(SystemParameterRepository systemParameterRepository, FinanceDefaultsProperties financeDefaultsProperties, javax.sql.DataSource dataSource, AppDefaultsProperties appDefaults) {
|
||||
this.systemParameterRepository = systemParameterRepository;
|
||||
this.financeDefaultsProperties = financeDefaultsProperties;
|
||||
this.dataSource = dataSource;
|
||||
this.appDefaults = appDefaults;
|
||||
}
|
||||
|
||||
public Map<String, Object> getCategories(Long shopId) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
// 0) 优先从 finance_categories 表读取(避免中文乱码/统一排序)
|
||||
List<Map<String, String>> income = queryCategoriesFromTable(shopId, "income");
|
||||
List<Map<String, String>> expense = queryCategoriesFromTable(shopId, "expense");
|
||||
|
||||
// 1) 回落读取 system_parameters
|
||||
try {
|
||||
if (income == null || income.isEmpty() || expense == null || expense.isEmpty()) {
|
||||
Optional<SystemParameter> opt = systemParameterRepository.findByShopIdAndKey(shopId, PARAM_KEY);
|
||||
if (opt.isPresent()) {
|
||||
String json = opt.get().getValue();
|
||||
if (json != null && !json.isBlank()) {
|
||||
JsonNode root = objectMapper.readTree(json);
|
||||
if (income == null || income.isEmpty()) {
|
||||
JsonNode incNode = root.get("income");
|
||||
if (incNode != null && incNode.isArray()) {
|
||||
income = objectMapper.convertValue(incNode, new TypeReference<List<Map<String,String>>>(){});
|
||||
}
|
||||
}
|
||||
if (expense == null || expense.isEmpty()) {
|
||||
JsonNode expNode = root.get("expense");
|
||||
if (expNode != null && expNode.isArray()) {
|
||||
expense = objectMapper.convertValue(expNode, new TypeReference<List<Map<String,String>>>(){});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 忽略异常,回落至默认配置
|
||||
}
|
||||
|
||||
// 2) 回落:应用配置 app.finance.*
|
||||
if (income == null || income.isEmpty()) {
|
||||
income = parsePairs(financeDefaultsProperties.getIncomeCategories());
|
||||
}
|
||||
if (expense == null || expense.isEmpty()) {
|
||||
expense = parsePairs(financeDefaultsProperties.getExpenseCategories());
|
||||
}
|
||||
|
||||
body.put("incomeCategories", income);
|
||||
body.put("expenseCategories", expense);
|
||||
return body;
|
||||
}
|
||||
|
||||
private List<Map<String, String>> parsePairs(String pairs) {
|
||||
if (pairs == null || pairs.isBlank()) return Collections.emptyList();
|
||||
return Arrays.stream(pairs.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.map(s -> {
|
||||
int idx = s.indexOf(":");
|
||||
String key = idx > 0 ? s.substring(0, idx).trim() : s.trim();
|
||||
String label = idx > 0 ? s.substring(idx + 1).trim() : key;
|
||||
Map<String, String> m = new HashMap<>();
|
||||
m.put("key", key);
|
||||
m.put("label", label);
|
||||
return m;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<Map<String, String>> queryCategoriesFromTable(Long shopId, String type) {
|
||||
Long fallbackShopId = appDefaults == null ? 1L : (appDefaults.getShopId() == null ? 1L : appDefaults.getShopId());
|
||||
try (java.sql.Connection c = dataSource.getConnection();
|
||||
java.sql.PreparedStatement ps = c.prepareStatement(
|
||||
"SELECT shop_id, `key`, label FROM finance_categories WHERE shop_id IN (?, ?) AND type=? AND status=1 " +
|
||||
"ORDER BY CASE WHEN shop_id=? THEN 0 ELSE 1 END, sort_order, id")) {
|
||||
ps.setLong(1, shopId);
|
||||
ps.setLong(2, fallbackShopId);
|
||||
ps.setString(3, type);
|
||||
ps.setLong(4, shopId);
|
||||
try (java.sql.ResultSet rs = ps.executeQuery()) {
|
||||
java.util.Map<String,String> firstByKey = new java.util.LinkedHashMap<>();
|
||||
while (rs.next()) {
|
||||
String key = rs.getString(2);
|
||||
String label = rs.getString(3);
|
||||
if (!firstByKey.containsKey(key)) {
|
||||
firstByKey.put(key, label);
|
||||
}
|
||||
}
|
||||
java.util.List<java.util.Map<String,String>> list = new java.util.ArrayList<>();
|
||||
for (java.util.Map.Entry<String,String> e : firstByKey.entrySet()) {
|
||||
java.util.Map<String,String> m = new java.util.HashMap<>();
|
||||
m.put("key", e.getKey());
|
||||
m.put("label", e.getValue());
|
||||
list.add(m);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
return java.util.Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class RequestLoggingFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain) throws ServletException, IOException {
|
||||
long start = System.currentTimeMillis();
|
||||
String method = request.getMethod();
|
||||
String uri = request.getRequestURI();
|
||||
String query = request.getQueryString();
|
||||
String shopId = request.getHeader("X-Shop-Id");
|
||||
String userId = request.getHeader("X-User-Id");
|
||||
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
long cost = System.currentTimeMillis() - start;
|
||||
int status = response.getStatus();
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("{} {}{} | status={} cost={}ms | shopId={} userId={}",
|
||||
method,
|
||||
uri,
|
||||
(query == null ? "" : ("?" + query)),
|
||||
status,
|
||||
cost,
|
||||
(shopId == null ? "" : shopId),
|
||||
(userId == null ? "" : userId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "system_parameters")
|
||||
public class SystemParameter {
|
||||
|
||||
@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 = "`key`", nullable = false, length = 64)
|
||||
private String key;
|
||||
|
||||
@Column(name = "value", nullable = false, columnDefinition = "JSON")
|
||||
private String value;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = 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 getKey() { return key; }
|
||||
public void setKey(String key) { this.key = key; }
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = value; }
|
||||
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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SystemParameterRepository extends JpaRepository<SystemParameter, Long> {
|
||||
Optional<SystemParameter> findByShopIdAndKey(Long shopId, String key);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,13 @@ public class OrderController {
|
||||
return ResponseEntity.ok(orderService.list(sid, biz, type, kw, Math.max(0, page-1), size, startDate, endDate));
|
||||
}
|
||||
|
||||
@GetMapping("/orders/{id}")
|
||||
public ResponseEntity<?> getOrderDetail(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@PathVariable("id") Long id) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
return ResponseEntity.ok(orderService.getSalesOrderDetail(sid, id));
|
||||
}
|
||||
|
||||
// 兼容前端直接调用 /api/purchase-orders
|
||||
@GetMapping("/purchase-orders")
|
||||
public ResponseEntity<?> purchaseOrders(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@@ -74,6 +81,13 @@ public class OrderController {
|
||||
return ResponseEntity.ok(orderService.list(sid, "purchase", type, kw, Math.max(0, page-1), size, startDate, endDate));
|
||||
}
|
||||
|
||||
@GetMapping("/purchase-orders/{id}")
|
||||
public ResponseEntity<?> getPurchaseOrderDetail(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@PathVariable("id") Long id) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
return ResponseEntity.ok(orderService.getPurchaseOrderDetail(sid, id));
|
||||
}
|
||||
|
||||
@GetMapping("/payments")
|
||||
public ResponseEntity<?> listPayments(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestParam(name = "direction", required = false) String direction,
|
||||
@@ -101,6 +115,15 @@ public class OrderController {
|
||||
return ResponseEntity.ok(orderService.listOtherTransactions(sid, type, accountId, kw, Math.max(0, page-1), size, startDate, endDate));
|
||||
}
|
||||
|
||||
@PostMapping("/other-transactions")
|
||||
public ResponseEntity<?> createOtherTransaction(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestHeader(name = "X-User-Id", required = false) Long userId,
|
||||
@RequestBody OrderDtos.CreateOtherTransactionRequest req) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Long uid = (userId == null ? defaults.getUserId() : userId);
|
||||
return ResponseEntity.ok(orderService.createOtherTransaction(sid, uid, req));
|
||||
}
|
||||
|
||||
@GetMapping("/inventories/logs")
|
||||
public ResponseEntity<?> listInventoryLogs(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestParam(name = "productId", required = false) Long productId,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.demo.order;
|
||||
|
||||
import com.example.demo.common.AccountDefaultsProperties;
|
||||
import com.example.demo.order.dto.OrderDtos;
|
||||
import com.example.demo.product.entity.Inventory;
|
||||
import com.example.demo.product.repo.InventoryRepository;
|
||||
@@ -18,14 +19,15 @@ import java.util.List;
|
||||
public class OrderService {
|
||||
|
||||
private final InventoryRepository inventoryRepository;
|
||||
|
||||
|
||||
private final AccountDefaultsProperties accountDefaults;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public OrderService(InventoryRepository inventoryRepository,
|
||||
JdbcTemplate jdbcTemplate) {
|
||||
JdbcTemplate jdbcTemplate,
|
||||
AccountDefaultsProperties accountDefaults) {
|
||||
this.inventoryRepository = inventoryRepository;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.accountDefaults = accountDefaults;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -163,6 +165,10 @@ public class OrderService {
|
||||
if (req == null) return new OrderDtos.CreatePaymentsResponse(ids);
|
||||
String direction = "sale".equals(bizType) ? "in" : "out";
|
||||
for (OrderDtos.PaymentItem p : req) {
|
||||
// 收/付款必须绑定订单(资金类不在此接口中处理)
|
||||
if (("sale".equals(bizType) || "purchase".equals(bizType)) && p.orderId == null) {
|
||||
throw new IllegalArgumentException("收/付款必须绑定订单");
|
||||
}
|
||||
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) " +
|
||||
@@ -247,39 +253,56 @@ public class OrderService {
|
||||
}
|
||||
|
||||
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=?");
|
||||
StringBuilder sql = new StringBuilder();
|
||||
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 ("purchase".equals(biz)) {
|
||||
// 进货单(含退货按状态过滤),返回驼峰并带供应商名称
|
||||
sql.append("SELECT po.id, po.order_no AS orderNo, po.order_time AS orderTime, po.amount, s.name AS supplierName,\n")
|
||||
.append("CASE WHEN po.status='returned' THEN 'purchase.return' ELSE 'purchase.in' END AS docType\n")
|
||||
.append("FROM purchase_orders po\n")
|
||||
.append("LEFT JOIN suppliers s ON s.id = po.supplier_id\n")
|
||||
.append("WHERE po.shop_id=?");
|
||||
if ("purchase.return".equals(type)) {
|
||||
sql.append(" AND po.status='returned'");
|
||||
}
|
||||
if (kw != null && !kw.isBlank()) { sql.append(" AND (po.order_no LIKE ?)"); ps.add('%' + kw + '%'); }
|
||||
if (startDate != null && !startDate.isBlank()) { sql.append(" AND po.order_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
|
||||
if (endDate != null && !endDate.isBlank()) { sql.append(" AND po.order_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
|
||||
sql.append(" ORDER BY po.order_time DESC LIMIT ? OFFSET ?");
|
||||
} else { // 默认销售
|
||||
// 销售单,返回驼峰并带客户名称
|
||||
sql.append("SELECT so.id, so.order_no AS orderNo, so.order_time AS orderTime, so.amount, c.name AS customerName, 'sale.out' AS docType\n")
|
||||
.append("FROM sales_orders so\n")
|
||||
.append("LEFT JOIN customers c ON c.id = so.customer_id\n")
|
||||
.append("WHERE so.shop_id=?");
|
||||
if (kw != null && !kw.isBlank()) { sql.append(" AND (so.order_no LIKE ?)"); ps.add('%' + kw + '%'); }
|
||||
if (startDate != null && !startDate.isBlank()) { sql.append(" AND so.order_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
|
||||
if (endDate != null && !endDate.isBlank()) { sql.append(" AND so.order_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
|
||||
sql.append(" ORDER BY so.order_time DESC LIMIT ? OFFSET ?");
|
||||
}
|
||||
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=?");
|
||||
StringBuilder sumSql;
|
||||
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")); }
|
||||
if ("purchase".equals(biz)) {
|
||||
sumSql = new StringBuilder("SELECT COALESCE(SUM(amount),0) FROM purchase_orders WHERE shop_id=?");
|
||||
if ("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")); }
|
||||
} else {
|
||||
sumSql = new StringBuilder("SELECT COALESCE(SUM(amount),0) FROM sales_orders WHERE shop_id=?");
|
||||
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);
|
||||
@@ -288,53 +311,132 @@ public class OrderService {
|
||||
}
|
||||
|
||||
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=?");
|
||||
StringBuilder sql = new StringBuilder("SELECT p.id, p.biz_type AS bizType, p.account_id, a.name AS accountName, p.direction, p.amount, p.pay_time AS orderTime,\n" +
|
||||
"CASE \n" +
|
||||
" WHEN p.biz_type='sale' AND p.direction='in' THEN 'sale.collect' \n" +
|
||||
" WHEN p.biz_type='purchase' AND p.direction='out' THEN 'purchase.pay' \n" +
|
||||
" WHEN p.biz_type='other' AND p.direction='in' THEN 'other.income' \n" +
|
||||
" WHEN p.biz_type='other' AND p.direction='out' THEN 'other.expense' \n" +
|
||||
" ELSE CONCAT(p.biz_type, '.', p.direction) END AS docType\n" +
|
||||
"FROM payments p LEFT JOIN accounts a ON a.id=p.account_id WHERE p.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 ?");
|
||||
if (direction != null && !direction.isBlank()) { sql.append(" AND p.direction=?"); ps.add(direction); }
|
||||
if (bizType != null && !bizType.isBlank()) { sql.append(" AND p.biz_type=?"); ps.add(bizType); }
|
||||
if (accountId != null) { sql.append(" AND p.account_id=?"); ps.add(accountId); }
|
||||
if (kw != null && !kw.isBlank()) { sql.append(" AND (CAST(p.id AS CHAR) LIKE ?)"); ps.add('%'+kw+'%'); }
|
||||
if (startDate != null && !startDate.isBlank()) { sql.append(" AND p.pay_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
|
||||
if (endDate != null && !endDate.isBlank()) { sql.append(" AND p.pay_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
|
||||
sql.append(" ORDER BY p.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=?");
|
||||
StringBuilder sumSql = new StringBuilder("SELECT COALESCE(SUM(p.amount),0) FROM payments p WHERE p.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")); }
|
||||
if (direction != null && !direction.isBlank()) { sumSql.append(" AND p.direction=?"); sumPs.add(direction); }
|
||||
if (bizType != null && !bizType.isBlank()) { sumSql.append(" AND p.biz_type=?"); sumPs.add(bizType); }
|
||||
if (accountId != null) { sumSql.append(" AND p.account_id=?"); sumPs.add(accountId); }
|
||||
if (kw != null && !kw.isBlank()) { sumSql.append(" AND (CAST(p.id AS CHAR) LIKE ?)"); sumPs.add('%'+kw+'%'); }
|
||||
if (startDate != null && !startDate.isBlank()) { sumSql.append(" AND p.pay_time>=?"); sumPs.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
|
||||
if (endDate != null && !endDate.isBlank()) { sumSql.append(" AND p.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=?");
|
||||
StringBuilder sql = new StringBuilder("SELECT ot.id, ot.`type`, CONCAT('other.', ot.`type`) AS docType, ot.account_id, a.name AS accountName, ot.amount, ot.tx_time AS txTime, ot.remark FROM other_transactions ot LEFT JOIN accounts a ON a.id=ot.account_id WHERE ot.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);
|
||||
if (type != null && !type.isBlank()) { sql.append(" AND ot.`type`=?"); ps.add(type); }
|
||||
if (accountId != null) { sql.append(" AND ot.account_id=?"); ps.add(accountId); }
|
||||
if (kw != null && !kw.isBlank()) { sql.append(" AND (ot.remark LIKE ? OR ot.category LIKE ?)"); ps.add('%'+kw+'%'); ps.add('%'+kw+'%'); }
|
||||
if (startDate != null && !startDate.isBlank()) { sql.append(" AND ot.tx_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
|
||||
if (endDate != null && !endDate.isBlank()) { sql.append(" AND ot.tx_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
|
||||
sql.append(" ORDER BY ot.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=?");
|
||||
StringBuilder sumSql = new StringBuilder("SELECT COALESCE(SUM(ot.amount),0) FROM other_transactions ot WHERE ot.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")); }
|
||||
if (type != null && !type.isBlank()) { sumSql.append(" AND ot.`type`=?"); sumPs.add(type); }
|
||||
if (accountId != null) { sumSql.append(" AND ot.account_id=?"); sumPs.add(accountId); }
|
||||
if (kw != null && !kw.isBlank()) { sumSql.append(" AND (ot.remark LIKE ? OR ot.category LIKE ?)"); sumPs.add('%'+kw+'%'); sumPs.add('%'+kw+'%'); }
|
||||
if (startDate != null && !startDate.isBlank()) { sumSql.append(" AND ot.tx_time>=?"); sumPs.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
|
||||
if (endDate != null && !endDate.isBlank()) { sumSql.append(" AND ot.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;
|
||||
}
|
||||
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public java.util.Map<String, Object> createOtherTransaction(Long shopId, Long userId, OrderDtos.CreateOtherTransactionRequest req) {
|
||||
if (req == null) throw new IllegalArgumentException("请求为空");
|
||||
String type = req.type == null ? null : req.type.trim().toLowerCase();
|
||||
if (!"income".equals(type) && !"expense".equals(type)) throw new IllegalArgumentException("type 仅支持 income/expense");
|
||||
if (req.accountId == null) throw new IllegalArgumentException("账户必选");
|
||||
java.math.BigDecimal amt = n(req.amount);
|
||||
if (amt.compareTo(java.math.BigDecimal.ZERO) <= 0) throw new IllegalArgumentException("金额需大于0");
|
||||
java.time.LocalDateTime when;
|
||||
if (req.txTime == null || req.txTime.isBlank()) when = nowUtc();
|
||||
else {
|
||||
// 允许 yyyy-MM-dd 或完整时间
|
||||
try {
|
||||
if (req.txTime.length() == 10) when = java.time.LocalDate.parse(req.txTime).atStartOfDay();
|
||||
else when = java.time.LocalDateTime.parse(req.txTime);
|
||||
} catch (Exception e) { when = nowUtc(); }
|
||||
}
|
||||
final java.sql.Timestamp whenTs = java.sql.Timestamp.from(when.atZone(java.time.ZoneOffset.UTC).toInstant());
|
||||
|
||||
// 插入 other_transactions
|
||||
org.springframework.jdbc.support.GeneratedKeyHolder kh = new org.springframework.jdbc.support.GeneratedKeyHolder();
|
||||
String sql = "INSERT INTO other_transactions (shop_id,user_id,`type`,category,counterparty_type,counterparty_id,account_id,amount,tx_time,remark,created_at,updated_at) " +
|
||||
"VALUES (?,?,?,?,?,?,?,?,?, ?, NOW(), NOW())";
|
||||
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++, type);
|
||||
ps.setString(i++, req.category);
|
||||
if (req.counterpartyType == null) ps.setNull(i++, java.sql.Types.VARCHAR); else ps.setString(i++, req.counterpartyType);
|
||||
if (req.counterpartyId == null) ps.setNull(i++, java.sql.Types.BIGINT); else ps.setLong(i++, req.counterpartyId);
|
||||
ps.setLong(i++, req.accountId);
|
||||
ps.setBigDecimal(i++, scale2(amt));
|
||||
ps.setTimestamp(i++, whenTs);
|
||||
ps.setString(i, req.remark);
|
||||
return ps;
|
||||
}, kh);
|
||||
Number key = kh.getKey();
|
||||
Long id = key == null ? null : key.longValue();
|
||||
|
||||
// 写支付流水,联动账户余额
|
||||
String direction = "income".equals(type) ? "in" : "out";
|
||||
org.springframework.jdbc.support.GeneratedKeyHolder payKh = new org.springframework.jdbc.support.GeneratedKeyHolder();
|
||||
final Long idForPayment = id;
|
||||
jdbcTemplate.update(con -> {
|
||||
java.sql.PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO payments (shop_id,user_id,biz_type,biz_id,account_id,direction,amount,pay_time,remark,created_at) VALUES (?,?,?,?,?,?,?,?,?,NOW())",
|
||||
new String[]{"id"}
|
||||
);
|
||||
int i = 1;
|
||||
ps.setLong(i++, shopId);
|
||||
ps.setLong(i++, userId);
|
||||
ps.setString(i++, "other");
|
||||
if (idForPayment == null) ps.setNull(i++, java.sql.Types.BIGINT); else ps.setLong(i++, idForPayment);
|
||||
ps.setLong(i++, req.accountId);
|
||||
ps.setString(i++, direction);
|
||||
ps.setBigDecimal(i++, scale2(amt));
|
||||
ps.setTimestamp(i++, whenTs);
|
||||
ps.setString(i, req.remark);
|
||||
return ps;
|
||||
}, payKh);
|
||||
|
||||
// 联动账户余额:收入加,支出减
|
||||
java.math.BigDecimal delta = "income".equals(type) ? amt : amt.negate();
|
||||
jdbcTemplate.update("UPDATE accounts SET balance = balance + ?, updated_at=NOW() WHERE id=? AND shop_id=?", scale2(delta), req.accountId, shopId);
|
||||
|
||||
java.util.Map<String,Object> resp = new java.util.HashMap<>();
|
||||
resp.put("id", id);
|
||||
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=?");
|
||||
StringBuilder sql = new StringBuilder("SELECT id, product_id, qty_delta, amount_delta, COALESCE(amount_delta,0) AS amount, reason, tx_time AS txTime, remark 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); }
|
||||
@@ -354,6 +456,50 @@ public class OrderService {
|
||||
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> getSalesOrderDetail(Long shopId, Long id) {
|
||||
java.util.List<java.util.Map<String,Object>> heads = jdbcTemplate.queryForList(
|
||||
"SELECT so.id, so.order_no AS orderNo, so.order_time AS orderTime, so.status, so.amount, so.paid_amount AS paidAmount, so.customer_id AS customerId, c.name AS customerName, so.remark\n" +
|
||||
"FROM sales_orders so LEFT JOIN customers c ON c.id=so.customer_id WHERE so.shop_id=? AND so.id=?",
|
||||
shopId, id);
|
||||
if (heads.isEmpty()) return java.util.Map.of();
|
||||
java.util.Map<String,Object> head = heads.get(0);
|
||||
java.util.List<java.util.Map<String,Object>> items = jdbcTemplate.queryForList(
|
||||
"SELECT i.id, i.product_id AS productId, p.name, p.spec, i.quantity, i.unit_price AS unitPrice, i.discount_rate AS discountRate, i.amount\n" +
|
||||
"FROM sales_order_items i JOIN products p ON p.id=i.product_id WHERE i.order_id=?",
|
||||
id);
|
||||
java.util.List<java.util.Map<String,Object>> pays = jdbcTemplate.queryForList(
|
||||
"SELECT p.id, p.amount, p.pay_time AS payTime, p.account_id AS accountId, a.name AS accountName, p.direction\n" +
|
||||
"FROM payments p LEFT JOIN accounts a ON a.id=p.account_id WHERE p.biz_type='sale' AND p.biz_id=?",
|
||||
id);
|
||||
java.util.Map<String,Object> resp = new java.util.HashMap<>(head);
|
||||
resp.put("items", items);
|
||||
resp.put("payments", pays);
|
||||
return resp;
|
||||
}
|
||||
|
||||
// 详情:进货单
|
||||
public java.util.Map<String,Object> getPurchaseOrderDetail(Long shopId, Long id) {
|
||||
java.util.List<java.util.Map<String,Object>> heads = jdbcTemplate.queryForList(
|
||||
"SELECT po.id, po.order_no AS orderNo, po.order_time AS orderTime, po.status, po.amount, po.paid_amount AS paidAmount, po.supplier_id AS supplierId, s.name AS supplierName, po.remark\n" +
|
||||
"FROM purchase_orders po LEFT JOIN suppliers s ON s.id=po.supplier_id WHERE po.shop_id=? AND po.id=?",
|
||||
shopId, id);
|
||||
if (heads.isEmpty()) return java.util.Map.of();
|
||||
java.util.Map<String,Object> head = heads.get(0);
|
||||
java.util.List<java.util.Map<String,Object>> items = jdbcTemplate.queryForList(
|
||||
"SELECT i.id, i.product_id AS productId, p.name, p.spec, i.quantity, i.unit_price AS unitPrice, i.amount\n" +
|
||||
"FROM purchase_order_items i JOIN products p ON p.id=i.product_id WHERE i.order_id=?",
|
||||
id);
|
||||
java.util.List<java.util.Map<String,Object>> pays = jdbcTemplate.queryForList(
|
||||
"SELECT p.id, p.amount, p.pay_time AS payTime, p.account_id AS accountId, a.name AS accountName, p.direction\n" +
|
||||
"FROM payments p LEFT JOIN accounts a ON a.id=p.account_id WHERE p.biz_type='purchase' AND p.biz_id=?",
|
||||
id);
|
||||
java.util.Map<String,Object> resp = new java.util.HashMap<>(head);
|
||||
resp.put("items", items);
|
||||
resp.put("payments", pays);
|
||||
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()); }
|
||||
@@ -364,10 +510,11 @@ public class OrderService {
|
||||
}
|
||||
|
||||
private void ensureDefaultAccounts(Long shopId, Long userId) {
|
||||
// 为 cash/bank/wechat 分别确保存在一条账户记录;按 type→name 顺序检查,避免同名唯一冲突
|
||||
ensureAccount(shopId, userId, "cash", "现金");
|
||||
ensureAccount(shopId, userId, "bank", "银行存款");
|
||||
ensureAccount(shopId, userId, "wechat", "微信");
|
||||
// 为 cash/bank/wechat/alipay 分别确保存在一条账户记录;按 type→name 顺序检查,避免同名唯一冲突
|
||||
ensureAccount(shopId, userId, "cash", accountDefaults.getCashName());
|
||||
ensureAccount(shopId, userId, "bank", accountDefaults.getBankName());
|
||||
ensureAccount(shopId, userId, "wechat", accountDefaults.getWechatName());
|
||||
ensureAccount(shopId, userId, "alipay", accountDefaults.getAlipayName());
|
||||
}
|
||||
|
||||
private void ensureAccount(Long shopId, Long userId, String type, String name) {
|
||||
@@ -383,8 +530,11 @@ public class OrderService {
|
||||
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 = "微信";
|
||||
if ("alipay".equalsIgnoreCase(method)) type = "alipay";
|
||||
String name = accountDefaults.getCashName();
|
||||
if ("bank".equals(type)) name = accountDefaults.getBankName();
|
||||
else if ("wechat".equals(type)) name = accountDefaults.getWechatName();
|
||||
else if ("alipay".equals(type)) name = accountDefaults.getAlipayName();
|
||||
// 先按 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);
|
||||
|
||||
@@ -39,6 +39,17 @@ public class OrderDtos {
|
||||
public java.util.List<Long> paymentIds;
|
||||
public CreatePaymentsResponse(java.util.List<Long> ids) { this.paymentIds = ids; }
|
||||
}
|
||||
|
||||
public static class CreateOtherTransactionRequest {
|
||||
public String type; // income | expense
|
||||
public String category; // 分类key
|
||||
public String counterpartyType; // customer | supplier | other
|
||||
public Long counterpartyId; // 可空
|
||||
public Long accountId; // 必填
|
||||
public java.math.BigDecimal amount; // 必填,>0
|
||||
public String txTime; // yyyy-MM-dd 或 ISO8601
|
||||
public String remark; // 可空
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ spring.application.name=demo
|
||||
# 正确的配置
|
||||
# 格式为: 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.url=${DB_URL:jdbc:mysql://mysql.tonaspace.com:3306/partsinquiry?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8mb4&connectionCollation=utf8mb4_0900_ai_ci}
|
||||
|
||||
# 用户名和密码直接写值
|
||||
spring.datasource.username=${DB_USER:root}
|
||||
@@ -16,6 +16,10 @@ spring.jpa.open-in-view=false
|
||||
spring.jpa.show-sql=false
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
|
||||
|
||||
# 日志级别(开发调试)
|
||||
logging.level.com.example.demo=DEBUG
|
||||
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=INFO
|
||||
|
||||
# CORS 简单放开(如需跨域)
|
||||
spring.web.cors.allowed-origins=${CORS_ALLOWED_ORIGINS:*}
|
||||
spring.web.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
|
||||
@@ -38,3 +42,13 @@ attachments.placeholder.url-path=${ATTACHMENTS_PLACEHOLDER_URL:/api/attachments/
|
||||
# 应用默认上下文(用于开发/演示环境)
|
||||
app.defaults.shop-id=${APP_DEFAULT_SHOP_ID:1}
|
||||
app.defaults.user-id=${APP_DEFAULT_USER_ID:2}
|
||||
|
||||
# 财务分类默认配置(前端请调用 /api/finance/categories 获取,禁止硬编码)
|
||||
app.finance.income-categories=${APP_FINANCE_INCOME:operation_income:经营所得,interest_income:利息收入,other_income:其它收入,deposit_ar_income:收订金/欠款,investment_income:投资收入,sale_income:销售收入,account_operation:账户操作,fund_transfer_in:资金转账转入}
|
||||
app.finance.expense-categories=${APP_FINANCE_EXPENSE:operation_expense:经营支出,office_supplies:办公用品,rent:房租,interest_expense:利息支出,other_expense:其它支出,account_operation:账户操作,fund_transfer_out:资金转账转出}
|
||||
|
||||
# 账户默认名称(避免硬编码,可被环境变量覆盖)
|
||||
app.account.defaults.cash-name=${APP_ACCOUNT_CASH_NAME:现金}
|
||||
app.account.defaults.bank-name=${APP_ACCOUNT_BANK_NAME:银行存款}
|
||||
app.account.defaults.wechat-name=${APP_ACCOUNT_WECHAT_NAME:微信}
|
||||
app.account.defaults.alipay-name=${APP_ACCOUNT_ALIPAY_NAME:支付宝}
|
||||
|
||||
Reference in New Issue
Block a user