Compare commits
No commits in common. "c765ffaa89995277552277810e532a1136cbe486" and "a8889cf4329ccb6c570c85e5822fa4a41b27589e" have entirely different histories.
c765ffaa89
...
a8889cf432
@ -1,15 +0,0 @@
|
|||||||
package com.accounting.config;
|
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Configuration
|
|
||||||
@ConfigurationProperties(prefix = "k780")
|
|
||||||
public class K780Config {
|
|
||||||
private String apiUrl = "https://sapi.k780.com";
|
|
||||||
private String appKey = "78346";
|
|
||||||
private String sign = "1b502c535927b66d9b888a6d4701bf72";
|
|
||||||
private int timeout = 5000;
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package com.accounting.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class RestTemplateConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public RestTemplate restTemplate() {
|
|
||||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
|
||||||
factory.setConnectTimeout(5000);
|
|
||||||
factory.setReadTimeout(5000);
|
|
||||||
return new RestTemplate(factory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package com.accounting.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableScheduling
|
|
||||||
public class SchedulerConfig {
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -35,7 +35,6 @@ public class SecurityConfig {
|
|||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/api/auth/**").permitAll()
|
.requestMatchers("/api/auth/**").permitAll()
|
||||||
.requestMatchers("/api/gold-price/**").permitAll()
|
|
||||||
.requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**").permitAll()
|
.requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
@ -57,14 +56,10 @@ public class SecurityConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
CorsConfiguration configuration = new CorsConfiguration();
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
// 允许你的前端域名访问,根据实际情况修改
|
configuration.setAllowedOrigins(Arrays.asList("*"));
|
||||||
configuration.setAllowedOrigins(Arrays.asList(
|
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
"https://accounting.aqroid.cn",
|
|
||||||
"http://localhost:5173"
|
|
||||||
));
|
|
||||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"));
|
|
||||||
configuration.setAllowedHeaders(Arrays.asList("*"));
|
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||||
configuration.setAllowCredentials(true); // 允许携带认证信息
|
configuration.setAllowCredentials(false);
|
||||||
configuration.setMaxAge(3600L);
|
configuration.setMaxAge(3600L);
|
||||||
|
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
package com.accounting.controller;
|
|
||||||
|
|
||||||
import com.accounting.dto.AccountRequest;
|
|
||||||
import com.accounting.dto.AccountResponse;
|
|
||||||
import com.accounting.entity.User;
|
|
||||||
import com.accounting.mapper.UserMapper;
|
|
||||||
import com.accounting.service.AccountService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
@Tag(name = "账户管理", description = "账户管理接口")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/accounts")
|
|
||||||
public class AccountController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AccountService accountService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserMapper userMapper;
|
|
||||||
|
|
||||||
@Operation(summary = "获取账户信息")
|
|
||||||
@GetMapping
|
|
||||||
public ResponseEntity<AccountResponse> getAccount(Authentication authentication) {
|
|
||||||
Long userId = getUserId(authentication);
|
|
||||||
AccountResponse response = accountService.getAccountBalance(userId);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "更新账户信息")
|
|
||||||
@PutMapping
|
|
||||||
public ResponseEntity<AccountResponse> updateAccount(
|
|
||||||
@RequestBody AccountRequest request,
|
|
||||||
Authentication authentication) {
|
|
||||||
Long userId = getUserId(authentication);
|
|
||||||
AccountResponse response = accountService.updateAccount(userId, request);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "更新账户余额")
|
|
||||||
@PutMapping("/balance")
|
|
||||||
public ResponseEntity<AccountResponse> updateAccountBalance(
|
|
||||||
@RequestBody AccountRequest request,
|
|
||||||
Authentication authentication) {
|
|
||||||
Long userId = getUserId(authentication);
|
|
||||||
AccountResponse response = accountService.updateAccountBalance(userId, request.getInitialBalance());
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getUserId(Authentication authentication) {
|
|
||||||
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
|
||||||
String username = userDetails.getUsername();
|
|
||||||
User user = userMapper.selectOne(
|
|
||||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<User>()
|
|
||||||
.eq(User::getUsername, username)
|
|
||||||
);
|
|
||||||
return user != null ? user.getId() : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -2,7 +2,6 @@ package com.accounting.controller;
|
|||||||
|
|
||||||
import com.accounting.dto.BillRequest;
|
import com.accounting.dto.BillRequest;
|
||||||
import com.accounting.dto.BillResponse;
|
import com.accounting.dto.BillResponse;
|
||||||
import com.accounting.dto.BatchBillRequest;
|
|
||||||
import com.accounting.entity.User;
|
import com.accounting.entity.User;
|
||||||
import com.accounting.mapper.UserMapper;
|
import com.accounting.mapper.UserMapper;
|
||||||
import com.accounting.service.BillService;
|
import com.accounting.service.BillService;
|
||||||
@ -38,14 +37,6 @@ public class BillController {
|
|||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "批量创建账单")
|
|
||||||
@PostMapping("/batch")
|
|
||||||
public ResponseEntity<List<BillResponse>> createBills(@Valid @RequestBody BatchBillRequest request, Authentication authentication) {
|
|
||||||
Long userId = getUserId(authentication);
|
|
||||||
List<BillResponse> responses = billService.createBills(request, userId);
|
|
||||||
return ResponseEntity.ok(responses);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "更新账单")
|
@Operation(summary = "更新账单")
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public ResponseEntity<BillResponse> updateBill(@PathVariable Long id, @Valid @RequestBody BillRequest request, Authentication authentication) {
|
public ResponseEntity<BillResponse> updateBill(@PathVariable Long id, @Valid @RequestBody BillRequest request, Authentication authentication) {
|
||||||
|
|||||||
@ -1,80 +0,0 @@
|
|||||||
package com.accounting.controller;
|
|
||||||
|
|
||||||
import com.accounting.dto.BudgetRequest;
|
|
||||||
import com.accounting.dto.BudgetResponse;
|
|
||||||
import com.accounting.dto.BudgetSettlementResponse;
|
|
||||||
import com.accounting.entity.User;
|
|
||||||
import com.accounting.mapper.UserMapper;
|
|
||||||
import com.accounting.service.BudgetService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
@Tag(name = "预算管理", description = "预算管理接口")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/budgets")
|
|
||||||
public class BudgetController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private BudgetService budgetService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserMapper userMapper;
|
|
||||||
|
|
||||||
@Operation(summary = "获取当前月份的预算信息")
|
|
||||||
@GetMapping
|
|
||||||
public ResponseEntity<BudgetResponse> getBudget(Authentication authentication) {
|
|
||||||
Long userId = getUserId(authentication);
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
BudgetResponse response = budgetService.getBudgetWithStatistics(userId, today.getYear(), today.getMonthValue());
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "获取上月预算结算信息")
|
|
||||||
@GetMapping("/settlement")
|
|
||||||
public ResponseEntity<BudgetSettlementResponse> getBudgetSettlement(Authentication authentication) {
|
|
||||||
Long userId = getUserId(authentication);
|
|
||||||
BudgetSettlementResponse response = budgetService.getLastMonthSettlement(userId);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "设置/更新预算")
|
|
||||||
@PutMapping
|
|
||||||
public ResponseEntity<BudgetResponse> setBudget(
|
|
||||||
@RequestBody BudgetRequest request,
|
|
||||||
Authentication authentication) {
|
|
||||||
Long userId = getUserId(authentication);
|
|
||||||
BudgetResponse response = budgetService.setBudget(userId, request);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "更新当前月份预算")
|
|
||||||
@PutMapping("/current")
|
|
||||||
public ResponseEntity<BudgetResponse> updateCurrentBudget(
|
|
||||||
@RequestBody BudgetRequest request,
|
|
||||||
Authentication authentication) {
|
|
||||||
Long userId = getUserId(authentication);
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
request.setYear(today.getYear());
|
|
||||||
request.setMonth(today.getMonthValue());
|
|
||||||
BudgetResponse response = budgetService.setBudget(userId, request);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getUserId(Authentication authentication) {
|
|
||||||
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
|
||||||
String username = userDetails.getUsername();
|
|
||||||
User user = userMapper.selectOne(
|
|
||||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<User>()
|
|
||||||
.eq(User::getUsername, username)
|
|
||||||
);
|
|
||||||
return user != null ? user.getId() : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
package com.accounting.controller;
|
|
||||||
|
|
||||||
import com.accounting.dto.GoldPriceResponse;
|
|
||||||
import com.accounting.service.GoldPriceService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Tag(name = "黄金价格", description = "黄金价格查询接口")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/gold-price")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class GoldPriceController {
|
|
||||||
|
|
||||||
private final GoldPriceService goldPriceService;
|
|
||||||
|
|
||||||
private static final String DEFAULT_GOLD_ID = "1053";
|
|
||||||
|
|
||||||
@Operation(summary = "获取当前黄金价格")
|
|
||||||
@GetMapping("/current")
|
|
||||||
public ResponseEntity<Map<String, Object>> getCurrentPrice(
|
|
||||||
@RequestParam(required = false, defaultValue = DEFAULT_GOLD_ID) String goldId) {
|
|
||||||
try {
|
|
||||||
GoldPriceResponse data = goldPriceService.getCurrentPrice(goldId);
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("code", 200);
|
|
||||||
if (data == null) {
|
|
||||||
response.put("message", "暂无黄金价格数据,请稍后再试");
|
|
||||||
response.put("data", null);
|
|
||||||
} else {
|
|
||||||
response.put("message", "success");
|
|
||||||
response.put("data", data);
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("code", 500);
|
|
||||||
response.put("message", e.getMessage());
|
|
||||||
response.put("data", null);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "获取历史黄金价格")
|
|
||||||
@GetMapping("/history")
|
|
||||||
public ResponseEntity<Map<String, Object>> getHistoryPrices(
|
|
||||||
@RequestParam(required = false, defaultValue = DEFAULT_GOLD_ID) String goldId,
|
|
||||||
@RequestParam(required = false, defaultValue = "30") Integer days) {
|
|
||||||
try {
|
|
||||||
List<GoldPriceResponse> data = goldPriceService.getHistoryPrices(goldId, days);
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("code", 200);
|
|
||||||
response.put("message", "success");
|
|
||||||
response.put("data", data);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("code", 500);
|
|
||||||
response.put("message", e.getMessage());
|
|
||||||
response.put("data", null);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "获取指定日期的黄金价格记录")
|
|
||||||
@GetMapping("/by-date")
|
|
||||||
public ResponseEntity<Map<String, Object>> getPricesByDate(
|
|
||||||
@RequestParam(required = false, defaultValue = DEFAULT_GOLD_ID) String goldId,
|
|
||||||
@RequestParam(required = true) String date) {
|
|
||||||
try {
|
|
||||||
LocalDate localDate = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
|
||||||
List<GoldPriceResponse> data = goldPriceService.getPricesByDate(goldId, localDate);
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("code", 200);
|
|
||||||
response.put("message", "success");
|
|
||||||
response.put("data", data);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("code", 500);
|
|
||||||
response.put("message", e.getMessage());
|
|
||||||
response.put("data", null);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "获取最近有数据的日期")
|
|
||||||
@GetMapping("/latest-date")
|
|
||||||
public ResponseEntity<Map<String, Object>> getLatestDate(
|
|
||||||
@RequestParam(required = false, defaultValue = DEFAULT_GOLD_ID) String goldId) {
|
|
||||||
try {
|
|
||||||
LocalDate latestDate = goldPriceService.getLatestDate(goldId);
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("code", 200);
|
|
||||||
response.put("message", "success");
|
|
||||||
if (latestDate != null) {
|
|
||||||
response.put("data", latestDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
|
|
||||||
} else {
|
|
||||||
response.put("data", null);
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("code", 500);
|
|
||||||
response.put("message", e.getMessage());
|
|
||||||
response.put("data", null);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "立即刷新黄金价格")
|
|
||||||
@PostMapping("/refresh")
|
|
||||||
public ResponseEntity<Map<String, Object>> refreshPrice(
|
|
||||||
@RequestParam(required = false, defaultValue = DEFAULT_GOLD_ID) String goldId) {
|
|
||||||
try {
|
|
||||||
goldPriceService.refreshCurrentPrice(goldId);
|
|
||||||
// 刷新后返回最新的价格
|
|
||||||
GoldPriceResponse data = goldPriceService.getCurrentPrice(goldId);
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("code", 200);
|
|
||||||
response.put("message", "刷新成功");
|
|
||||||
response.put("data", data);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("code", 500);
|
|
||||||
response.put("message", e.getMessage());
|
|
||||||
response.put("data", null);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
package com.accounting.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class AccountRequest {
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
private BigDecimal initialBalance;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package com.accounting.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class AccountResponse {
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
private BigDecimal initialBalance;
|
|
||||||
|
|
||||||
private BigDecimal balance; // 计算后的余额(初始余额 + 收入 - 支出)
|
|
||||||
|
|
||||||
private BigDecimal totalIncome; // 总收入
|
|
||||||
|
|
||||||
private BigDecimal totalExpense; // 总支出
|
|
||||||
|
|
||||||
private LocalDateTime createTime;
|
|
||||||
|
|
||||||
private LocalDateTime updateTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
package com.accounting.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class BatchBillRequest {
|
|
||||||
@NotEmpty(message = "账单列表不能为空")
|
|
||||||
@Valid
|
|
||||||
private List<BillRequest> bills;
|
|
||||||
|
|
||||||
public List<BillRequest> getBills() {
|
|
||||||
return bills;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBills(List<BillRequest> bills) {
|
|
||||||
this.bills = bills;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
package com.accounting.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class BudgetRequest {
|
|
||||||
private Integer year;
|
|
||||||
|
|
||||||
private Integer month;
|
|
||||||
|
|
||||||
private BigDecimal amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
package com.accounting.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class BudgetResponse {
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
private Integer year;
|
|
||||||
|
|
||||||
private Integer month;
|
|
||||||
|
|
||||||
private BigDecimal amount; // 预算金额
|
|
||||||
|
|
||||||
private BigDecimal usedAmount; // 已用金额(本月支出)
|
|
||||||
|
|
||||||
private BigDecimal remainingAmount; // 剩余预算
|
|
||||||
|
|
||||||
private BigDecimal remainingDaily; // 剩余日均
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
package com.accounting.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class BudgetSettlementResponse {
|
|
||||||
private Integer year; // 年份
|
|
||||||
|
|
||||||
private Integer month; // 月份
|
|
||||||
|
|
||||||
private BigDecimal budgetAmount; // 预算金额
|
|
||||||
|
|
||||||
private BigDecimal actualExpense; // 实际支出
|
|
||||||
|
|
||||||
private Boolean isOverBudget; // 是否超支
|
|
||||||
|
|
||||||
private BigDecimal overAmount; // 超支金额(如果超支)
|
|
||||||
|
|
||||||
private BigDecimal completionRate; // 完成率(实际支出/预算金额,如果预算为0则为null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
package com.accounting.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class GoldPriceResponse {
|
|
||||||
private String goldId;
|
|
||||||
|
|
||||||
private String goldName;
|
|
||||||
|
|
||||||
private BigDecimal price;
|
|
||||||
|
|
||||||
private BigDecimal priceChange;
|
|
||||||
|
|
||||||
private BigDecimal priceChangePercent;
|
|
||||||
|
|
||||||
private BigDecimal highPrice;
|
|
||||||
|
|
||||||
private BigDecimal lowPrice;
|
|
||||||
|
|
||||||
private BigDecimal openPrice;
|
|
||||||
|
|
||||||
private BigDecimal yesterdayClose;
|
|
||||||
|
|
||||||
private String updateTime;
|
|
||||||
|
|
||||||
private String priceDate;
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package com.accounting.dto;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class K780Response {
|
|
||||||
@JsonProperty("success")
|
|
||||||
private String success;
|
|
||||||
|
|
||||||
@JsonProperty("result")
|
|
||||||
private K780Result result;
|
|
||||||
|
|
||||||
@JsonProperty("msg")
|
|
||||||
private String message;
|
|
||||||
}
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
package com.accounting.dto;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class K780Result {
|
|
||||||
@JsonProperty("dtQuery")
|
|
||||||
private String dtQuery;
|
|
||||||
|
|
||||||
@JsonProperty("dtCount")
|
|
||||||
private String dtCount;
|
|
||||||
|
|
||||||
@JsonProperty("dtList")
|
|
||||||
private Map<String, GoldPriceData> dtList;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class GoldPriceData {
|
|
||||||
@JsonProperty("goldid")
|
|
||||||
private String goldId;
|
|
||||||
|
|
||||||
@JsonProperty("variety")
|
|
||||||
private String variety;
|
|
||||||
|
|
||||||
@JsonProperty("varietynm")
|
|
||||||
private String varietynm;
|
|
||||||
|
|
||||||
@JsonProperty("last_price")
|
|
||||||
private String lastPrice;
|
|
||||||
|
|
||||||
@JsonProperty("buy_price")
|
|
||||||
private String buyPrice;
|
|
||||||
|
|
||||||
@JsonProperty("sell_price")
|
|
||||||
private String sellPrice;
|
|
||||||
|
|
||||||
@JsonProperty("open_price")
|
|
||||||
private String openPrice;
|
|
||||||
|
|
||||||
@JsonProperty("yesy_price")
|
|
||||||
private String yesyPrice;
|
|
||||||
|
|
||||||
@JsonProperty("high_price")
|
|
||||||
private String highPrice;
|
|
||||||
|
|
||||||
@JsonProperty("low_price")
|
|
||||||
private String lowPrice;
|
|
||||||
|
|
||||||
@JsonProperty("change_price")
|
|
||||||
private String changePrice;
|
|
||||||
|
|
||||||
@JsonProperty("change_margin")
|
|
||||||
private String changeMargin;
|
|
||||||
|
|
||||||
@JsonProperty("uptime")
|
|
||||||
private String uptime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
package com.accounting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("account")
|
|
||||||
public class Account {
|
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
private Long userId;
|
|
||||||
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
private BigDecimal initialBalance;
|
|
||||||
|
|
||||||
private LocalDateTime createTime;
|
|
||||||
|
|
||||||
private LocalDateTime updateTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -17,8 +17,6 @@ public class Bill {
|
|||||||
|
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
private Long accountId;
|
|
||||||
|
|
||||||
private Long categoryId;
|
private Long categoryId;
|
||||||
|
|
||||||
private BigDecimal amount;
|
private BigDecimal amount;
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
package com.accounting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("budget")
|
|
||||||
public class Budget {
|
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
private Long userId;
|
|
||||||
|
|
||||||
private Integer year;
|
|
||||||
|
|
||||||
private Integer month;
|
|
||||||
|
|
||||||
private BigDecimal amount;
|
|
||||||
|
|
||||||
private LocalDateTime createTime;
|
|
||||||
|
|
||||||
private LocalDateTime updateTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
package com.accounting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("gold_price")
|
|
||||||
public class GoldPrice {
|
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
private String goldId;
|
|
||||||
|
|
||||||
private String goldName;
|
|
||||||
|
|
||||||
private BigDecimal price;
|
|
||||||
|
|
||||||
private BigDecimal priceChange;
|
|
||||||
|
|
||||||
private BigDecimal priceChangePercent;
|
|
||||||
|
|
||||||
private BigDecimal highPrice;
|
|
||||||
|
|
||||||
private BigDecimal lowPrice;
|
|
||||||
|
|
||||||
private BigDecimal openPrice;
|
|
||||||
|
|
||||||
private BigDecimal yesterdayClose;
|
|
||||||
|
|
||||||
private LocalDateTime updateTime;
|
|
||||||
|
|
||||||
private LocalDate priceDate;
|
|
||||||
|
|
||||||
private LocalDateTime createTime;
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package com.accounting.mapper;
|
|
||||||
|
|
||||||
import com.accounting.entity.Account;
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface AccountMapper extends BaseMapper<Account> {
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package com.accounting.mapper;
|
|
||||||
|
|
||||||
import com.accounting.entity.Budget;
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface BudgetMapper extends BaseMapper<Budget> {
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
package com.accounting.mapper;
|
|
||||||
|
|
||||||
import com.accounting.entity.GoldPrice;
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface GoldPriceMapper extends BaseMapper<GoldPrice> {
|
|
||||||
|
|
||||||
@Select("SELECT * FROM gold_price WHERE gold_id = #{goldId} ORDER BY price_date DESC LIMIT #{limit}")
|
|
||||||
List<GoldPrice> selectHistoryByGoldId(@Param("goldId") String goldId, @Param("limit") Integer limit);
|
|
||||||
|
|
||||||
@Select("SELECT * FROM gold_price WHERE gold_id = #{goldId} AND price_date = #{date} ORDER BY update_time ASC")
|
|
||||||
List<GoldPrice> selectByDate(@Param("goldId") String goldId, @Param("date") LocalDate date);
|
|
||||||
|
|
||||||
@Select("SELECT MAX(price_date) FROM gold_price WHERE gold_id = #{goldId}")
|
|
||||||
LocalDate selectLatestDate(@Param("goldId") String goldId);
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package com.accounting.schedule;
|
|
||||||
|
|
||||||
import com.accounting.service.GoldPriceService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class GoldPriceScheduler {
|
|
||||||
|
|
||||||
private static final String DEFAULT_GOLD_ID = "1053";
|
|
||||||
|
|
||||||
private final GoldPriceService goldPriceService;
|
|
||||||
|
|
||||||
@Scheduled(cron = "0 */15 * * * ?", zone = "Asia/Shanghai")
|
|
||||||
public void fetchPeriodicPrice() {
|
|
||||||
runJob("periodic");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runJob(String tag) {
|
|
||||||
try {
|
|
||||||
log.info("Gold price scheduled job [{}] start", tag);
|
|
||||||
goldPriceService.refreshCurrentPrice(DEFAULT_GOLD_ID);
|
|
||||||
log.info("Gold price scheduled job [{}] success", tag);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Gold price scheduled job [{}] failed", tag, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
package com.accounting.service;
|
|
||||||
|
|
||||||
import com.accounting.dto.AccountRequest;
|
|
||||||
import com.accounting.dto.AccountResponse;
|
|
||||||
import com.accounting.entity.Account;
|
|
||||||
import com.accounting.entity.Bill;
|
|
||||||
import com.accounting.mapper.AccountMapper;
|
|
||||||
import com.accounting.mapper.BillMapper;
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import org.springframework.beans.BeanUtils;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class AccountService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AccountMapper accountMapper;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private BillMapper billMapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取或创建用户账户(如果不存在则创建默认账户)
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public Account getOrCreateAccount(Long userId) {
|
|
||||||
Account account = accountMapper.selectOne(
|
|
||||||
new LambdaQueryWrapper<Account>()
|
|
||||||
.eq(Account::getUserId, userId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (account == null) {
|
|
||||||
account = new Account();
|
|
||||||
account.setUserId(userId);
|
|
||||||
account.setName("默认账户");
|
|
||||||
account.setInitialBalance(BigDecimal.ZERO);
|
|
||||||
accountMapper.insert(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户账户
|
|
||||||
*/
|
|
||||||
public Account getAccount(Long userId) {
|
|
||||||
return accountMapper.selectOne(
|
|
||||||
new LambdaQueryWrapper<Account>()
|
|
||||||
.eq(Account::getUserId, userId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新账户信息(主要是初始余额)
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public AccountResponse updateAccount(Long userId, AccountRequest request) {
|
|
||||||
Account account = getOrCreateAccount(userId);
|
|
||||||
|
|
||||||
if (request.getName() != null) {
|
|
||||||
account.setName(request.getName());
|
|
||||||
}
|
|
||||||
if (request.getInitialBalance() != null) {
|
|
||||||
account.setInitialBalance(request.getInitialBalance());
|
|
||||||
}
|
|
||||||
|
|
||||||
accountMapper.updateById(account);
|
|
||||||
|
|
||||||
return getAccountBalance(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新账户余额(仅更新初始余额)
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public AccountResponse updateAccountBalance(Long userId, BigDecimal initialBalance) {
|
|
||||||
Account account = getOrCreateAccount(userId);
|
|
||||||
account.setInitialBalance(initialBalance);
|
|
||||||
accountMapper.updateById(account);
|
|
||||||
return getAccountBalance(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算账户余额(初始余额 + 收入总额 - 支出总额)
|
|
||||||
*/
|
|
||||||
public BigDecimal calculateBalance(Long accountId) {
|
|
||||||
Account account = accountMapper.selectById(accountId);
|
|
||||||
if (account == null) {
|
|
||||||
return BigDecimal.ZERO;
|
|
||||||
}
|
|
||||||
|
|
||||||
BigDecimal initialBalance = account.getInitialBalance() != null ? account.getInitialBalance() : BigDecimal.ZERO;
|
|
||||||
|
|
||||||
// 查询该账户的所有账单
|
|
||||||
List<Bill> bills = billMapper.selectList(
|
|
||||||
new LambdaQueryWrapper<Bill>()
|
|
||||||
.eq(Bill::getAccountId, accountId)
|
|
||||||
);
|
|
||||||
|
|
||||||
BigDecimal totalIncome = BigDecimal.ZERO;
|
|
||||||
BigDecimal totalExpense = BigDecimal.ZERO;
|
|
||||||
|
|
||||||
for (Bill bill : bills) {
|
|
||||||
if (bill.getType() != null && bill.getAmount() != null) {
|
|
||||||
if (bill.getType() == 2) { // 收入
|
|
||||||
totalIncome = totalIncome.add(bill.getAmount());
|
|
||||||
} else if (bill.getType() == 1) { // 支出
|
|
||||||
totalExpense = totalExpense.add(bill.getAmount().abs());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return initialBalance.add(totalIncome).subtract(totalExpense);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取账户余额信息(包含余额、总收入、总支出)
|
|
||||||
*/
|
|
||||||
public AccountResponse getAccountBalance(Long userId) {
|
|
||||||
Account account = getOrCreateAccount(userId);
|
|
||||||
|
|
||||||
AccountResponse response = new AccountResponse();
|
|
||||||
BeanUtils.copyProperties(account, response);
|
|
||||||
|
|
||||||
BigDecimal balance = calculateBalance(account.getId());
|
|
||||||
response.setBalance(balance);
|
|
||||||
|
|
||||||
// 计算总收入
|
|
||||||
List<Bill> incomeBills = billMapper.selectList(
|
|
||||||
new LambdaQueryWrapper<Bill>()
|
|
||||||
.eq(Bill::getAccountId, account.getId())
|
|
||||||
.eq(Bill::getType, 2) // 收入
|
|
||||||
);
|
|
||||||
BigDecimal totalIncome = incomeBills.stream()
|
|
||||||
.map(Bill::getAmount)
|
|
||||||
.filter(amount -> amount != null)
|
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
response.setTotalIncome(totalIncome);
|
|
||||||
|
|
||||||
// 计算总支出
|
|
||||||
List<Bill> expenseBills = billMapper.selectList(
|
|
||||||
new LambdaQueryWrapper<Bill>()
|
|
||||||
.eq(Bill::getAccountId, account.getId())
|
|
||||||
.eq(Bill::getType, 1) // 支出
|
|
||||||
);
|
|
||||||
BigDecimal totalExpense = expenseBills.stream()
|
|
||||||
.map(Bill::getAmount)
|
|
||||||
.map(BigDecimal::abs)
|
|
||||||
.filter(amount -> amount != null)
|
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
response.setTotalExpense(totalExpense);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -2,8 +2,6 @@ package com.accounting.service;
|
|||||||
|
|
||||||
import com.accounting.dto.BillRequest;
|
import com.accounting.dto.BillRequest;
|
||||||
import com.accounting.dto.BillResponse;
|
import com.accounting.dto.BillResponse;
|
||||||
import com.accounting.dto.BatchBillRequest;
|
|
||||||
import com.accounting.entity.Account;
|
|
||||||
import com.accounting.entity.Bill;
|
import com.accounting.entity.Bill;
|
||||||
import com.accounting.entity.Category;
|
import com.accounting.entity.Category;
|
||||||
import com.accounting.mapper.BillMapper;
|
import com.accounting.mapper.BillMapper;
|
||||||
@ -15,7 +13,6 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -28,9 +25,6 @@ public class BillService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private CategoryMapper categoryMapper;
|
private CategoryMapper categoryMapper;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AccountService accountService;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public BillResponse createBill(BillRequest request, Long userId) {
|
public BillResponse createBill(BillRequest request, Long userId) {
|
||||||
// 验证分类是否存在
|
// 验证分类是否存在
|
||||||
@ -39,13 +33,9 @@ public class BillService {
|
|||||||
throw new RuntimeException("分类不存在");
|
throw new RuntimeException("分类不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取或创建账户
|
|
||||||
Account account = accountService.getOrCreateAccount(userId);
|
|
||||||
|
|
||||||
// 创建账单
|
// 创建账单
|
||||||
Bill bill = new Bill();
|
Bill bill = new Bill();
|
||||||
bill.setUserId(userId);
|
bill.setUserId(userId);
|
||||||
bill.setAccountId(account.getId()); // 自动关联账户
|
|
||||||
bill.setCategoryId(request.getCategoryId());
|
bill.setCategoryId(request.getCategoryId());
|
||||||
bill.setAmount(request.getAmount());
|
bill.setAmount(request.getAmount());
|
||||||
bill.setDescription(request.getDescription());
|
bill.setDescription(request.getDescription());
|
||||||
@ -58,39 +48,6 @@ public class BillService {
|
|||||||
return convertToResponse(bill, category);
|
return convertToResponse(bill, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public List<BillResponse> createBills(BatchBillRequest request, Long userId) {
|
|
||||||
List<BillResponse> responses = new ArrayList<>();
|
|
||||||
|
|
||||||
// 获取或创建账户
|
|
||||||
Account account = accountService.getOrCreateAccount(userId);
|
|
||||||
|
|
||||||
for (BillRequest billRequest : request.getBills()) {
|
|
||||||
// 验证分类是否存在
|
|
||||||
Category category = categoryMapper.selectById(billRequest.getCategoryId());
|
|
||||||
if (category == null) {
|
|
||||||
throw new RuntimeException("分类不存在,分类ID: " + billRequest.getCategoryId());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建账单
|
|
||||||
Bill bill = new Bill();
|
|
||||||
bill.setUserId(userId);
|
|
||||||
bill.setAccountId(account.getId()); // 自动关联账户
|
|
||||||
bill.setCategoryId(billRequest.getCategoryId());
|
|
||||||
bill.setAmount(billRequest.getAmount());
|
|
||||||
bill.setDescription(billRequest.getDescription());
|
|
||||||
bill.setBillDate(billRequest.getBillDate() != null ? billRequest.getBillDate() : LocalDate.now());
|
|
||||||
bill.setImageUrl(billRequest.getImageUrl());
|
|
||||||
bill.setType(billRequest.getType()); // 设置账单类型
|
|
||||||
|
|
||||||
billMapper.insert(bill);
|
|
||||||
|
|
||||||
responses.add(convertToResponse(bill, category));
|
|
||||||
}
|
|
||||||
|
|
||||||
return responses;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public BillResponse updateBill(Long id, BillRequest request, Long userId) {
|
public BillResponse updateBill(Long id, BillRequest request, Long userId) {
|
||||||
// 验证账单是否存在且属于当前用户
|
// 验证账单是否存在且属于当前用户
|
||||||
@ -110,12 +67,6 @@ public class BillService {
|
|||||||
throw new RuntimeException("分类不存在");
|
throw new RuntimeException("分类不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果账单没有关联账户,则自动关联
|
|
||||||
if (bill.getAccountId() == null) {
|
|
||||||
Account account = accountService.getOrCreateAccount(userId);
|
|
||||||
bill.setAccountId(account.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新账单
|
// 更新账单
|
||||||
bill.setCategoryId(request.getCategoryId());
|
bill.setCategoryId(request.getCategoryId());
|
||||||
bill.setAmount(request.getAmount());
|
bill.setAmount(request.getAmount());
|
||||||
|
|||||||
@ -1,191 +0,0 @@
|
|||||||
package com.accounting.service;
|
|
||||||
|
|
||||||
import com.accounting.dto.BudgetRequest;
|
|
||||||
import com.accounting.dto.BudgetResponse;
|
|
||||||
import com.accounting.dto.BudgetSettlementResponse;
|
|
||||||
import com.accounting.dto.StatisticsResponse;
|
|
||||||
import com.accounting.entity.Budget;
|
|
||||||
import com.accounting.mapper.BudgetMapper;
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class BudgetService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private BudgetMapper budgetMapper;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private StatisticsService statisticsService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取或创建预算(如果不存在则创建默认0)
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public Budget getOrCreateBudget(Long userId, int year, int month) {
|
|
||||||
Budget budget = budgetMapper.selectOne(
|
|
||||||
new LambdaQueryWrapper<Budget>()
|
|
||||||
.eq(Budget::getUserId, userId)
|
|
||||||
.eq(Budget::getYear, year)
|
|
||||||
.eq(Budget::getMonth, month)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (budget == null) {
|
|
||||||
budget = new Budget();
|
|
||||||
budget.setUserId(userId);
|
|
||||||
budget.setYear(year);
|
|
||||||
budget.setMonth(month);
|
|
||||||
budget.setAmount(BigDecimal.ZERO);
|
|
||||||
budgetMapper.insert(budget);
|
|
||||||
}
|
|
||||||
|
|
||||||
return budget;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取预算
|
|
||||||
*/
|
|
||||||
public Budget getBudget(Long userId, int year, int month) {
|
|
||||||
return budgetMapper.selectOne(
|
|
||||||
new LambdaQueryWrapper<Budget>()
|
|
||||||
.eq(Budget::getUserId, userId)
|
|
||||||
.eq(Budget::getYear, year)
|
|
||||||
.eq(Budget::getMonth, month)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置/更新预算
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public BudgetResponse setBudget(Long userId, BudgetRequest request) {
|
|
||||||
int year = request.getYear();
|
|
||||||
int month = request.getMonth();
|
|
||||||
BigDecimal amount = request.getAmount();
|
|
||||||
|
|
||||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
|
|
||||||
throw new RuntimeException("预算金额必须大于等于0");
|
|
||||||
}
|
|
||||||
|
|
||||||
Budget budget = getOrCreateBudget(userId, year, month);
|
|
||||||
budget.setAmount(amount);
|
|
||||||
budgetMapper.updateById(budget);
|
|
||||||
|
|
||||||
return getBudgetWithStatistics(userId, year, month);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取预算及统计信息
|
|
||||||
*/
|
|
||||||
public BudgetResponse getBudgetWithStatistics(Long userId, int year, int month) {
|
|
||||||
Budget budget = getOrCreateBudget(userId, year, month);
|
|
||||||
|
|
||||||
BudgetResponse response = new BudgetResponse();
|
|
||||||
response.setId(budget.getId());
|
|
||||||
response.setYear(budget.getYear());
|
|
||||||
response.setMonth(budget.getMonth());
|
|
||||||
response.setAmount(budget.getAmount());
|
|
||||||
|
|
||||||
// 获取本月支出统计
|
|
||||||
StatisticsResponse stats = statisticsService.getMonthlyStatistics(userId, year, month);
|
|
||||||
BigDecimal usedAmount = stats.getTotalExpense() != null ? stats.getTotalExpense() : BigDecimal.ZERO;
|
|
||||||
response.setUsedAmount(usedAmount);
|
|
||||||
|
|
||||||
// 计算剩余预算
|
|
||||||
BigDecimal remainingAmount = budget.getAmount().subtract(usedAmount);
|
|
||||||
response.setRemainingAmount(remainingAmount);
|
|
||||||
|
|
||||||
// 计算剩余日均
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
LocalDate monthStart = LocalDate.of(year, month, 1);
|
|
||||||
LocalDate monthEnd = monthStart.withDayOfMonth(monthStart.lengthOfMonth());
|
|
||||||
|
|
||||||
int remainingDays = 0;
|
|
||||||
if (year == today.getYear() && month == today.getMonthValue()) {
|
|
||||||
// 当前月份
|
|
||||||
remainingDays = monthEnd.getDayOfMonth() - today.getDayOfMonth() + 1;
|
|
||||||
if (remainingDays < 0) {
|
|
||||||
remainingDays = 0;
|
|
||||||
}
|
|
||||||
} else if (year < today.getYear() || (year == today.getYear() && month < today.getMonthValue())) {
|
|
||||||
// 过去的月份
|
|
||||||
remainingDays = 0;
|
|
||||||
} else {
|
|
||||||
// 未来的月份
|
|
||||||
remainingDays = monthEnd.getDayOfMonth();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remainingDays > 0) {
|
|
||||||
BigDecimal remainingDaily = remainingAmount.divide(BigDecimal.valueOf(remainingDays), 2, RoundingMode.HALF_UP);
|
|
||||||
response.setRemainingDaily(remainingDaily);
|
|
||||||
} else {
|
|
||||||
response.setRemainingDaily(BigDecimal.ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取上月预算结算信息
|
|
||||||
*/
|
|
||||||
public BudgetSettlementResponse getLastMonthSettlement(Long userId) {
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
LocalDate lastMonth = today.minusMonths(1);
|
|
||||||
int year = lastMonth.getYear();
|
|
||||||
int month = lastMonth.getMonthValue();
|
|
||||||
|
|
||||||
Budget budget = getBudget(userId, year, month);
|
|
||||||
BigDecimal budgetAmount = budget != null && budget.getAmount() != null ? budget.getAmount() : BigDecimal.ZERO;
|
|
||||||
|
|
||||||
// 获取上月支出统计
|
|
||||||
StatisticsResponse stats = statisticsService.getMonthlyStatistics(userId, year, month);
|
|
||||||
BigDecimal actualExpense = stats.getTotalExpense() != null ? stats.getTotalExpense() : BigDecimal.ZERO;
|
|
||||||
|
|
||||||
BudgetSettlementResponse response = new BudgetSettlementResponse();
|
|
||||||
response.setYear(year);
|
|
||||||
response.setMonth(month);
|
|
||||||
response.setBudgetAmount(budgetAmount);
|
|
||||||
response.setActualExpense(actualExpense);
|
|
||||||
|
|
||||||
// 判断是否超支
|
|
||||||
boolean isOverBudget = actualExpense.compareTo(budgetAmount) > 0;
|
|
||||||
response.setIsOverBudget(isOverBudget);
|
|
||||||
|
|
||||||
if (isOverBudget) {
|
|
||||||
BigDecimal overAmount = actualExpense.subtract(budgetAmount);
|
|
||||||
response.setOverAmount(overAmount);
|
|
||||||
} else {
|
|
||||||
response.setOverAmount(BigDecimal.ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算完成率
|
|
||||||
if (budgetAmount.compareTo(BigDecimal.ZERO) > 0) {
|
|
||||||
BigDecimal completionRate = actualExpense.divide(budgetAmount, 4, RoundingMode.HALF_UP)
|
|
||||||
.multiply(BigDecimal.valueOf(100));
|
|
||||||
response.setCompletionRate(completionRate);
|
|
||||||
} else {
|
|
||||||
response.setCompletionRate(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查并创建本月预算(如果不存在则创建默认0)
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public void checkAndCreateCurrentMonthBudget(Long userId) {
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
int year = today.getYear();
|
|
||||||
int month = today.getMonthValue();
|
|
||||||
|
|
||||||
getOrCreateBudget(userId, year, month);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
package com.accounting.service;
|
|
||||||
|
|
||||||
import com.accounting.dto.GoldPriceResponse;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface GoldPriceService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从数据库获取最新黄金价格(不触发外部接口调用)
|
|
||||||
* @param goldId 黄金品种ID
|
|
||||||
* @return 最新黄金价格响应,可能为 null
|
|
||||||
*/
|
|
||||||
GoldPriceResponse getCurrentPrice(String goldId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主动从外部接口刷新当前黄金价格并写入数据库(供定时任务使用)
|
|
||||||
* @param goldId 黄金品种ID
|
|
||||||
*/
|
|
||||||
void refreshCurrentPrice(String goldId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取历史黄金价格
|
|
||||||
* @param goldId 黄金品种ID
|
|
||||||
* @param days 查询天数
|
|
||||||
* @return 黄金价格列表
|
|
||||||
*/
|
|
||||||
List<GoldPriceResponse> getHistoryPrices(String goldId, Integer days);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定日期的所有黄金价格记录
|
|
||||||
* @param goldId 黄金品种ID
|
|
||||||
* @param date 日期
|
|
||||||
* @return 指定日期的价格记录列表
|
|
||||||
*/
|
|
||||||
List<GoldPriceResponse> getPricesByDate(String goldId, LocalDate date);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取最近有数据的日期
|
|
||||||
* @param goldId 黄金品种ID
|
|
||||||
* @return 最近有数据的日期,可能为 null
|
|
||||||
*/
|
|
||||||
LocalDate getLatestDate(String goldId);
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
package com.accounting.service;
|
|
||||||
|
|
||||||
import com.accounting.dto.K780Response;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class K780ApiClient {
|
|
||||||
|
|
||||||
private static final String API_BASE_URL = "https://sapi.k780.com";
|
|
||||||
private static final String APP = "finance.gold_price";
|
|
||||||
private static final String APP_KEY = "78346";
|
|
||||||
private static final String SIGN = "1b502c535927b66d9b888a6d4701bf72";
|
|
||||||
private static final String FORMAT = "json";
|
|
||||||
|
|
||||||
private final RestTemplate restTemplate;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
public K780ApiClient() {
|
|
||||||
this.restTemplate = new RestTemplate();
|
|
||||||
this.objectMapper = new ObjectMapper();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从K780 API获取黄金价格
|
|
||||||
* @param goldId 黄金品种ID
|
|
||||||
* @return K780Response
|
|
||||||
*/
|
|
||||||
public K780Response fetchGoldPrice(String goldId) {
|
|
||||||
try {
|
|
||||||
String url = String.format("%s/?app=%s&goldid=%s&appkey=%s&sign=%s&format=%s",
|
|
||||||
API_BASE_URL, APP, goldId, APP_KEY, SIGN, FORMAT);
|
|
||||||
|
|
||||||
log.info("Fetching gold price from K780 API: {}", url);
|
|
||||||
|
|
||||||
String response = restTemplate.getForObject(url, String.class);
|
|
||||||
K780Response k780Response = objectMapper.readValue(response, K780Response.class);
|
|
||||||
|
|
||||||
log.info("K780 API response: {}", k780Response);
|
|
||||||
|
|
||||||
return k780Response;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to fetch gold price from K780 API", e);
|
|
||||||
throw new RuntimeException("获取黄金价格失败: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@ -1,188 +0,0 @@
|
|||||||
package com.accounting.service.impl;
|
|
||||||
|
|
||||||
import com.accounting.dto.GoldPriceResponse;
|
|
||||||
import com.accounting.dto.K780Response;
|
|
||||||
import com.accounting.dto.K780Result;
|
|
||||||
import com.accounting.entity.GoldPrice;
|
|
||||||
import com.accounting.mapper.GoldPriceMapper;
|
|
||||||
import com.accounting.service.GoldPriceService;
|
|
||||||
import com.accounting.service.K780ApiClient;
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class GoldPriceServiceImpl implements GoldPriceService {
|
|
||||||
|
|
||||||
private final K780ApiClient k780ApiClient;
|
|
||||||
private final GoldPriceMapper goldPriceMapper;
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
||||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public GoldPriceResponse getCurrentPrice(String goldId) {
|
|
||||||
// 仅从数据库读取最新记录
|
|
||||||
GoldPrice latest = goldPriceMapper.selectOne(
|
|
||||||
new LambdaQueryWrapper<GoldPrice>()
|
|
||||||
.eq(GoldPrice::getGoldId, goldId)
|
|
||||||
.orderByDesc(GoldPrice::getPriceDate, GoldPrice::getUpdateTime)
|
|
||||||
.last("LIMIT 1")
|
|
||||||
);
|
|
||||||
if (latest == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return convertToResponse(latest);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void refreshCurrentPrice(String goldId) {
|
|
||||||
K780Response response = k780ApiClient.fetchGoldPrice(goldId);
|
|
||||||
|
|
||||||
if (!"1".equals(response.getSuccess()) || response.getResult() == null) {
|
|
||||||
throw new RuntimeException("获取黄金价格失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
K780Result result = response.getResult();
|
|
||||||
K780Result.GoldPriceData goldPriceData = extractGoldPriceData(result, goldId);
|
|
||||||
GoldPrice goldPrice = convertToEntity(goldPriceData);
|
|
||||||
|
|
||||||
// 插入新记录 每15分钟查询一次结果并存储
|
|
||||||
goldPrice.setCreateTime(LocalDateTime.now());
|
|
||||||
goldPriceMapper.insert(goldPrice);
|
|
||||||
|
|
||||||
log.info("Refreshed gold price for goldId={}, priceDate={}", goldPrice.getGoldId(), goldPrice.getPriceDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<GoldPriceResponse> getHistoryPrices(String goldId, Integer days) {
|
|
||||||
int limit = (days != null && days > 0) ? Math.min(days, 30) : 30;
|
|
||||||
List<GoldPrice> prices = goldPriceMapper.selectHistoryByGoldId(goldId, limit);
|
|
||||||
return prices.stream()
|
|
||||||
.map(this::convertToResponse)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public List<GoldPriceResponse> getPricesByDate(String goldId, LocalDate date) {
|
|
||||||
List<GoldPrice> prices = goldPriceMapper.selectByDate(goldId, date);
|
|
||||||
return prices.stream()
|
|
||||||
.map(this::convertToResponse)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public LocalDate getLatestDate(String goldId) {
|
|
||||||
return goldPriceMapper.selectLatestDate(goldId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private K780Result.GoldPriceData extractGoldPriceData(K780Result result, String goldId) {
|
|
||||||
if (result.getDtList() == null || result.getDtList().isEmpty()) {
|
|
||||||
throw new RuntimeException("黄金价格数据为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试通过提供的goldId获取数据
|
|
||||||
K780Result.GoldPriceData data = result.getDtList().get(goldId);
|
|
||||||
if (data == null) {
|
|
||||||
// 如果没找到指定goldId的数据,则取第一个可用数据
|
|
||||||
data = result.getDtList().values().stream().findFirst()
|
|
||||||
.orElseThrow(() -> new RuntimeException("未找到黄金价格数据"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GoldPrice convertToEntity(K780Result.GoldPriceData data) {
|
|
||||||
GoldPrice entity = new GoldPrice();
|
|
||||||
|
|
||||||
// 确保 goldId 不为 null
|
|
||||||
String goldId = data.getGoldId();
|
|
||||||
if (goldId == null || goldId.isEmpty()) {
|
|
||||||
throw new RuntimeException("黄金品种ID不能为空");
|
|
||||||
}
|
|
||||||
entity.setGoldId(goldId);
|
|
||||||
|
|
||||||
entity.setGoldName(data.getVarietynm());
|
|
||||||
entity.setPrice(parseBigDecimal(data.getLastPrice()));
|
|
||||||
entity.setPriceChange(parseBigDecimal(data.getChangePrice()));
|
|
||||||
entity.setOpenPrice(parseBigDecimal(data.getOpenPrice()));
|
|
||||||
entity.setHighPrice(parseBigDecimal(data.getHighPrice()));
|
|
||||||
entity.setLowPrice(parseBigDecimal(data.getLowPrice()));
|
|
||||||
entity.setYesterdayClose(parseBigDecimal(data.getYesyPrice()));
|
|
||||||
|
|
||||||
// 处理涨跌百分比
|
|
||||||
String changeMargin = data.getChangeMargin();
|
|
||||||
if (changeMargin != null && !changeMargin.isEmpty() && !"-".equals(changeMargin)) {
|
|
||||||
// 移除百分号并转换
|
|
||||||
String marginWithoutPercent = changeMargin.replace("%", "");
|
|
||||||
entity.setPriceChangePercent(parseBigDecimal(marginWithoutPercent));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析更新时间
|
|
||||||
if (data.getUptime() != null && !data.getUptime().isEmpty()) {
|
|
||||||
try {
|
|
||||||
entity.setUpdateTime(LocalDateTime.parse(data.getUptime(), DATE_TIME_FORMATTER));
|
|
||||||
entity.setPriceDate(entity.getUpdateTime().toLocalDate());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Failed to parse update time: {}", data.getUptime());
|
|
||||||
entity.setUpdateTime(LocalDateTime.now());
|
|
||||||
entity.setPriceDate(LocalDate.now());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
entity.setUpdateTime(LocalDateTime.now());
|
|
||||||
entity.setPriceDate(LocalDate.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GoldPriceResponse convertToResponse(GoldPrice entity) {
|
|
||||||
GoldPriceResponse response = new GoldPriceResponse();
|
|
||||||
response.setGoldId(entity.getGoldId());
|
|
||||||
response.setGoldName(entity.getGoldName());
|
|
||||||
response.setPrice(entity.getPrice());
|
|
||||||
response.setPriceChange(entity.getPriceChange());
|
|
||||||
response.setPriceChangePercent(entity.getPriceChangePercent());
|
|
||||||
response.setHighPrice(entity.getHighPrice());
|
|
||||||
response.setLowPrice(entity.getLowPrice());
|
|
||||||
response.setOpenPrice(entity.getOpenPrice());
|
|
||||||
response.setYesterdayClose(entity.getYesterdayClose());
|
|
||||||
|
|
||||||
if (entity.getUpdateTime() != null) {
|
|
||||||
response.setUpdateTime(entity.getUpdateTime().format(DATE_TIME_FORMATTER));
|
|
||||||
}
|
|
||||||
if (entity.getPriceDate() != null) {
|
|
||||||
response.setPriceDate(entity.getPriceDate().format(DATE_FORMATTER));
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BigDecimal parseBigDecimal(String value) {
|
|
||||||
if (value == null || value.isEmpty() || "-".equals(value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return new BigDecimal(value);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
log.warn("Failed to parse BigDecimal: {}", value);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,7 +9,10 @@ import java.time.LocalDate;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.format.DateTimeParseException;
|
import java.time.format.DateTimeParseException;
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@ -20,10 +23,8 @@ public class OcrAmountParser {
|
|||||||
"[¥¥]?\\s*(\\d{1,10}(\\.\\d{1,2})?)\\s*[元]?"
|
"[¥¥]?\\s*(\\d{1,10}(\\.\\d{1,2})?)\\s*[元]?"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 日期正则表达式:匹配字符串中是否含有月或日
|
||||||
|
private static final Pattern UNION_DATE_PATTERN = Pattern.compile(".*月.*日.*");
|
||||||
// 日期正则表达式:匹配字符串中是否含有月、日或天
|
|
||||||
private static final Pattern UNION_DATE_PATTERN = Pattern.compile(".*(今天|昨天).*");
|
|
||||||
|
|
||||||
|
|
||||||
// 日期正则表达式:匹配 12月2日13:14 这样的格式
|
// 日期正则表达式:匹配 12月2日13:14 这样的格式
|
||||||
@ -31,11 +32,6 @@ public class OcrAmountParser {
|
|||||||
"(\\d{1,2})月(\\d{1,2})日\\s*(\\d{1,2})[::](\\d{1,2})"
|
"(\\d{1,2})月(\\d{1,2})日\\s*(\\d{1,2})[::](\\d{1,2})"
|
||||||
);
|
);
|
||||||
|
|
||||||
// 日期正则表达式:匹配 12-11 13:14 这样的格式
|
|
||||||
private static final Pattern DATE_PATTERN_DASH = Pattern.compile(
|
|
||||||
"(\\d{1,2})-(\\d{1,2})\\s*(\\d{1,2})[::](\\d{1,2})"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 商户名称关键词(常见支付平台)
|
// 商户名称关键词(常见支付平台)
|
||||||
private static final String[] MERCHANT_KEYWORDS = {
|
private static final String[] MERCHANT_KEYWORDS = {
|
||||||
"微信支付", "支付宝", "收款", "付款", "商户", "商家", "店铺", "超市", "餐厅", "饭店"
|
"微信支付", "支付宝", "收款", "付款", "商户", "商家", "店铺", "超市", "餐厅", "饭店"
|
||||||
@ -87,19 +83,7 @@ public class OcrAmountParser {
|
|||||||
if (dateStr == null || dateStr.trim().isEmpty()) {
|
if (dateStr == null || dateStr.trim().isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 匹配今天、昨天
|
return UNION_DATE_PATTERN.matcher(dateStr.trim()).matches();
|
||||||
if (UNION_DATE_PATTERN.matcher(dateStr.trim()).matches()){
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// 匹配12-11 13:14
|
|
||||||
if (DATE_PATTERN_DASH.matcher(dateStr.trim()).matches()){
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// 匹配12月11日 13:14
|
|
||||||
if (DATE_PATTERN.matcher(dateStr.trim()).matches()){
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,90 +123,34 @@ public class OcrAmountParser {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析日期字符串为LocalDateTime对象
|
* 解析日期字符串为LocalDateTime对象
|
||||||
* @param dateStr 解析3中日期字符串,例如:"12月2日13:14" "12-11 13:14" "今天 13:14" "昨天 13:14"
|
* @param dateStr 日期字符串,例如:"12月2日13:14"
|
||||||
* @return LocalDateTime对象
|
* @return LocalDateTime对象
|
||||||
*/
|
*/
|
||||||
private static LocalDateTime parseDateTimeString(String dateStr) {
|
private static LocalDateTime parseDateTimeString(String dateStr) {
|
||||||
if (dateStr == null || dateStr.trim().isEmpty()) {
|
if (dateStr == null || dateStr.trim().isEmpty()) {
|
||||||
return LocalDateTime.now();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//先处理今天、昨天
|
try {
|
||||||
if (UNION_DATE_PATTERN.matcher(dateStr.trim()).matches()){
|
Matcher matcher = DATE_PATTERN.matcher(dateStr.trim());
|
||||||
String trimmedDateStr = dateStr.trim();
|
if (matcher.find()) {
|
||||||
LocalDateTime baseDate;
|
int month = Integer.parseInt(matcher.group(1));
|
||||||
|
int day = Integer.parseInt(matcher.group(2));
|
||||||
|
int hour = Integer.parseInt(matcher.group(3));
|
||||||
|
int minute = Integer.parseInt(matcher.group(4));
|
||||||
|
|
||||||
if (trimmedDateStr.startsWith("今天")) {
|
// 使用当前年份
|
||||||
baseDate = LocalDateTime.now();
|
int year = java.time.Year.now().getValue();
|
||||||
} else if (trimmedDateStr.startsWith("昨天")) {
|
return LocalDateTime.of(year, month, day, hour, minute);
|
||||||
baseDate = LocalDateTime.now().minusDays(1);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
// 提取时间部分
|
e.printStackTrace();
|
||||||
String timePart = trimmedDateStr.substring(2).trim(); // 去掉"今天"/"昨天"
|
|
||||||
if (!timePart.isEmpty()) {
|
|
||||||
String[] timeParts = timePart.split("[::]");
|
|
||||||
if (timeParts.length >= 2) {
|
|
||||||
try {
|
|
||||||
int hour = Integer.parseInt(timeParts[0]);
|
|
||||||
int minute = Integer.parseInt(timeParts[1]);
|
|
||||||
return baseDate.withHour(hour).withMinute(minute).withSecond(0).withNano(0);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
// 时间解析失败,返回基础日期
|
|
||||||
return baseDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return baseDate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//处理 12月2日13:14
|
|
||||||
if (DATE_PATTERN.matcher(dateStr.trim()).matches()){
|
|
||||||
try {
|
|
||||||
Matcher matcher = DATE_PATTERN.matcher(dateStr.trim());
|
|
||||||
if (matcher.find()) {
|
|
||||||
int month = Integer.parseInt(matcher.group(1));
|
|
||||||
int day = Integer.parseInt(matcher.group(2));
|
|
||||||
int hour = Integer.parseInt(matcher.group(3));
|
|
||||||
int minute = Integer.parseInt(matcher.group(4));
|
|
||||||
|
|
||||||
// 使用当前年份
|
|
||||||
int year = java.time.Year.now().getValue();
|
|
||||||
return LocalDateTime.of(year, month, day, hour, minute);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DATE_PATTERN_DASH.matcher(dateStr.trim()).matches()){
|
|
||||||
try {
|
|
||||||
Matcher matcher = DATE_PATTERN_DASH.matcher(dateStr.trim());
|
|
||||||
if (matcher.find()) {
|
|
||||||
int month = Integer.parseInt(matcher.group(1));
|
|
||||||
int day = Integer.parseInt(matcher.group(2));
|
|
||||||
int hour = Integer.parseInt(matcher.group(3));
|
|
||||||
int minute = Integer.parseInt(matcher.group(4));
|
|
||||||
|
|
||||||
// 使用当前年份
|
|
||||||
int year = java.time.Year.now().getValue();
|
|
||||||
return LocalDateTime.of(year, month, day, hour, minute);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析OCR识别结果,提取金额、商户名称、日期等信息
|
* 解析OCR识别结果,提取金额、商户名称、日期等信息
|
||||||
* 重构OCR识别结果的处理逻辑,大概思想是先识别时间 时间可能有多种格式 微信的格式为 12-12 13:14 支付宝的格式为 今天 13:14 昨天 13:14
|
|
||||||
* 其他平台的格式为 12月2日13:14 等等 识别出日期以后 日期之前的3-4个值就可能是此笔支付的其他数据 一般的格式为 [商户/描述] [金额] 可能存在的[分类] [时间]
|
|
||||||
* 所以在识别出日期后 查看前3-4个值中是否有类似金额的值
|
|
||||||
*/
|
*/
|
||||||
public static List<ParseResult> parse(String ocrResultJson) {
|
public static List<ParseResult> parse(String ocrResultJson) {
|
||||||
try {
|
try {
|
||||||
@ -236,101 +164,52 @@ public class OcrAmountParser {
|
|||||||
|
|
||||||
JSONObject dataObject = JSON.parseObject(data);
|
JSONObject dataObject = JSON.parseObject(data);
|
||||||
String content = dataObject.getString("content");
|
String content = dataObject.getString("content");
|
||||||
// String content = "下午2:01 0.3K/s必 5G ra HD ID 4G 10 C 49 D < Q搜索交易记录 搜索 全部 支出 转账 退款 订单筛选 ¥198 ¥3,092.83 ¥0.00 收支分析 设置支出预算> C 五华区皓月千里便利店 -4.20 日用百货 今天 13:30 扫收钱码付款-给快乐 -11.00 餐饮美食 今天 12:22 余额宝-收益发放 0.19 投资理财 今天 04:47 2000406014951497 -20.00 餐饮美食 昨天 22:14 扫收钱码付款-给扫码点单店主 -5.00 公共服务 昨天 21:44 蜜雪冰城920749店 -2.18 餐饮美食 TA ";
|
|
||||||
System.out.println(content);
|
|
||||||
String[] split = content.split(" ");
|
String[] split = content.split(" ");
|
||||||
|
|
||||||
System.out.println("split:"+ split);
|
|
||||||
System.out.println("split.size:"+ split.length);
|
|
||||||
|
|
||||||
ArrayList<Map<String,String>> signList = new ArrayList<>();
|
ArrayList<Map<String,String>> signList = new ArrayList<>();
|
||||||
|
|
||||||
System.out.println("开始解析");
|
|
||||||
for (int i = 1; i < split.length; i++) {
|
for (int i = 1; i < split.length; i++) {
|
||||||
String currentLine = split[i].trim();
|
String currentLine = split[i].trim();
|
||||||
|
String previousLine = split[i-1].trim();
|
||||||
|
|
||||||
// 安全地获取 i+1, i+2, i+3 位置的值
|
// 安全地获取 i+1, i+2, i+3 位置的值
|
||||||
String iPlusOne = null;
|
String iPlusOne = null;
|
||||||
String iPlusTwo = null;
|
String iPlusTwo = null;
|
||||||
String iPlusThree = null;
|
String iPlusThree = null;
|
||||||
|
|
||||||
iPlusOne = split[i - 1].trim();
|
if (i + 1 < split.length) {
|
||||||
if (i > 1) {
|
iPlusOne = split[i + 1].trim();
|
||||||
iPlusTwo = split[i - 2].trim();
|
|
||||||
}
|
}
|
||||||
if (i > 2) {
|
if (i + 2 < split.length) {
|
||||||
iPlusThree = split[i - 3].trim();
|
iPlusTwo = split[i + 2].trim();
|
||||||
|
}
|
||||||
|
if (i + 3 < split.length) {
|
||||||
|
iPlusThree = split[i + 3].trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查当前行是否符合日期格式
|
// 检查当前行是否以+或-开头
|
||||||
if (isValidDate(currentLine)) {
|
if (currentLine.startsWith("+") || currentLine.startsWith("-")) {
|
||||||
Map<String, String> signMap = new HashMap<>();
|
Map<String, String> signMap = new HashMap<>();
|
||||||
|
signMap.put("money", currentLine);
|
||||||
|
signMap.put("content", previousLine);
|
||||||
|
|
||||||
//判断前三个值是否包含了金额
|
// 检查 i+1, i+2, i+3 哪个是日期格式
|
||||||
|
String dateValue = null;
|
||||||
//plusOne是金额
|
if (i + 1 < split.length && isValidDate(iPlusOne)) {
|
||||||
if (iPlusOne != null && ((iPlusOne.startsWith("+") || iPlusOne.startsWith("-"))
|
dateValue = iPlusOne;
|
||||||
|| AMOUNT_PATTERN.matcher(Objects.requireNonNull(iPlusOne).trim()).matches())) {
|
} else if (i + 2 < split.length && isValidDate(iPlusTwo)) {
|
||||||
if (UNION_DATE_PATTERN.matcher(currentLine).matches()){
|
dateValue = iPlusTwo;
|
||||||
// 检查 i+1 是否在数组范围内
|
} else if (i + 3 < split.length && isValidDate(iPlusThree)) {
|
||||||
if (i + 1 < split.length) {
|
dateValue = iPlusThree;
|
||||||
signMap.put("data", currentLine + " " + split[i+1].trim());
|
|
||||||
} else {
|
|
||||||
signMap.put("data", currentLine);
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
signMap.put("data", currentLine);
|
|
||||||
}
|
|
||||||
signMap.put("money", iPlusOne);
|
|
||||||
signMap.put("content", iPlusTwo);
|
|
||||||
signList.add(signMap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//plusTwo是金额
|
if (dateValue != null) {
|
||||||
if (iPlusTwo != null && ((iPlusTwo.startsWith("+") || iPlusTwo.startsWith("-"))
|
signMap.put("date", dateValue);
|
||||||
|| AMOUNT_PATTERN.matcher(Objects.requireNonNull(iPlusTwo).trim()).matches())) {
|
|
||||||
if (UNION_DATE_PATTERN.matcher(currentLine).matches()){
|
|
||||||
// 检查 i+1 是否在数组范围内
|
|
||||||
if (i + 1 < split.length) {
|
|
||||||
signMap.put("data", currentLine + " " + split[i+1].trim());
|
|
||||||
} else {
|
|
||||||
signMap.put("data", currentLine);
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
signMap.put("data", currentLine);
|
|
||||||
}
|
|
||||||
signMap.put("money", iPlusTwo);
|
|
||||||
signMap.put("content", iPlusThree);
|
|
||||||
signList.add(signMap);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//plusThree是金额
|
|
||||||
if (iPlusThree != null && ((iPlusThree.startsWith("+") || iPlusThree.startsWith("-"))
|
|
||||||
|| AMOUNT_PATTERN.matcher(Objects.requireNonNull(iPlusThree).trim()).matches())) {
|
|
||||||
if (UNION_DATE_PATTERN.matcher(currentLine).matches()){
|
|
||||||
// 检查 i+1 是否在数组范围内
|
|
||||||
if (i + 1 < split.length) {
|
|
||||||
signMap.put("data", currentLine + " " + split[i+1].trim());
|
|
||||||
} else {
|
|
||||||
signMap.put("data", currentLine);
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
signMap.put("data", currentLine);
|
|
||||||
}
|
|
||||||
signMap.put("money", iPlusThree);
|
|
||||||
// 检查 i-4 是否在数组范围内
|
|
||||||
if (i - 4 >= 0) {
|
|
||||||
signMap.put("content", split[i - 4].trim());
|
|
||||||
}
|
|
||||||
signList.add(signMap);
|
|
||||||
}
|
}
|
||||||
|
signList.add(signMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("识别结束,开始对结果进行处理");
|
|
||||||
System.out.println("signList:"+signList);
|
|
||||||
|
|
||||||
//识别完成,对识别结果进行处理
|
//识别完成,对识别结果进行处理
|
||||||
|
|
||||||
ArrayList<ParseResult> parseList = new ArrayList<>();
|
ArrayList<ParseResult> parseList = new ArrayList<>();
|
||||||
@ -340,10 +219,10 @@ public class OcrAmountParser {
|
|||||||
result.setAmount(parseMoneyString(signMap.get("money")));
|
result.setAmount(parseMoneyString(signMap.get("money")));
|
||||||
}
|
}
|
||||||
if (signMap.containsKey("content")){
|
if (signMap.containsKey("content")){
|
||||||
result.setMerchant(signMap.get("content"));
|
result.setMerchant(parseMerchant(signMap.get("content")));
|
||||||
}
|
}
|
||||||
if (signMap.containsKey("data")){
|
if (signMap.containsKey("date")){
|
||||||
result.setDate(parseDateTimeString(signMap.get("data")));
|
result.setDate(parseDateTimeString(signMap.get("date")));
|
||||||
}
|
}
|
||||||
|
|
||||||
parseList.add(result);
|
parseList.add(result);
|
||||||
|
|||||||
@ -60,15 +60,8 @@ knife4j:
|
|||||||
- url: http://localhost:8080
|
- url: http://localhost:8080
|
||||||
description: 本地开发环境
|
description: 本地开发环境
|
||||||
|
|
||||||
# K780 API配置
|
|
||||||
k780:
|
|
||||||
api-url: https://sapi.k780.com
|
|
||||||
app-key: 78346
|
|
||||||
sign: 1b502c535927b66d9b888a6d4701bf72
|
|
||||||
timeout: 5000
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 12345
|
port: 8080
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
-- 删除 gold_price 表的唯一约束 uk_gold_date
|
|
||||||
-- 原因:需要支持每15分钟查询一次,同一天需要存储多条记录
|
|
||||||
-- 执行时间:2026-01-23
|
|
||||||
|
|
||||||
ALTER TABLE `gold_price` DROP INDEX `uk_gold_date`;
|
|
||||||
@ -34,7 +34,6 @@ CREATE TABLE IF NOT EXISTS `category` (
|
|||||||
CREATE TABLE IF NOT EXISTS `bill` (
|
CREATE TABLE IF NOT EXISTS `bill` (
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '账单ID',
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '账单ID',
|
||||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||||
`account_id` BIGINT COMMENT '账户ID',
|
|
||||||
`category_id` BIGINT NOT NULL COMMENT '分类ID',
|
`category_id` BIGINT NOT NULL COMMENT '分类ID',
|
||||||
`amount` DECIMAL(10,2) NOT NULL COMMENT '金额',
|
`amount` DECIMAL(10,2) NOT NULL COMMENT '金额',
|
||||||
`description` VARCHAR(255) COMMENT '描述',
|
`description` VARCHAR(255) COMMENT '描述',
|
||||||
@ -45,44 +44,12 @@ CREATE TABLE IF NOT EXISTS `bill` (
|
|||||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
INDEX `idx_user_id` (`user_id`),
|
INDEX `idx_user_id` (`user_id`),
|
||||||
INDEX `idx_account_id` (`account_id`),
|
|
||||||
INDEX `idx_category_id` (`category_id`),
|
INDEX `idx_category_id` (`category_id`),
|
||||||
INDEX `idx_bill_date` (`bill_date`),
|
INDEX `idx_bill_date` (`bill_date`),
|
||||||
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
|
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE RESTRICT,
|
|
||||||
FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ON DELETE RESTRICT
|
FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ON DELETE RESTRICT
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='账单表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='账单表';
|
||||||
|
|
||||||
-- 账户表
|
|
||||||
CREATE TABLE IF NOT EXISTS `account` (
|
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '账户ID',
|
|
||||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
|
||||||
`name` VARCHAR(50) NOT NULL DEFAULT '默认账户' COMMENT '账户名称',
|
|
||||||
`initial_balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '初始余额',
|
|
||||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
INDEX `idx_user_id` (`user_id`),
|
|
||||||
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
|
|
||||||
UNIQUE KEY `uk_user_account` (`user_id`) COMMENT '每个用户只有一个账户'
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='账户表';
|
|
||||||
|
|
||||||
-- 预算表
|
|
||||||
CREATE TABLE IF NOT EXISTS `budget` (
|
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '预算ID',
|
|
||||||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
|
||||||
`year` INT NOT NULL COMMENT '年份',
|
|
||||||
`month` INT NOT NULL COMMENT '月份(1-12)',
|
|
||||||
`amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '预算金额',
|
|
||||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
INDEX `idx_user_id` (`user_id`),
|
|
||||||
INDEX `idx_year_month` (`year`, `month`),
|
|
||||||
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
|
|
||||||
UNIQUE KEY `uk_user_year_month` (`user_id`, `year`, `month`) COMMENT '每个用户每个月只有一个预算'
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='预算表';
|
|
||||||
|
|
||||||
-- OCR记录表
|
-- OCR记录表
|
||||||
CREATE TABLE IF NOT EXISTS `ocr_record` (
|
CREATE TABLE IF NOT EXISTS `ocr_record` (
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'OCR记录ID',
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'OCR记录ID',
|
||||||
@ -102,27 +69,6 @@ CREATE TABLE IF NOT EXISTS `ocr_record` (
|
|||||||
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
|
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='OCR记录表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='OCR记录表';
|
||||||
|
|
||||||
-- 黄金价格记录表
|
|
||||||
CREATE TABLE IF NOT EXISTS `gold_price` (
|
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '记录ID',
|
|
||||||
`gold_id` VARCHAR(20) NOT NULL COMMENT '黄金品种ID',
|
|
||||||
`gold_name` VARCHAR(50) COMMENT '黄金品种名称',
|
|
||||||
`price` DECIMAL(10,2) NOT NULL COMMENT '当前价格',
|
|
||||||
`price_change` DECIMAL(10,2) COMMENT '涨跌额',
|
|
||||||
`price_change_percent` DECIMAL(5,2) COMMENT '涨跌百分比',
|
|
||||||
`high_price` DECIMAL(10,2) COMMENT '最高价',
|
|
||||||
`low_price` DECIMAL(10,2) COMMENT '最低价',
|
|
||||||
`open_price` DECIMAL(10,2) COMMENT '开盘价',
|
|
||||||
`yesterday_close` DECIMAL(10,2) COMMENT '昨收价',
|
|
||||||
`update_time` DATETIME NOT NULL COMMENT 'API返回的更新时间',
|
|
||||||
`price_date` DATE NOT NULL COMMENT '价格日期',
|
|
||||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
INDEX `idx_gold_id` (`gold_id`),
|
|
||||||
INDEX `idx_price_date` (`price_date`),
|
|
||||||
INDEX `idx_gold_date` (`gold_id`, `price_date`) COMMENT '索引:用于按日期查询'
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='黄金价格记录表';
|
|
||||||
|
|
||||||
-- 插入预设分类(支出)
|
-- 插入预设分类(支出)
|
||||||
INSERT INTO `category` (`user_id`, `name`, `icon`, `type`, `sort_order`) VALUES
|
INSERT INTO `category` (`user_id`, `name`, `icon`, `type`, `sort_order`) VALUES
|
||||||
(NULL, '餐饮', '🍔', 1, 1),
|
(NULL, '餐饮', '🍔', 1, 1),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user