新增黄金价格查看功能,通过定时任务每15分钟查询一次最新的黄金价格;修改账户余额和预算的修改位置。

This commit is contained in:
ni ziyi 2026-01-23 20:39:50 +08:00
parent 9641dcaf9a
commit c765ffaa89
21 changed files with 774 additions and 9 deletions

View File

@ -0,0 +1,15 @@
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;
}

View File

@ -0,0 +1,18 @@
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);
}
}

View File

@ -0,0 +1,10 @@
package com.accounting.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
public class SchedulerConfig {
}

View File

@ -35,6 +35,7 @@ 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()
) )
@ -56,10 +57,14 @@ public class SecurityConfig {
@Bean @Bean
public CorsConfigurationSource corsConfigurationSource() { public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration(); CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*")); // 允许你的前端域名访问根据实际情况修改
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedOrigins(Arrays.asList(
"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(false); configuration.setAllowCredentials(true); // 允许携带认证信息
configuration.setMaxAge(3600L); configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

View File

@ -42,6 +42,16 @@ public class AccountController {
return ResponseEntity.ok(response); 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) { private Long getUserId(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal(); UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String username = userDetails.getUsername(); String username = userDetails.getUsername();

View File

@ -54,6 +54,19 @@ public class BudgetController {
return ResponseEntity.ok(response); 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) { private Long getUserId(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal(); UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String username = userDetails.getUsername(); String username = userDetails.getUsername();

View File

@ -0,0 +1,140 @@
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);
}
}
}

View File

@ -0,0 +1,30 @@
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;
}

View File

@ -0,0 +1,16 @@
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;
}

View File

@ -0,0 +1,60 @@
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;
}
}

View File

@ -0,0 +1,41 @@
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;
}

View File

@ -0,0 +1,23 @@
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);
}

View File

@ -0,0 +1,33 @@
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);
}
}
}

View File

@ -74,6 +74,17 @@ public class AccountService {
return getAccountBalance(userId); return getAccountBalance(userId);
} }
/**
* 更新账户余额仅更新初始余额
*/
@Transactional
public AccountResponse updateAccountBalance(Long userId, BigDecimal initialBalance) {
Account account = getOrCreateAccount(userId);
account.setInitialBalance(initialBalance);
accountMapper.updateById(account);
return getAccountBalance(userId);
}
/** /**
* 计算账户余额初始余额 + 收入总额 - 支出总额 * 计算账户余额初始余额 + 收入总额 - 支出总额
*/ */

View File

@ -0,0 +1,44 @@
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);
}

View File

@ -0,0 +1,50 @@
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);
}
}
}

View File

@ -0,0 +1,188 @@
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;
}
}
}

View File

