diff --git a/src/main/java/com/accounting/config/K780Config.java b/src/main/java/com/accounting/config/K780Config.java new file mode 100644 index 0000000..0a44447 --- /dev/null +++ b/src/main/java/com/accounting/config/K780Config.java @@ -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; +} diff --git a/src/main/java/com/accounting/config/RestTemplateConfig.java b/src/main/java/com/accounting/config/RestTemplateConfig.java new file mode 100644 index 0000000..f7cad74 --- /dev/null +++ b/src/main/java/com/accounting/config/RestTemplateConfig.java @@ -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); + } +} diff --git a/src/main/java/com/accounting/config/SchedulerConfig.java b/src/main/java/com/accounting/config/SchedulerConfig.java new file mode 100644 index 0000000..1ff4616 --- /dev/null +++ b/src/main/java/com/accounting/config/SchedulerConfig.java @@ -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 { +} + diff --git a/src/main/java/com/accounting/config/SecurityConfig.java b/src/main/java/com/accounting/config/SecurityConfig.java index 45b36d3..9c4610f 100644 --- a/src/main/java/com/accounting/config/SecurityConfig.java +++ b/src/main/java/com/accounting/config/SecurityConfig.java @@ -35,6 +35,7 @@ public class SecurityConfig { .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/api/gold-price/**").permitAll() .requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**").permitAll() .anyRequest().authenticated() ) @@ -56,10 +57,14 @@ public class SecurityConfig { @Bean public CorsConfigurationSource corsConfigurationSource() { 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.setAllowCredentials(false); + configuration.setAllowCredentials(true); // 允许携带认证信息 configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); diff --git a/src/main/java/com/accounting/controller/AccountController.java b/src/main/java/com/accounting/controller/AccountController.java index f2001f9..df42df3 100644 --- a/src/main/java/com/accounting/controller/AccountController.java +++ b/src/main/java/com/accounting/controller/AccountController.java @@ -42,6 +42,16 @@ public class AccountController { return ResponseEntity.ok(response); } + @Operation(summary = "更新账户余额") + @PutMapping("/balance") + public ResponseEntity 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(); diff --git a/src/main/java/com/accounting/controller/BudgetController.java b/src/main/java/com/accounting/controller/BudgetController.java index 0f96580..f0a707f 100644 --- a/src/main/java/com/accounting/controller/BudgetController.java +++ b/src/main/java/com/accounting/controller/BudgetController.java @@ -54,6 +54,19 @@ public class BudgetController { return ResponseEntity.ok(response); } + @Operation(summary = "更新当前月份预算") + @PutMapping("/current") + public ResponseEntity 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(); diff --git a/src/main/java/com/accounting/controller/GoldPriceController.java b/src/main/java/com/accounting/controller/GoldPriceController.java new file mode 100644 index 0000000..77bc450 --- /dev/null +++ b/src/main/java/com/accounting/controller/GoldPriceController.java @@ -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> getCurrentPrice( + @RequestParam(required = false, defaultValue = DEFAULT_GOLD_ID) String goldId) { + try { + GoldPriceResponse data = goldPriceService.getCurrentPrice(goldId); + Map 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 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> getHistoryPrices( + @RequestParam(required = false, defaultValue = DEFAULT_GOLD_ID) String goldId, + @RequestParam(required = false, defaultValue = "30") Integer days) { + try { + List data = goldPriceService.getHistoryPrices(goldId, days); + Map response = new HashMap<>(); + response.put("code", 200); + response.put("message", "success"); + response.put("data", data); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map 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> 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 data = goldPriceService.getPricesByDate(goldId, localDate); + Map response = new HashMap<>(); + response.put("code", 200); + response.put("message", "success"); + response.put("data", data); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map 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> getLatestDate( + @RequestParam(required = false, defaultValue = DEFAULT_GOLD_ID) String goldId) { + try { + LocalDate latestDate = goldPriceService.getLatestDate(goldId); + Map 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 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> refreshPrice( + @RequestParam(required = false, defaultValue = DEFAULT_GOLD_ID) String goldId) { + try { + goldPriceService.refreshCurrentPrice(goldId); + // 刷新后返回最新的价格 + GoldPriceResponse data = goldPriceService.getCurrentPrice(goldId); + Map response = new HashMap<>(); + response.put("code", 200); + response.put("message", "刷新成功"); + response.put("data", data); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("code", 500); + response.put("message", e.getMessage()); + response.put("data", null); + return ResponseEntity.ok(response); + } + } +} diff --git a/src/main/java/com/accounting/dto/GoldPriceResponse.java b/src/main/java/com/accounting/dto/GoldPriceResponse.java new file mode 100644 index 0000000..a990779 --- /dev/null +++ b/src/main/java/com/accounting/dto/GoldPriceResponse.java @@ -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; +} diff --git a/src/main/java/com/accounting/dto/K780Response.java b/src/main/java/com/accounting/dto/K780Response.java new file mode 100644 index 0000000..3251a68 --- /dev/null +++ b/src/main/java/com/accounting/dto/K780Response.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/accounting/dto/K780Result.java b/src/main/java/com/accounting/dto/K780Result.java new file mode 100644 index 0000000..bfb5152 --- /dev/null +++ b/src/main/java/com/accounting/dto/K780Result.java @@ -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 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/accounting/entity/GoldPrice.java b/src/main/java/com/accounting/entity/GoldPrice.java new file mode 100644 index 0000000..22482a4 --- /dev/null +++ b/src/main/java/com/accounting/entity/GoldPrice.java @@ -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; +} diff --git a/src/main/java/com/accounting/mapper/GoldPriceMapper.java b/src/main/java/com/accounting/mapper/GoldPriceMapper.java new file mode 100644 index 0000000..96aa8c5 --- /dev/null +++ b/src/main/java/com/accounting/mapper/GoldPriceMapper.java @@ -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 { + + @Select("SELECT * FROM gold_price WHERE gold_id = #{goldId} ORDER BY price_date DESC LIMIT #{limit}") + List 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 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); +} diff --git a/src/main/java/com/accounting/schedule/GoldPriceScheduler.java b/src/main/java/com/accounting/schedule/GoldPriceScheduler.java new file mode 100644 index 0000000..bdec52e --- /dev/null +++ b/src/main/java/com/accounting/schedule/GoldPriceScheduler.java @@ -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); + } + } +} + diff --git a/src/main/java/com/accounting/service/AccountService.java b/src/main/java/com/accounting/service/AccountService.java index f0a601b..a92148d 100644 --- a/src/main/java/com/accounting/service/AccountService.java +++ b/src/main/java/com/accounting/service/AccountService.java @@ -74,6 +74,17 @@ public class AccountService { return getAccountBalance(userId); } + /** + * 更新账户余额(仅更新初始余额) + */ + @Transactional + public AccountResponse updateAccountBalance(Long userId, BigDecimal initialBalance) { + Account account = getOrCreateAccount(userId); + account.setInitialBalance(initialBalance); + accountMapper.updateById(account); + return getAccountBalance(userId); + } + /** * 计算账户余额(初始余额 + 收入总额 - 支出总额) */ diff --git a/src/main/java/com/accounting/service/GoldPriceService.java b/src/main/java/com/accounting/service/GoldPriceService.java new file mode 100644 index 0000000..c4e66d8 --- /dev/null +++ b/src/main/java/com/accounting/service/GoldPriceService.java @@ -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 getHistoryPrices(String goldId, Integer days); + + /** + * 获取指定日期的所有黄金价格记录 + * @param goldId 黄金品种ID + * @param date 日期 + * @return 指定日期的价格记录列表 + */ + List getPricesByDate(String goldId, LocalDate date); + + /** + * 获取最近有数据的日期 + * @param goldId 黄金品种ID + * @return 最近有数据的日期,可能为 null + */ + LocalDate getLatestDate(String goldId); +} diff --git a/src/main/java/com/accounting/service/K780ApiClient.java b/src/main/java/com/accounting/service/K780ApiClient.java new file mode 100644 index 0000000..b82205d --- /dev/null +++ b/src/main/java/com/accounting/service/K780ApiClient.java @@ -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); + } + } +} diff --git a/src/main/java/com/accounting/service/impl/GoldPriceServiceImpl.java b/src/main/java/com/accounting/service/impl/GoldPriceServiceImpl.java new file mode 100644 index 0000000..6e11cfa --- /dev/null +++ b/src/main/java/com/accounting/service/impl/GoldPriceServiceImpl.java @@ -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() + .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 getHistoryPrices(String goldId, Integer days) { + int limit = (days != null && days > 0) ? Math.min(days, 30) : 30; + List prices = goldPriceMapper.selectHistoryByGoldId(goldId, limit); + return prices.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + } + + @Override + @Transactional(readOnly = true) + public List getPricesByDate(String goldId, LocalDate date) { + List 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; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/accounting/util/OcrAmountParser.java b/src/main/java/com/accounting/util/OcrAmountParser.java index 3e561cb..1af45bf 100644 --- a/src/main/java/com/accounting/util/OcrAmountParser.java +++ b/src/main/java/com/accounting/util/OcrAmountParser.java @@ -237,13 +237,17 @@ public class OcrAmountParser { JSONObject dataObject = JSON.parseObject(data); 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(" "); + System.out.println("split:"+ split); + System.out.println("split.size:"+ split.length); + ArrayList> signList = new ArrayList<>(); + System.out.println("开始解析"); for (int i = 1; i < split.length; i++) { String currentLine = split[i].trim(); - String previousLine = split[i-1].trim(); // 安全地获取 i+1, i+2, i+3 位置的值 String iPlusOne = null; @@ -268,7 +272,12 @@ public class OcrAmountParser { if (iPlusOne != null && ((iPlusOne.startsWith("+") || iPlusOne.startsWith("-")) || AMOUNT_PATTERN.matcher(Objects.requireNonNull(iPlusOne).trim()).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 { signMap.put("data", currentLine); } @@ -281,7 +290,12 @@ public class OcrAmountParser { if (iPlusTwo != null && ((iPlusTwo.startsWith("+") || iPlusTwo.startsWith("-")) || AMOUNT_PATTERN.matcher(Objects.requireNonNull(iPlusTwo).trim()).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 { signMap.put("data", currentLine); } @@ -295,17 +309,28 @@ public class OcrAmountParser { if (iPlusThree != null && ((iPlusThree.startsWith("+") || iPlusThree.startsWith("-")) || AMOUNT_PATTERN.matcher(Objects.requireNonNull(iPlusThree).trim()).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 { signMap.put("data", currentLine); } 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); } } } + System.out.println("识别结束,开始对结果进行处理"); + System.out.println("signList:"+signList); + //识别完成,对识别结果进行处理 ArrayList parseList = new ArrayList<>(); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 09a8576..ad225af 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -60,8 +60,15 @@ knife4j: - url: http://localhost:8080 description: 本地开发环境 +# K780 API配置 +k780: + api-url: https://sapi.k780.com + app-key: 78346 + sign: 1b502c535927b66d9b888a6d4701bf72 + timeout: 5000 + server: - port: 8080 + port: 12345 logging: level: diff --git a/src/main/resources/db/migration_remove_gold_price_unique_constraint.sql b/src/main/resources/db/migration_remove_gold_price_unique_constraint.sql new file mode 100644 index 0000000..218663c --- /dev/null +++ b/src/main/resources/db/migration_remove_gold_price_unique_constraint.sql @@ -0,0 +1,5 @@ +-- 删除 gold_price 表的唯一约束 uk_gold_date +-- 原因:需要支持每15分钟查询一次,同一天需要存储多条记录 +-- 执行时间:2026-01-23 + +ALTER TABLE `gold_price` DROP INDEX `uk_gold_date`; diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql index 3140a6e..bd13c74 100644 --- a/src/main/resources/db/schema.sql +++ b/src/main/resources/db/schema.sql @@ -102,6 +102,27 @@ CREATE TABLE IF NOT EXISTS `ocr_record` ( FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ) 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 (NULL, '餐饮', '🍔', 1, 1),