因为数据库连接问题导致的插入数据库异常
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
34
src/main/java/com/rj/config/HxrAdminProperties.java
Normal file
34
src/main/java/com/rj/config/HxrAdminProperties.java
Normal 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 = "";
|
||||
}
|
||||
98
src/main/java/com/rj/controller/LbOrderRowController.java
Normal file
98
src/main/java/com/rj/controller/LbOrderRowController.java
Normal 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);
|
||||
}
|
||||
}
|
||||
24
src/main/java/com/rj/dto/LbOrderRowSyncFromHxrRequest.java
Normal file
24
src/main/java/com/rj/dto/LbOrderRowSyncFromHxrRequest.java
Normal 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;
|
||||
}
|
||||
28
src/main/java/com/rj/dto/hxr/HxrOrderRow.java
Normal file
28
src/main/java/com/rj/dto/hxr/HxrOrderRow.java
Normal 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) {}
|
||||
9
src/main/java/com/rj/dto/hxr/HxrOrderSelectResponse.java
Normal file
9
src/main/java/com/rj/dto/hxr/HxrOrderSelectResponse.java
Normal 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) {}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/main/java/com/rj/entity/JsonFlexibleLocalDateTime.java
Normal file
19
src/main/java/com/rj/entity/JsonFlexibleLocalDateTime.java
Normal 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 {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
113
src/main/java/com/rj/entity/LbOrderRow.java
Normal file
113
src/main/java/com/rj/entity/LbOrderRow.java
Normal 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;
|
||||
}
|
||||
9
src/main/java/com/rj/mapper/LbOrderRowMapper.java
Normal file
9
src/main/java/com/rj/mapper/LbOrderRowMapper.java
Normal 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> {
|
||||
}
|
||||
66
src/main/java/com/rj/scheduler/LBAdminPullScheduler.java
Normal file
66
src/main/java/com/rj/scheduler/LBAdminPullScheduler.java
Normal 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:10~23: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:10~23:59:59 窗口内)");
|
||||
hxrAdminOrderSelectService.fetchAndLogEachOrder();
|
||||
} catch (Exception e) {
|
||||
log.error("未寄卖 admin 订单拉取失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
148
src/main/java/com/rj/service/HxrAdminOrderSelectService.java
Normal file
148
src/main/java/com/rj/service/HxrAdminOrderSelectService.java
Normal 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) + "...";
|
||||
}
|
||||
}
|
||||
31
src/main/java/com/rj/service/ILbOrderRowService.java
Normal file
31
src/main/java/com/rj/service/ILbOrderRowService.java
Normal 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);
|
||||
}
|
||||
273
src/main/java/com/rj/service/impl/LbOrderRowServiceImpl.java
Normal file
273
src/main/java/com/rj/service/impl/LbOrderRowServiceImpl.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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: "若机器人安全设置为加签则必填"
|
||||
@@ -11,6 +11,15 @@ app:
|
||||
yihangyi:
|
||||
asr:
|
||||
enabled: true
|
||||
|
||||
# hxrd 后台订单列表(LBAdminPullScheduler:每 50 分钟一次,仅工作日 18:10~23: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:
|
||||
|
||||
@@ -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})
|
||||
* > 环境变量 {@code HXR_ORDER_COOKIE} > 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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:10~23: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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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=
|
||||
Reference in New Issue
Block a user