@ -237,13 +237,17 @@ 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 = "下午201 0.3K/s必 5G ra HD ID 4G 10 C 49 D < Q搜索交易记录 搜索 全部 支出 转账 退款 订单筛选 ¥198 ¥3092.83 ¥0.00 收支分析 设置支出预算> C 五华区皓月千里便利店 -4.20 日用百货 今天 1330 扫收钱码付款-给快乐 -11.00 餐饮美食 今天 1222 余额宝-收益发放 0.19 投资理财 今天 0447 2000406014951497 -20.00 餐饮美食 昨天 2214 扫收钱码付款-给扫码点单店主 -5.00 公共服务 昨天 2144 蜜雪冰城920749店 -2.18 餐饮美食 TA "; // String content = "下午201 0.3K/s必 5G ra HD ID 4G 10 C 49 D < Q搜索交易记录 搜索 全部 支出 转账 退款 订单筛选 ¥198 ¥3092.83 ¥0.00 收支分析 设置支出预算> C 五华区皓月千里便利店 -4.20 日用百货 今天 1330 扫收钱码付款-给快乐 -11.00 餐饮美食 今天 1222 余额宝-收益发放 0.19 投资理财 今天 0447 2000406014951497 -20.00 餐饮美食 昨天 2214 扫收钱码付款-给扫码点单店主 -5.00 公共服务 昨天 2144 蜜雪冰城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;
@ -268,7 +272,12 @@ public class OcrAmountParser {
if (iPlusOne != null && ((iPlusOne.startsWith("+") || iPlusOne.startsWith("-")) if (iPlusOne != null && ((iPlusOne.startsWith("+") || iPlusOne.startsWith("-"))
|| AMOUNT_PATTERN.matcher(Objects.requireNonNull(iPlusOne).trim()).matches())) { || AMOUNT_PATTERN.matcher(Objects.requireNonNull(iPlusOne).trim()).matches())) {
if (UNION_DATE_PATTERN.matcher(currentLine).matches()){ if (UNION_DATE_PATTERN.matcher(currentLine).matches()){
signMap.put("data", currentLine + split[i+1].trim()); // 检查 i+1 是否在数组范围内
if (i + 1 < split.length) {
signMap.put("data", currentLine + " " + split[i+1].trim());
} else {
signMap.put("data", currentLine);
}
}else { }else {
signMap.put("data", currentLine); signMap.put("data", currentLine);
} }
@ -281,7 +290,12 @@ public class OcrAmountParser {
if (iPlusTwo != null && ((iPlusTwo.startsWith("+") || iPlusTwo.startsWith("-")) if (iPlusTwo != null && ((iPlusTwo.startsWith("+") || iPlusTwo.startsWith("-"))
|| AMOUNT_PATTERN.matcher(Objects.requireNonNull(iPlusTwo).trim()).matches())) { || AMOUNT_PATTERN.matcher(Objects.requireNonNull(iPlusTwo).trim()).matches())) {
if (UNION_DATE_PATTERN.matcher(currentLine).matches()){ if (UNION_DATE_PATTERN.matcher(currentLine).matches()){
signMap.put("data", currentLine + split[i+1].trim()); // 检查 i+1 是否在数组范围内
if (i + 1 < split.length) {
signMap.put("data", currentLine + " " + split[i+1].trim());
} else {
signMap.put("data", currentLine);
}
}else { }else {
signMap.put("data", currentLine); signMap.put("data", currentLine);
} }
@ -295,17 +309,28 @@ public class OcrAmountParser {
if (iPlusThree != null && ((iPlusThree.startsWith("+") || iPlusThree.startsWith("-")) if (iPlusThree != null && ((iPlusThree.startsWith("+") || iPlusThree.startsWith("-"))
|| AMOUNT_PATTERN.matcher(Objects.requireNonNull(iPlusThree).trim()).matches())) { || AMOUNT_PATTERN.matcher(Objects.requireNonNull(iPlusThree).trim()).matches())) {
if (UNION_DATE_PATTERN.matcher(currentLine).matches()){ if (UNION_DATE_PATTERN.matcher(currentLine).matches()){
signMap.put("data", currentLine + split[i+1].trim()); // 检查 i+1 是否在数组范围内
if (i + 1 < split.length) {
signMap.put("data", currentLine + " " + split[i+1].trim());
} else {
signMap.put("data", currentLine);
}
}else { }else {
signMap.put("data", currentLine); signMap.put("data", currentLine);
} }
signMap.put("money", iPlusThree); signMap.put("money", iPlusThree);
signMap.put("content", split[i - 4].trim()); // 检查 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<>();

View File

@ -60,8 +60,15 @@ 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: 8080 port: 12345
logging: logging:
level: level:

View File

@ -0,0 +1,5 @@
-- 删除 gold_price 表的唯一约束 uk_gold_date
-- 原因需要支持每15分钟查询一次同一天需要存储多条记录
-- 执行时间2026-01-23
ALTER TABLE `gold_price` DROP INDEX `uk_gold_date`;

View File

@ -102,6 +102,27 @@ 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),