因为数据库连接问题导致的插入数据库异常

This commit is contained in:
2026-05-15 23:30:40 +08:00
parent 42b1555c80
commit d66daf375c
25 changed files with 1371 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
package com.rj;
import com.rj.config.AmapProperties;
import com.rj.config.HxrAdminProperties;
import com.rj.config.LbAssessmentDingTalkProperties;
import com.rj.config.YihangyiVllmAsrProperties;
import org.mybatis.spring.annotation.MapperScan;
@@ -41,7 +42,12 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@MapperScan("com.rj.mapper")
@SpringBootApplication
@EnableScheduling
@EnableConfigurationProperties({YihangyiVllmAsrProperties.class, AmapProperties.class, LbAssessmentDingTalkProperties.class})
@EnableConfigurationProperties({
YihangyiVllmAsrProperties.class,
AmapProperties.class,
LbAssessmentDingTalkProperties.class,
HxrAdminProperties.class
})
public class AISmartCard20251230Application {
public static void main(String[] args) {

View File

@@ -1,10 +1,13 @@
package com.rj.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.time.Clock;
/**
* 显式扩大 {@code @Scheduled} 线程池,避免单个耗时任务(如长时间 HTTP占满默认单线程后拖死全部定时任务。
* <p>与 {@code spring.task.scheduling.pool.size} 配置互补;此处代码保证至少 8 个调度线程。
@@ -14,6 +17,11 @@ public class AppSchedulingConfiguration implements SchedulingConfigurer {
private static final int SCHEDULER_POOL_SIZE = 8;
@Bean
public Clock applicationClock() {
return Clock.systemDefaultZone();
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();

View File

@@ -0,0 +1,34 @@
package com.rj.config;
import com.rj.scheduler.LBAdminPullScheduler;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* hxrd 后台订单列表拉取(供 {@link LBAdminPullScheduler} 使用)。
*/
@Data
@ConfigurationProperties(prefix = "hxr.admin")
public class HxrAdminProperties {
/**
* 是否启用拉取任务;未启用时不发 HTTP 请求。
*/
private boolean enabled = true;
/**
* 完整请求 URL含查询串与浏览器地址栏一致即可。
*/
private String orderSelectUrl =
"https://hxrdhoutai.hxrdsm.cn/app/admin/order/select?page=1&limit=90";
/**
* 请求头 Cookie 完整取值,例如 {@code PHPSID=xxxx}。非空时优先于 {@link #phpsid}。
*/
private String cookie = "";
/**
* 仅 PHPSID 会话值(不含 {@code PHPSID=} 前缀);在 {@link #cookie} 为空时使用。
*/
private String phpsid = "";
}

View File

@@ -0,0 +1,98 @@
package com.rj.controller;
import com.rj.dto.LbOrderRowSyncFromHxrRequest;
import com.rj.entity.LbOrderRow;
import com.rj.service.ILbOrderRowService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/lbOrderRow")
@Tag(name = "LB 订单行", description = "lb_order_row 增删改查与分页")
public class LbOrderRowController {
@Autowired
private ILbOrderRowService lbOrderRowService;
@PostMapping("/add")
@Operation(summary = "新增")
public ResponseEntity<Map<String, Object>> add(
@Parameter(description = "实体id 为订单 id需调用方指定", required = true) @RequestBody LbOrderRow entity) {
Map<String, Object> result = lbOrderRowService.add(entity);
Boolean success = (Boolean) result.get("success");
if (success != null && success) {
return ResponseEntity.ok(result);
}
return ResponseEntity.badRequest().body(result);
}
@PutMapping("/update")
@Operation(summary = "编辑")
public ResponseEntity<Map<String, Object>> update(
@Parameter(description = "实体", required = true) @RequestBody LbOrderRow entity) {
Map<String, Object> result = lbOrderRowService.update(entity);
Boolean success = (Boolean) result.get("success");
if (success != null && success) {
return ResponseEntity.ok(result);
}
return ResponseEntity.badRequest().body(result);
}
@DeleteMapping("/delete/{id}")
@Operation(summary = "删除")
public ResponseEntity<Map<String, Object>> delete(
@Parameter(description = "主键(订单 id", required = true) @PathVariable Long id) {
Map<String, Object> result = lbOrderRowService.deleteById(id);
Boolean success = (Boolean) result.get("success");
if (success != null && success) {
return ResponseEntity.ok(result);
}
return ResponseEntity.badRequest().body(result);
}
@GetMapping("/list")
@Operation(summary = "分页查询")
public ResponseEntity<Map<String, Object>> list(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String isResell,
@RequestParam(required = false) String orderSn,
@RequestParam(required = false) Long buyerId,
@RequestParam(required = false) Long sellerId,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) Long merchandiseId) {
Map<String, Object> result =
lbOrderRowService.pageQuery(
current, size, isResell, orderSn, buyerId, sellerId, status, merchandiseId);
Boolean success = (Boolean) result.get("success");
if (success != null && success) {
return ResponseEntity.ok(result);
}
return ResponseEntity.internalServerError().body(result);
}
@PostMapping("/sync-order-from-third")
@Operation(
summary = "从 外部系统 同步订单",
description = "按购买时间区间、租户 id、转卖状态调用后台 order/select将结果写入 lb_order_row按订单 id upsert")
public ResponseEntity<Map<String, Object>> syncFromHxr(
@Parameter(description = "同步条件", required = true) @RequestBody LbOrderRowSyncFromHxrRequest request) {
Map<String, Object> result =
lbOrderRowService.syncFromHxrAdmin(
request.getBuyTimeStart(),
request.getBuyTimeEnd(),
request.getTenantId(),
request.getIsResell());
Boolean success = (Boolean) result.get("success");
if (success != null && success) {
return ResponseEntity.ok(result);
}
return ResponseEntity.badRequest().body(result);
}
}

View File

@@ -0,0 +1,24 @@
package com.rj.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 从 hxrd 后台同步订单到 {@code lb_order_row} 的请求体。
*/
@Data
@Schema(description = "LB 订单从 hxrd 后台同步请求")
public class LbOrderRowSyncFromHxrRequest {
@Schema(description = "购买时间起(对应后台 buy_time[0]),可空表示不限制")
private String buyTimeStart;
@Schema(description = "购买时间止(对应后台 buy_time[1]),可空表示不限制")
private String buyTimeEnd;
@Schema(description = "写入本系统的租户 id", requiredMode = Schema.RequiredMode.REQUIRED)
private String tenantId;
@Schema(description = "转卖状态筛选(对应后台 is_resell", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer isResell;
}

View File

@@ -0,0 +1,28 @@
package com.rj.dto.hxr;
/**
* hxrd 后台 {@code /app/admin/order/select} 返回的 {@code data} 中单条订单。
*/
public record HxrOrderRow(
long id,
Long oldId,
long sellerId,
long buyerId,
String orderSn,
String totalMoney,
String payTime,
String payImg,
int status,
int isResell,
int isShow,
String consignee,
String phone,
String province,
String city,
String area,
String address,
long merchandiseId,
String confirmTime,
String buyTime,
String createdAt,
String updatedAt) {}

View File

@@ -0,0 +1,9 @@
package com.rj.dto.hxr;
import java.util.List;
/**
* hxrd 后台 {@code /app/admin/order/select} 顶层 JSON。
*/
public record HxrOrderSelectResponse(
int code, String msg, int count, List<HxrOrderRow> data, String allMoney) {}

View File

@@ -0,0 +1,41 @@
package com.rj.entity;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
/**
* Parses {@link LocalDateTime} from ISO-8601 or common SQL-style {@code yyyy-MM-dd HH:mm:ss}.
*/
public class FlexibleLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
private static final DateTimeFormatter SQL_SPACE = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String text = p.getValueAsString();
if (text == null) {
return null;
}
text = text.trim();
if (text.isEmpty()) {
return null;
}
try {
return LocalDateTime.parse(text, SQL_SPACE);
} catch (DateTimeParseException ignored) {
// continue
}
try {
return LocalDateTime.parse(text, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
} catch (DateTimeParseException e) {
throw ctxt.weirdStringException(text, LocalDateTime.class,
"Expected ISO-8601 local date-time or yyyy-MM-dd HH:mm:ss");
}
}
}

View File

@@ -0,0 +1,19 @@
package com.rj.entity;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Accepts ISO-8601 local date-time and common SQL-style {@code yyyy-MM-dd HH:mm:ss} for JSON binding.
*/
@JacksonAnnotationsInside
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@JsonDeserialize(using = FlexibleLocalDateTimeDeserializer.class)
public @interface JsonFlexibleLocalDateTime {
}

View File

@@ -76,13 +76,16 @@ public class LbAssessmentApply implements Serializable {
@TableField("application_datetime")
@Schema(description = "申请日期时间")
@JsonFlexibleLocalDateTime
private LocalDateTime applicationDatetime;
@TableField("created_at")
@Schema(description = "创建时间")
@JsonFlexibleLocalDateTime
private LocalDateTime createdAt;
@TableField("updated_at")
@Schema(description = "修改时间")
@JsonFlexibleLocalDateTime
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,113 @@
package com.rj.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("lb_order_row")
@Schema(description = "LB 订单行表")
public class LbOrderRow implements Serializable {
private static final long serialVersionUID = 1L;
/** 业务主键为订单 id由调用方传入非数据库自增 */
@TableId(value = "id", type = IdType.INPUT)
@Schema(description = "订单 id")
private Long id;
@TableField("old_id")
@Schema(description = "旧订单 id")
private Long oldId;
@TableField("tenant_id")
@Schema(description = "租户ID")
private String tenantId;
@TableField("seller_id")
@Schema(description = "卖家 id")
private Long sellerId;
@TableField("buyer_id")
@Schema(description = "买家 id")
private Long buyerId;
@TableField("order_sn")
@Schema(description = "订单号")
private String orderSn;
@TableField("total_money")
@Schema(description = "总金额(接口为字符串)")
private String totalMoney;
@TableField("pay_time")
@Schema(description = "支付时间(原样字符串)")
private String payTime;
@TableField("pay_img")
@Schema(description = "支付凭证图")
private String payImg;
@TableField("status")
@Schema(description = "状态")
private Integer status;
@TableField("is_resell")
@Schema(description = "是否转卖")
private Integer isResell;
@TableField("is_show")
@Schema(description = "是否展示")
private Integer isShow;
@TableField("consignee")
@Schema(description = "收货人")
private String consignee;
@TableField("phone")
@Schema(description = "电话")
private String phone;
@TableField("province")
@Schema(description = "")
private String province;
@TableField("city")
@Schema(description = "")
private String city;
@TableField("area")
@Schema(description = "")
private String area;
@TableField("address")
@Schema(description = "详细地址")
private String address;
@TableField("merchandise_id")
@Schema(description = "商品 id")
private Long merchandiseId;
@TableField("confirm_time")
@Schema(description = "确认时间")
private String confirmTime;
@TableField("buy_time")
@Schema(description = "下单时间")
private String buyTime;
@TableField("created_at")
@Schema(description = "创建时间")
private String createdAt;
@TableField("updated_at")
@Schema(description = "更新时间")
private String updatedAt;
}

View File

@@ -0,0 +1,9 @@
package com.rj.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.rj.entity.LbOrderRow;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LbOrderRowMapper extends BaseMapper<LbOrderRow> {
}

View File

@@ -0,0 +1,66 @@
package com.rj.scheduler;
import com.rj.config.AppConfig;
import com.rj.config.HxrAdminProperties;
import com.rj.service.HxrAdminOrderSelectService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Clock;
import java.time.DayOfWeek;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
/**
* hxrd 后台订单列表定时拉取:每 50 分钟触发一次,仅工作日(周一至周五)且在每天 18:1023:59:59应用时区内真正执行。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class LBAdminPullScheduler {
private static final long FIFTY_MINUTES_MS = 50L * 60L * 1000L;
private final AppConfig appConfig;
private final HxrAdminProperties hxrAdminProperties;
private final HxrAdminOrderSelectService hxrAdminOrderSelectService;
private final Clock clock;
@Scheduled(
fixedRate = FIFTY_MINUTES_MS,
initialDelayString = "${hxr.admin.pull-initial-delay-ms:0}")
public void pullAdminOrders() {
try {
if (!appConfig.getScheduler().isStart()) {
log.debug("全局调度未启用,跳过 未寄卖 订单拉取");
return;
}
if (!hxrAdminProperties.isEnabled()) {
log.debug("hxr.admin.enabled=false跳过订单拉取");
return;
}
ZoneId zone = ZoneId.of(appConfig.getTimezone());
ZonedDateTime nowZdt = ZonedDateTime.now(clock.withZone(zone));
DayOfWeek dow = nowZdt.getDayOfWeek();
if (dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY) {
log.debug("当前为周末 {},跳过 未寄卖 订单拉取", dow);
return;
}
LocalTime now = nowZdt.toLocalTime();
LocalTime windowStart = LocalTime.of(18, 10);
LocalTime windowEnd = LocalTime.of(23, 59, 59);
if (now.isBefore(windowStart) || now.isAfter(windowEnd)) {
log.debug("当前时刻 {} 不在窗口 {}{},跳过 未寄卖 订单拉取", now, windowStart, windowEnd);
return;
}
log.info("开始执行 未寄卖 订单拉取(工作日 18:1023:59:59 窗口内)");
hxrAdminOrderSelectService.fetchAndLogEachOrder();
} catch (Exception e) {
log.error("未寄卖 admin 订单拉取失败", e);
}
}
}

View File

@@ -0,0 +1,148 @@
package com.rj.service;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.rj.config.HxrAdminProperties;
import com.rj.dto.hxr.HxrOrderRow;
import com.rj.dto.hxr.HxrOrderSelectResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Optional;
/**
* 调用 hxrd 后台订单列表接口并解析 JSON。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class HxrAdminOrderSelectService {
private static final ObjectMapper JSON = new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private final HxrAdminProperties properties;
/**
* 拉取订单列表;配置未就绪或 HTTP 非 2xx 时返回 empty。
* <p>使用 {@link HxrAdminProperties#getOrderSelectUrl()} 原样请求(与定时任务一致)。
*/
public Optional<HxrOrderSelectResponse> fetchOrderSelect() throws Exception {
return fetchOrderSelectInternal(properties.getOrderSelectUrl());
}
/**
* 按购买时间区间、转卖状态等条件拉取订单列表(在配置的 {@code orderSelectUrl} 上覆盖查询参数)。
*
* @param buyTimeStart 购买时间起,对应 {@code buy_time[0]},可为 null 或空串表示不限制
* @param buyTimeEnd 购买时间止,对应 {@code buy_time[1]}
* @param isResell 转卖筛选,对应 {@code is_resell};为 null 时不改写该参数
*/
public Optional<HxrOrderSelectResponse> fetchOrderSelect(
String buyTimeStart, String buyTimeEnd, Integer isResell) throws Exception {
return fetchOrderSelectInternal(buildOrderSelectUrl(buyTimeStart, buyTimeEnd, isResell));
}
private Optional<HxrOrderSelectResponse> fetchOrderSelectInternal(String requestUri) throws Exception {
String cookieHeader = resolveCookieHeader();
if (cookieHeader == null || cookieHeader.isBlank()) {
log.warn("hxr.admin 未配置 cookie 或 phpsid跳过拉取");
return Optional.empty();
}
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(35))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(requestUri))
.timeout(Duration.ofSeconds(160))
.header("Accept", "application/json, text/javascript, */*; q=0.01")
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
.header("Cookie", cookieHeader)
.header("Priority", "u=1, i")
.header("Referer", "https://hxrdhoutai.hxrdsm.cn/app/admin/order/index")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
if (status < 200 || status >= 300) {
log.warn("hxr admin order/select HTTP {} bodyPrefix={}", status, abbreviate(response.body(), 500));
return Optional.empty();
}
HxrOrderSelectResponse body = JSON.readValue(response.body(), HxrOrderSelectResponse.class);
if (body.code() != 0) {
log.warn("hxr admin order/select api code={} msg={}", body.code(), body.msg());
return Optional.empty();
}
return Optional.of(body);
}
/**
* 在 {@link HxrAdminProperties#getOrderSelectUrl()} 上覆盖 {@code buy_time[0]}、{@code buy_time[1]}、{@code is_resell}。
*/
private String buildOrderSelectUrl(String buyTimeStart, String buyTimeEnd, Integer isResell) {
UriComponentsBuilder b = UriComponentsBuilder.fromUriString(properties.getOrderSelectUrl());
b.replaceQueryParam("buy_time[0]", buyTimeStart == null ? "" : buyTimeStart);
b.replaceQueryParam("buy_time[1]", buyTimeEnd == null ? "" : buyTimeEnd);
if (isResell != null) {
b.replaceQueryParam("is_resell", isResell);
}
return b.build().encode().toUriString();
}
/**
* 拉取并逐条打日志(一行一条 JSON
*/
public void fetchAndLogEachOrder() throws Exception {
Optional<HxrOrderSelectResponse> opt = fetchOrderSelect();
if (opt.isEmpty()) {
return;
}
HxrOrderSelectResponse body = opt.get();
log.info("未寄卖 order/select ok count={} allMoney={}", body.count(), body.allMoney());
for (HxrOrderRow row : body.data()) {
log.info("hxr order row {}", JSON.writeValueAsString(row));
}
}
private String resolveCookieHeader() {
String c = firstNonBlank(properties.getCookie());
if (c != null) {
return c;
}
String id = firstNonBlank(properties.getPhpsid());
if (id != null) {
return "PHPSID=" + id;
}
return "";
}
private static String firstNonBlank(String s) {
if (s == null) {
return null;
}
String t = s.trim();
return t.isEmpty() ? null : t;
}
private static String abbreviate(String s, int maxLen) {
if (s == null) {
return "";
}
return s.length() <= maxLen ? s : s.substring(0, maxLen) + "...";
}
}

View File

@@ -0,0 +1,31 @@
package com.rj.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.rj.entity.LbOrderRow;
import java.util.Map;
public interface ILbOrderRowService extends IService<LbOrderRow> {
Map<String, Object> add(LbOrderRow entity);
Map<String, Object> update(LbOrderRow entity);
Map<String, Object> deleteById(Long id);
Map<String, Object> pageQuery(
Integer current,
Integer size,
String tenantId,
String orderSn,
Long buyerId,
Long sellerId,
Integer status,
Long merchandiseId);
/**
* 按条件调用 hxrd 后台订单列表接口拉取数据,并 upsert 到 {@code lb_order_row}。
*/
Map<String, Object> syncFromHxrAdmin(
String buyTimeStart, String buyTimeEnd, String tenantId, Integer isResell);
}

View File

@@ -0,0 +1,273 @@
package com.rj.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rj.dto.hxr.HxrOrderRow;
import com.rj.dto.hxr.HxrOrderSelectResponse;
import com.rj.entity.LbOrderRow;
import com.rj.mapper.LbOrderRowMapper;
import com.rj.service.HxrAdminOrderSelectService;
import com.rj.service.ILbOrderRowService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
public class LbOrderRowServiceImpl extends ServiceImpl<LbOrderRowMapper, LbOrderRow> implements ILbOrderRowService {
@Autowired
private HxrAdminOrderSelectService hxrAdminOrderSelectService;
@Override
public Map<String, Object> add(LbOrderRow entity) {
Map<String, Object> result = new HashMap<>();
try {
if (entity.getId() == null) {
result.put("success", false);
result.put("message", "id订单 id不能为空");
return result;
}
if (entity.getTenantId() == null || entity.getTenantId().trim().isEmpty()) {
result.put("success", false);
result.put("message", "tenantId不能为空");
return result;
}
if (entity.getSellerId() == null) {
result.put("success", false);
result.put("message", "sellerId不能为空");
return result;
}
if (entity.getBuyerId() == null) {
result.put("success", false);
result.put("message", "buyerId不能为空");
return result;
}
if (entity.getOrderSn() == null || entity.getOrderSn().trim().isEmpty()) {
result.put("success", false);
result.put("message", "orderSn不能为空");
return result;
}
if (entity.getMerchandiseId() == null) {
result.put("success", false);
result.put("message", "merchandiseId不能为空");
return result;
}
if (this.getById(entity.getId()) != null) {
result.put("success", false);
result.put("message", "该订单 id 已存在");
return result;
}
entity.setTenantId(entity.getTenantId().trim());
entity.setOrderSn(entity.getOrderSn().trim());
if (entity.getStatus() == null) {
entity.setStatus(0);
}
if (entity.getIsResell() == null) {
entity.setIsResell(0);
}
if (entity.getIsShow() == null) {
entity.setIsShow(0);
}
boolean ok = this.save(entity);
result.put("success", ok);
result.put("message", ok ? "新增成功" : "新增失败");
if (ok) {
result.put("data", this.getById(entity.getId()));
}
return result;
} catch (Exception e) {
result.put("success", false);
result.put("message", "新增异常:" + e.getMessage());
return result;
}
}
@Override
public Map<String, Object> update(LbOrderRow entity) {
Map<String, Object> result = new HashMap<>();
try {
if (entity.getId() == null) {
result.put("success", false);
result.put("message", "id不能为空");
return result;
}
if (this.getById(entity.getId()) == null) {
result.put("success", false);
result.put("message", "记录不存在");
return result;
}
boolean ok = this.updateById(entity);
result.put("success", ok);
result.put("message", ok ? "编辑成功" : "编辑失败");
if (ok) {
result.put("data", this.getById(entity.getId()));
}
return result;
} catch (Exception e) {
result.put("success", false);
result.put("message", "编辑异常:" + e.getMessage());
return result;
}
}
@Override
public Map<String, Object> deleteById(Long id) {
Map<String, Object> result = new HashMap<>();
try {
boolean ok = this.removeById(id);
result.put("success", ok);
result.put("message", ok ? "删除成功" : "删除失败");
return result;
} catch (Exception e) {
result.put("success", false);
result.put("message", "删除异常:" + e.getMessage());
return result;
}
}
@Override
public Map<String, Object> pageQuery(
Integer current,
Integer size,
String isResell,
String orderSn,
Long buyerId,
Long sellerId,
Integer status,
Long merchandiseId) {
Map<String, Object> result = new HashMap<>();
try {
if (current == null || current < 1) {
current = 1;
}
if (size == null || size < 1) {
size = 10;
}
LambdaQueryWrapper<LbOrderRow> w = new LambdaQueryWrapper<>();
if (isResell != null && !isResell.trim().isEmpty()) {
w.eq(LbOrderRow::getIsResell, isResell.trim());
}
if (orderSn != null && !orderSn.trim().isEmpty()) {
w.like(LbOrderRow::getOrderSn, orderSn.trim());
}
if (buyerId != null) {
w.eq(LbOrderRow::getBuyerId, buyerId);
}
if (sellerId != null) {
w.eq(LbOrderRow::getSellerId, sellerId);
}
if (status != null) {
w.eq(LbOrderRow::getStatus, status);
}
if (merchandiseId != null) {
w.eq(LbOrderRow::getMerchandiseId, merchandiseId);
}
w.orderByDesc(LbOrderRow::getId);
Page<LbOrderRow> page = this.page(new Page<>(current, size), w);
result.put("success", true);
result.put("message", "查询成功");
result.put("data", page.getRecords());
result.put("total", page.getTotal());
result.put("current", page.getCurrent());
result.put("size", page.getSize());
result.put("pages", page.getPages());
return result;
} catch (Exception e) {
result.put("success", false);
result.put("message", "查询异常:" + e.getMessage());
return result;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> syncFromHxrAdmin(
String buyTimeStart, String buyTimeEnd, String tenantId, Integer isResell) {
Map<String, Object> result = new HashMap<>();
try {
if (tenantId == null || tenantId.trim().isEmpty()) {
result.put("success", false);
result.put("message", "tenantId不能为空");
return result;
}
Optional<HxrOrderSelectResponse> opt =
hxrAdminOrderSelectService.fetchOrderSelect(buyTimeStart, buyTimeEnd, isResell);
if (opt.isEmpty()) {
result.put("success", false);
result.put("message", "未拉取到订单(请检查 hxr.admin cookie/phpsid、网络或后台返回");
result.put("synced", 0);
return result;
}
HxrOrderSelectResponse body = opt.get();
List<HxrOrderRow> rows = body.data();
if (rows == null || rows.isEmpty()) {
result.put("success", true);
result.put("message", "接口成功,本页无订单数据");
result.put("synced", 0);
result.put("remoteCount", body.count());
return result;
}
String tid = tenantId.trim();
List<LbOrderRow> entities = new ArrayList<>(rows.size());
for (HxrOrderRow row : rows) {
entities.add(toLbOrderRow(row, tid));
}
boolean ok = this.saveOrUpdateBatch(entities);
result.put("success", ok);
result.put("message", ok ? "同步完成" : "批量保存失败");
result.put("synced", ok ? entities.size() : 0);
result.put("remoteCount", body.count());
result.put("allMoney", body.allMoney());
return result;
} catch (Exception e) {
result.put("success", false);
result.put("message", "同步异常:" + e.getMessage());
result.put("synced", 0);
return result;
}
}
private static LbOrderRow toLbOrderRow(HxrOrderRow row, String tenantId) {
LbOrderRow e = new LbOrderRow();
e.setId(row.id());
e.setOldId(row.oldId());
e.setTenantId(tenantId);
e.setSellerId(row.sellerId());
e.setBuyerId(row.buyerId());
e.setOrderSn(row.orderSn());
e.setTotalMoney(row.totalMoney());
e.setPayTime(row.payTime());
e.setPayImg(row.payImg());
e.setStatus(row.status());
e.setIsResell(row.isResell());
e.setIsShow(row.isShow());
e.setConsignee(row.consignee());
e.setPhone(row.phone());
e.setProvince(row.province());
e.setCity(row.city());
e.setArea(row.area());
e.setAddress(row.address());
e.setMerchandiseId(row.merchandiseId());
e.setConfirmTime(row.confirmTime());
e.setBuyTime(row.buyTime());
e.setCreatedAt(row.createdAt());
e.setUpdatedAt(row.updatedAt());
return e;
}
}

View File

@@ -0,0 +1,8 @@
# 复制为 application-local-dingtalk.yml 并填入 access-token及可选 secret
# application-local-dingtalk.yml 已在 .gitignore 中,不会被提交。
lb:
assessment-apply:
dingtalk:
enabled: true
access-token: "从钉钉 Webhook URL 中 access_token= 后的值"
# secret: "若机器人安全设置为加签则必填"

View File

@@ -11,6 +11,15 @@ app:
yihangyi:
asr:
enabled: true
# hxrd 后台订单列表LBAdminPullScheduler每 50 分钟一次,仅工作日 18:1023:59:59 执行,周末不跑)。默认关闭。
hxr:
admin:
enabled: false
# 首次拉单延迟毫秒0 表示启动后尽快执行;集成测试会设为较大值避免与用例并发
pull-initial-delay-ms: 0
cookie: PHPSID=f3710baf6381da418aa832660ccb9d08
phpsid: "f3710baf6381da418aa832660ccb9d08"
# DashScope API配置
dashscope:
api:
@@ -89,6 +98,9 @@ logging:
spring:
# 可选加载本地钉钉配置(见 application-local-dingtalk.example.yml实际文件已 .gitignore
config:
import: optional:classpath:application-local-dingtalk.yml
autoconfigure:
exclude:
- dev.langchain4j.community.store.embedding.redis.spring.RedisEmbeddingStoreAutoConfiguration
@@ -278,11 +290,13 @@ amap:
# 申请评估 - 钉钉群机器人(自定义机器人 Webhook
lb:
# 本文件已加入 .gitignore仅本机生效勿将真实 token 提交到仓库。
# 若机器人启用了「加签」,请增加 secret 行SEC 开头)。
assessment-apply:
dingtalk:
enabled: ${LB_ASSESSMENT_DINGTALK_ENABLED:true}
access-token: ${LB_ASSESSMENT_DINGTALK_ACCESS_TOKEN:}
secret: ${LB_ASSESSMENT_DINGTALK_SECRET:}
enabled: true
access-token: 65aad0554c700d12d2ea71f9c62a3214d5f8f8b2b24679d30489ce9c11a6105c
secret: "SECxxxxxxxx"
# MinIO 对象存储配置
minio:

View File

@@ -0,0 +1,178 @@
package com.rj.integration;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.rj.dto.hxr.HxrOrderRow;
import com.rj.dto.hxr.HxrOrderSelectResponse;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* 手动调用 hxrd 后台「订单列表」JSON 接口(需有效 PHPSID
* <p>
* 推荐:将 {@code src/test/resources/hxr-order-integration.example.properties} 复制为
* {@code hxr-order-integration.properties},填写 {@code hxr.order.cookie} 或 {@code hxr.order.phpsid}
* (该文件已加入 .gitignore
* <p>
* 覆盖优先级从高到低JVM 系统属性({@code -Dhxr.order.cookie} / {@code -Dhxr.order.phpsid}
* &gt; 环境变量 {@code HXR_ORDER_COOKIE} &gt; classpath 配置文件 {@code hxr-order-integration.properties}。
* <p>
* 不要手动设置含 {@code br}、{@code zstd} 的 {@code Accept-Encoding}{@link HttpClient} 只会对默认协商的编码自动解压,
* 否则服务端可能返回 Brotli/zstd响应体会变成乱码。
*/
class HxrAdminOrderSelectHttpTest {
private static final String CONFIG_RESOURCE = "hxr-order-integration.properties";
private static final String PROP_COOKIE = "hxr.order.cookie";
private static final String PROP_PHPSID = "hxr.order.phpsid";
private static final String ENV_COOKIE = "HXR_ORDER_COOKIE";
private static final String BASE_URL = "https://hxrdhoutai.hxrdsm.cn/app/admin/order/select";
private static final String QUERY =
"?page=1&limit=20"
+ "&order_sn%5B0%5D=like&order_sn%5B1%5D="
+ "&seller_id=&buyer_id=&status=&is_resell=0&is_show="
+ "&buy_time%5B0%5D=&buy_time%5B1%5D=&confirm_time%5B0%5D=&confirm_time%5B1%5D=";
private static volatile Properties integrationProps;
private static final ObjectMapper HXR_ORDER_JSON = new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Test
void getOrderSelectJson() throws Exception {
String cookie = resolveCookie();
Assumptions.assumeFalse(
cookie == null || cookie.isBlank(),
"跳过:请在 src/test/resources/ 下复制 "
+ "hxr-order-integration.example.properties 为 "
+ CONFIG_RESOURCE
+ " 并填写 hxr.order.cookie 或 hxr.order.phpsid或设置环境变量 "
+ ENV_COOKIE
+ " / VM 选项 -D"
+ PROP_COOKIE
+ " / -D"
+ PROP_PHPSID);
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(15))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + QUERY))
.timeout(Duration.ofSeconds(60))
.header("Accept", "application/json, text/javascript, */*; q=0.01")
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
.header("Cookie", cookie)
.header("Priority", "u=1, i")
.header("Referer", "https://hxrdhoutai.hxrdsm.cn/app/admin/order/index")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP " + response.statusCode());
assertTrue(
response.statusCode() >= 200 && response.statusCode() < 300,
"unexpected status: " + response.statusCode());
HxrOrderSelectResponse body = HXR_ORDER_JSON.readValue(response.body(), HxrOrderSelectResponse.class);
assertEquals(0, body.code(), () -> "api code != 0, msg=" + body.msg() + ", body=" + response.body());
for (HxrOrderRow row : body.data()) {
System.out.println(HXR_ORDER_JSON.writeValueAsString(row));
}
}
@Test
void parseSampleJson_printsOneLinePerOrder() throws Exception {
String json =
"""
{"code":0,"msg":"ok","count":7,"data":[{"id":358078,"old_id":null,"seller_id":97154,"buyer_id":97374,"order_sn":"47379117788104684482","total_money":"23423.95","pay_time":"2026-05-15 10:04:32","pay_img":"\\/upload\\/image\\/20260515\\/ec791c7a9ed2f5ade9450c0f50efd819_6a067f2f9317d.jpeg","status":2,"is_resell":0,"is_show":1,"consignee":"陈科亦","phone":"18810005315","province":"河北省","city":"廊坊市","area":"三河市","address":"燕郊开发区金谷爱舒荷","merchandise_id":344196,"confirm_time":"2026-05-15 10:08:04","buy_time":"2026-05-15 10:01:08","created_at":"2026-05-15 10:01:08","updated_at":"2026-05-15 10:08:04"},{"id":357968,"old_id":null,"seller_id":98719,"buyer_id":96030,"order_sn":"03069117788104035736","total_money":"27037.65","pay_time":"2026-05-15 10:02:48","pay_img":"\\/upload\\/image\\/20260515\\/37d6d7d09447d6c8ce41abd0727a39d5_6a067ec77a519.jpeg","status":1,"is_resell":0,"is_show":1,"consignee":"陈晓琴","phone":"17733627528","province":"河北省","city":"廊坊市","area":"三河市","address":"燕郊镇金谷爰舒荷十号楼","merchandise_id":344269,"confirm_time":null,"buy_time":"2026-05-15 10:00:03","created_at":"2026-05-15 10:00:03","updated_at":"2026-05-15 10:02:48"}],"all_money":"182851.70"}
""";
HxrOrderSelectResponse body = HXR_ORDER_JSON.readValue(json.trim(), HxrOrderSelectResponse.class);
assertEquals(0, body.code());
assertEquals(2, body.data().size());
for (HxrOrderRow row : body.data()) {
System.out.println(HXR_ORDER_JSON.writeValueAsString(row));
}
}
private static String resolveCookie() {
String fromFileCookie = firstNonBlank(integrationProps().getProperty(PROP_COOKIE));
String fromFilePhpsid = firstNonBlank(integrationProps().getProperty(PROP_PHPSID));
String fromProp = firstNonBlank(System.getProperty(PROP_COOKIE));
String phpsidProp = firstNonBlank(System.getProperty(PROP_PHPSID));
String fromEnv = firstNonBlank(System.getenv(ENV_COOKIE));
if (fromProp != null) {
return fromProp;
}
if (phpsidProp != null) {
return "PHPSID=" + phpsidProp;
}
if (fromEnv != null) {
return fromEnv;
}
if (fromFileCookie != null) {
return fromFileCookie;
}
if (fromFilePhpsid != null) {
return "PHPSID=" + fromFilePhpsid;
}
return "";
}
private static String firstNonBlank(String s) {
if (s == null) {
return null;
}
String t = s.trim();
return t.isEmpty() ? null : t;
}
private static Properties integrationProps() {
if (integrationProps != null) {
return integrationProps;
}
synchronized (HxrAdminOrderSelectHttpTest.class) {
if (integrationProps != null) {
return integrationProps;
}
Properties p = new Properties();
try (InputStream in =
HxrAdminOrderSelectHttpTest.class.getClassLoader().getResourceAsStream(CONFIG_RESOURCE)) {
if (in != null) {
try (InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
p.load(reader);
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
integrationProps = p;
return integrationProps;
}
}
}

View File

@@ -0,0 +1,94 @@
package com.rj.scheduler;
import com.rj.service.HxrAdminOrderSelectService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* {@link LBAdminPullScheduler#pullAdminOrders()} 集成测试:全量 Spring 上下文 + {@link LBAdminPullSchedulerTestClockConfiguration}
* HTTP 拉单由 {@link MockitoBean} 替身,避免外网与真实 Cookie。
*
* <p>另见:{@link LBAdminPullSchedulerWhenHxrDisabledIntegrationTest}、{@link LBAdminPullSchedulerWhenGlobalSchedulerOffIntegrationTest}。
*/
@SpringBootTest
@TestPropertySource(
properties = {
"spring.task.scheduling.enabled=false",
"hxr.admin.pull-initial-delay-ms=604800000",
"app.scheduler.start=true",
"hxr.admin.enabled=true",
"app.timezone=Asia/Shanghai"
})
@Import(LBAdminPullSchedulerTestClockConfiguration.class)
@ActiveProfiles("test")
class LBAdminPullSchedulerIntegrationTest {
@Autowired
private LBAdminPullScheduler lbAdminPullScheduler;
@Autowired
private LBAdminPullSchedulerTestClockConfiguration.MutableClock mutableClock;
@MockitoBean
private HxrAdminOrderSelectService hxrAdminOrderSelectService;
@BeforeEach
void resetInvocationHistory() {
Mockito.clearInvocations(hxrAdminOrderSelectService);
}
@Test
void pullAdminOrders_eveningWindow_invokesOrderSelectService() throws Exception {
mutableClock.setTo(
ZonedDateTime.of(LocalDate.of(2026, 5, 15), LocalTime.of(20, 0), ZoneId.of("Asia/Shanghai")));
lbAdminPullScheduler.pullAdminOrders();
verify(hxrAdminOrderSelectService).fetchAndLogEachOrder();
}
@Test
void pullAdminOrders_exactly1810_invokesOrderSelectService() throws Exception {
mutableClock.setTo(
ZonedDateTime.of(LocalDate.of(2026, 5, 15), LocalTime.of(18, 10), ZoneId.of("Asia/Shanghai")));
lbAdminPullScheduler.pullAdminOrders();
verify(hxrAdminOrderSelectService).fetchAndLogEachOrder();
}
@Test
void pullAdminOrders_beforeWindow_doesNotInvoke() throws Exception {
mutableClock.setTo(
ZonedDateTime.of(LocalDate.of(2026, 5, 15), LocalTime.of(12, 0), ZoneId.of("Asia/Shanghai")));
lbAdminPullScheduler.pullAdminOrders();
verify(hxrAdminOrderSelectService, never()).fetchAndLogEachOrder();
}
@Test
void pullAdminOrders_saturdayEveningInTimeWindow_doesNotInvoke() throws Exception {
mutableClock.setTo(
ZonedDateTime.of(LocalDate.of(2026, 5, 16), LocalTime.of(20, 0), ZoneId.of("Asia/Shanghai")));
lbAdminPullScheduler.pullAdminOrders();
verify(hxrAdminOrderSelectService, never()).fetchAndLogEachOrder();
}
}

View File

@@ -0,0 +1,48 @@
package com.rj.scheduler;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
/**
* 为 {@link LBAdminPullScheduler} 集成测试提供可拨 {@link Clock}{@link MutableClock})。
*/
@TestConfiguration
public class LBAdminPullSchedulerTestClockConfiguration {
@Bean
@Primary
MutableClock lbAdminPullTestMutableClock() {
return new MutableClock();
}
/** 测试中可切换时刻的 {@link Clock},用于固定 18:1023:59 时间窗口。 */
public static final class MutableClock extends Clock {
private volatile Clock delegate = Clock.systemDefaultZone();
public void setTo(ZonedDateTime zdt) {
delegate = Clock.fixed(zdt.toInstant(), zdt.getZone());
}
@Override
public ZoneId getZone() {
return delegate.getZone();
}
@Override
public Clock withZone(ZoneId zone) {
return delegate.withZone(zone);
}
@Override
public Instant instant() {
return delegate.instant();
}
}
}

View File

@@ -0,0 +1,51 @@
package com.rj.scheduler;
import com.rj.service.HxrAdminOrderSelectService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@SpringBootTest
@TestPropertySource(
properties = {
"spring.task.scheduling.enabled=false",
"hxr.admin.pull-initial-delay-ms=604800000",
"app.scheduler.start=false",
"hxr.admin.enabled=true",
"app.timezone=Asia/Shanghai"
})
@Import(LBAdminPullSchedulerTestClockConfiguration.class)
@ActiveProfiles("test")
class LBAdminPullSchedulerWhenGlobalSchedulerOffIntegrationTest {
@Autowired
private LBAdminPullScheduler lbAdminPullScheduler;
@Autowired
private LBAdminPullSchedulerTestClockConfiguration.MutableClock mutableClock;
@MockitoBean
private HxrAdminOrderSelectService hxrAdminOrderSelectService;
@Test
void pullAdminOrders_skipsWhenAppSchedulerStartFalse() throws Exception {
mutableClock.setTo(
ZonedDateTime.of(LocalDate.of(2026, 5, 15), LocalTime.of(20, 0), ZoneId.of("Asia/Shanghai")));
lbAdminPullScheduler.pullAdminOrders();
verify(hxrAdminOrderSelectService, never()).fetchAndLogEachOrder();
}
}

View File

@@ -0,0 +1,51 @@
package com.rj.scheduler;
import com.rj.service.HxrAdminOrderSelectService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@SpringBootTest
@TestPropertySource(
properties = {
"spring.task.scheduling.enabled=false",
"hxr.admin.pull-initial-delay-ms=604800000",
"app.scheduler.start=true",
"hxr.admin.enabled=false",
"app.timezone=Asia/Shanghai"
})
@Import(LBAdminPullSchedulerTestClockConfiguration.class)
@ActiveProfiles("test")
class LBAdminPullSchedulerWhenHxrDisabledIntegrationTest {
@Autowired
private LBAdminPullScheduler lbAdminPullScheduler;
@Autowired
private LBAdminPullSchedulerTestClockConfiguration.MutableClock mutableClock;
@MockitoBean
private HxrAdminOrderSelectService hxrAdminOrderSelectService;
@Test
void pullAdminOrders_eveningWindow_stillSkips() throws Exception {
mutableClock.setTo(
ZonedDateTime.of(LocalDate.of(2026, 5, 15), LocalTime.of(20, 0), ZoneId.of("Asia/Shanghai")));
lbAdminPullScheduler.pullAdminOrders();
verify(hxrAdminOrderSelectService, never()).fetchAndLogEachOrder();
}
}

View File

@@ -0,0 +1,7 @@
# 复制本文件为 hxr-order-integration.properties 后填写(后者已加入 .gitignore勿提交真实 Cookie
# 二选一:
# hxr.order.cookie=PHPSID=你的会话值
# hxr.order.phpsid=仅填PHPSID的值不含 PHPSID= 前缀)
hxr.order.cookie=
# hxr.order.phpsid=