Commit 0d2f60f7 by renyizhao

商品可永久 newapi登录修改

parent 17beaa7b
This diff is collapsed. Click to expand it.
package com.luhu.computility.module.apihub.controller.admin.newapi;
import lombok.AllArgsConstructor;
import lombok.experimental.Accessors;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* NewAPI /api/user/login 响应 data 字段映射
*
* <p>NewAPI v3 登录成功时,data 顶层字段结构:
* <pre>
* {
* "access_token": "...",
* "token_type": "Bearer",
* "access_expires_at": ...,
* "session": { ... },
* "user": { "id": 123, "username": "...", ... }
* }
* </pre>
* 之前 puhui 错误地把整个 data 映射到 NewApiUserVO,导致 id 等字段全为 null;
* 本 VO 直接持有 access_token 和嵌套 user.id,避免再次去取 session cookie / 走 /api/user/token 生成访问令牌。
*/
@Data
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class NewApiLoginRespVO {
/** data.access_token:可直接作为后续接口的 Bearer 凭据 */
private String accessToken;
/** data.token_type(如 "Bearer") */
private String tokenType;
/** data.user.id:NewAPI 用户 ID */
private Long userId;
/** data.user.username:冗余,便于日志/持久化 */
private String username;
}
......@@ -83,44 +83,31 @@ public class AiTokenService {
log.info("[AiTokenService.createTokenStep2] 准备登录: username={}", username);
try {
NewApiResponse<NewApiUserVO> loginResp = newApiClient.login(username, password);
NewApiResponse<NewApiLoginRespVO> loginResp = newApiClient.login(username, password);
log.info("[AiTokenService.createTokenStep2] 登录结果: success={}, message={}", loginResp.getSuccess(), loginResp.getMessage());
if (!Boolean.TRUE.equals(loginResp.getSuccess()) || loginResp.getData() == null) {
return AiTokenResult.builder()
.success(false)
.message("登录NewAPI失败: " + loginResp.getMessage())
.build();
}
Integer newApiUserId = loginResp.getData().getId();
log.info("[AiTokenService.createTokenStep2] 登录成功, newApiUserId={}", newApiUserId);
String session = newApiClient.getSessionCookie(loginResp.getHttpResponse());
log.info("[AiTokenService.createTokenStep2] 获取session: {}", session != null ? "成功" : "失败");
NewApiResponse<String> tokenResp = newApiClient.generateAccessToken(session, newApiUserId);
log.info("[AiTokenService.createTokenStep2] 生成AccessToken结果: success={}, message={}", tokenResp.getSuccess(), tokenResp.getMessage());
if (!Boolean.TRUE.equals(tokenResp.getSuccess()) || tokenResp.getData() == null) {
return AiTokenResult.builder()
.success(false)
.message("生成访问令牌失败: " + tokenResp.getMessage())
.newApiUsername(username)
.newApiPassword(password)
.newApiUserId(newApiUserId.longValue())
.build();
}
String accessToken = tokenResp.getData();
log.info("[AiTokenService.createTokenStep2] AccessToken生成成功: {}***", accessToken.substring(0, Math.min(5, accessToken.length())));
Long newApiUserId = loginResp.getData().getUserId();
String accessToken = loginResp.getData().getAccessToken();
log.info("[AiTokenService.createTokenStep2] 登录成功, newApiUserId={}, accessToken={}***",
newApiUserId, accessToken.substring(0, Math.min(5, accessToken.length())));
return createTokenStep4(newApiUserId, accessToken, userId, username, password);
// NewAPI v3 登录响应已直接返回 access_token,无需再调 /api/user/token;
// 直接以该 token 作为 Bearer 凭据进入 step4 创建 API Key。
return createTokenStep4(newApiUserId.intValue(), accessToken, userId, username, password);
} catch (Exception e) {
log.error("[AiTokenService.createTokenStep2] 登录异常", e);
return AiTokenResult.builder()
.success(false)
.message("登录NewAPI异常: " + e.getMessage())
.message("登录NewAPI异常: " + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()))
.newApiUsername(username)
.newApiPassword(password)
.build();
......
......@@ -96,7 +96,22 @@ public class NewApiClient {
}
}
public NewApiResponse<NewApiUserVO> login(String username, String password) {
/**
* 用户登录(NewAPI v3 /api/user/login)
*
* <p>NewAPI v3 登录响应 data 字段结构:
* <pre>
* {
* "access_token": "...", // 直接可用作 Bearer 凭据
* "token_type": "Bearer",
* "user": { "id": 123, "username": "...", ... }
* }
* </pre>
* 旧实现错误地把整个 data 映射到 NewApiUserVO,导致 userId 拿不到;
* 本方法改为显式从 data.access_token 和 data.user.id 取值,避免依赖
* session cookie 和后续 /api/user/token 步骤。
*/
public NewApiResponse<NewApiLoginRespVO> login(String username, String password) {
String url = newApiProperties.getBaseUrl() + "/api/user/login";
log.info("[NewApiClient.login] 请求URL: {}", url);
......@@ -113,18 +128,88 @@ public class NewApiClient {
.execute();
log.info("[NewApiClient.login] 响应状态: {}", response.getStatus());
log.info("[NewApiClient.login] 响应Cookie: {}", response.getCookies());
return parseResponse(response, NewApiUserVO.class);
NewApiResponse<NewApiLoginRespVO> resp = parseLoginResponse(response);
resp.setHttpResponse(response);
return resp;
} catch (Exception e) {
log.error("[NewApiClient.login] 请求异常", e);
return NewApiResponse.<NewApiUserVO>builder()
return NewApiResponse.<NewApiLoginRespVO>builder()
.success(false)
.message("请求异常: " + e.getMessage())
.build();
}
}
/**
* 解析 NewAPI /api/user/login 响应
*
* <p>只关心 data.access_token、data.token_type、data.user.id、data.user.username;
* 不抛异常,统一返回 NewApiResponse 调用方判断。
*/
private NewApiResponse<NewApiLoginRespVO> parseLoginResponse(HttpResponse response) {
try {
String body = response.body();
if (StrUtil.isBlank(body)) {
return NewApiResponse.<NewApiLoginRespVO>builder()
.success(false)
.message("Empty response")
.build();
}
JSONObject json = JSONUtil.parseObj(body);
NewApiResponse<NewApiLoginRespVO> result = new NewApiResponse<>();
result.setSuccess(json.getBool("success"));
result.setMessage(json.getStr("message"));
if (!Boolean.TRUE.equals(json.getBool("success"))) {
return result;
}
JSONObject data = json.getJSONObject("data");
if (data == null) {
return NewApiResponse.<NewApiLoginRespVO>builder()
.success(false)
.message("登录响应缺少 data 字段")
.build();
}
NewApiLoginRespVO vo = new NewApiLoginRespVO();
vo.setAccessToken(data.getStr("access_token"));
vo.setTokenType(data.getStr("token_type"));
JSONObject user = data.getJSONObject("user");
if (user != null) {
Object idObj = user.get("id");
if (idObj instanceof Number) {
vo.setUserId(((Number) idObj).longValue());
} else if (idObj != null) {
try {
vo.setUserId(Long.parseLong(idObj.toString()));
} catch (NumberFormatException ignored) {
}
}
vo.setUsername(user.getStr("username"));
}
if (vo.getAccessToken() == null || vo.getAccessToken().isEmpty()
|| vo.getUserId() == null) {
return NewApiResponse.<NewApiLoginRespVO>builder()
.success(false)
.message("登录响应缺少 access_token 或 user.id")
.build();
}
result.setData(vo);
return result;
} catch (Exception e) {
log.error("[NewApiClient.parseLoginResponse] 解析响应异常", e);
return NewApiResponse.<NewApiLoginRespVO>builder()
.success(false)
.message("解析响应失败: " + e.getMessage())
.build();
}
}
public String getSessionCookie(HttpResponse loginResponse) {
String session = loginResponse.getCookieValue("session");
log.info("[NewApiClient.getSessionCookie] session={}", session != null ? session : "null");
......
......@@ -18,8 +18,9 @@ public class ResourceSkuSaveReqVO {
@NotNull(message = "关联的SPU ID不能为空")
private Long spuId;
@Schema(description = "租赁天数(30/90/365等)", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "租赁天数(30/90/365等)不能为空")
@Schema(description = "租赁天数(30/90/365等;0=永久)", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "租赁天数不能为空")
@Min(value = 0, message = "租赁天数不能小于 0")
private Integer durationDays;
@Schema(description = "支付价格(单位:分)", requiredMode = Schema.RequiredMode.REQUIRED, example = "22794")
......
......@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
......@@ -52,8 +53,9 @@ public class AppResourceOrderSnapshotSaveReqVO {
// - 数据库 spu_name 列允许 NULL
private String spuName;
@Schema(description = "租赁天数", requiredMode = Schema.RequiredMode.REQUIRED, example = "7")
@Schema(description = "租赁天数(0=永久)", example = "7")
@NotNull(message = "租赁天数不能为空")
@Min(value = 0, message = "租赁天数不能小于 0")
private Integer durationDays;
@Schema(description = "租赁开始时间")
......
......@@ -76,7 +76,8 @@ public class AppResourceSkuController {
respVO.setName(spuVo.getName());
respVO.setRemark(spuVo.getIntro());
respVO.setPrice(sku.getPaymentPrice());
respVO.setFeeInfo(sku.getDurationDays() + "天");
// duration_days=0 表示永久,前端「/X天」后缀置空
respVO.setFeeInfo(sku.getDurationDays() != null && sku.getDurationDays() > 0 ? sku.getDurationDays() + "天" : "");
respVO.setSpecMap(spuVo.getSpecMap());
respVO.setLocation(spuVo.getLocation());
return respVO;
......@@ -102,7 +103,8 @@ public class AppResourceSkuController {
respVO.setName(resourceSpuVo.getName());
respVO.setRemark(resourceSpuVo.getIntro());
respVO.setPrice(resourceSku.getPaymentPrice());
respVO.setFeeInfo(resourceSku.getDurationDays() + "天");
// duration_days=0 表示永久,前端「/X天」后缀置空
respVO.setFeeInfo(resourceSku.getDurationDays() != null && resourceSku.getDurationDays() > 0 ? resourceSku.getDurationDays() + "天" : "");
respVO.setSpecMap(resourceSpuVo.getSpecMap());
respVO.setLocation(resourceSpuVo.getLocation());
return success(respVO);
......
......@@ -249,7 +249,11 @@ public class ResourceOrderServiceImpl implements ResourceOrderService {
.setAppKey(resourceOrderProperties.getPayAppKey()).setUserIp(order.getUserIp());
// 商户相关字段
payOrderCreateReqDTO.setMerchantOrderId(String.valueOf(order.getId()));
String subject = order.getSpuName() + " - " + getDurationDaysFromSku(order.getSkuId()) + "天";
// 永久订单(durationDays=0)只拼 SPU 名,不带「 - X天」后缀
Integer payDurationDays = getDurationDaysFromSku(order.getSkuId());
String subject = (payDurationDays != null && payDurationDays > 0)
? order.getSpuName() + " - " + payDurationDays + "天"
: order.getSpuName();
subject = StrUtils.maxLength(subject, PayOrderCreateReqDTO.SUBJECT_MAX_LENGTH); // 避免超过 32 位
payOrderCreateReqDTO.setSubject(subject);
payOrderCreateReqDTO.setBody(subject);
......@@ -463,7 +467,10 @@ public class ResourceOrderServiceImpl implements ResourceOrderService {
if (snapshot.getSpecSnapshot() != null) {
respVO.setSpecMap(JSONUtil.toBean(snapshot.getSpecSnapshot(), Map.class));
}
respVO.setSkuName(snapshot.getSpuName() + " - " + snapshot.getDurationDays() + "天");
// 永久订单(duration_days=0)只拼 SPU 名,不带「 - X天」后缀
respVO.setSkuName(snapshot.getDurationDays() != null && snapshot.getDurationDays() > 0
? snapshot.getSpuName() + " - " + snapshot.getDurationDays() + "天"
: snapshot.getSpuName());
// 快照中没有的字段(分类、位置)需要从SPU获取
ResourceSkuDO sku = resourceSkuService.getResourceSku(order.getSkuId());
......@@ -490,7 +497,11 @@ public class ResourceOrderServiceImpl implements ResourceOrderService {
respVO.setSpuName(spu.getName());
respVO.setCategoryId(spu.getCategoryId());
respVO.setLocation(spu.getLocation());
respVO.setSkuName(spu.getName() + " - " + sku.getDurationDays() + "天");
// 永久订单(duration_days=0)只拼 SPU 名,不带「 - X天」后缀
Integer legacyDurationDays = sku.getDurationDays();
respVO.setSkuName(legacyDurationDays != null && legacyDurationDays > 0
? spu.getName() + " - " + legacyDurationDays + "天"
: spu.getName());
// 设置分类名称
if (spu.getCategoryId() != null) {
......@@ -531,7 +542,10 @@ public class ResourceOrderServiceImpl implements ResourceOrderService {
if (snapshot.getSpecSnapshot() != null) {
respVO.setSpecMap(JSONUtil.toBean(snapshot.getSpecSnapshot(), Map.class));
}
respVO.setSkuName(snapshot.getSpuName() + " - " + snapshot.getDurationDays() + "天");
// 永久订单(duration_days=0)只拼 SPU 名,不带「 - X天」后缀
respVO.setSkuName(snapshot.getDurationDays() != null && snapshot.getDurationDays() > 0
? snapshot.getSpuName() + " - " + snapshot.getDurationDays() + "天"
: snapshot.getSpuName());
// 快照中没有的字段(分类、位置)需要从SPU获取
ResourceSkuDO sku = resourceSkuService.getResourceSku(order.getSkuId());
......@@ -663,7 +677,11 @@ public class ResourceOrderServiceImpl implements ResourceOrderService {
CommonWpgjCreateReqDTO createReqDTO = new CommonWpgjCreateReqDTO();
createReqDTO.setBusinessType(1); // 1-算力资源订单
createReqDTO.setBusinessOrderId(order.getId());
String orderTitle = order.getSpuName() + " - " + getDurationDaysFromSku(order.getSkuId()) + "天";
// 永久订单(durationDays=0)只拼 SPU 名,不带「 - X天」后缀
Integer wpgjDurationDays = getDurationDaysFromSku(order.getSkuId());
String orderTitle = (wpgjDurationDays != null && wpgjDurationDays > 0)
? order.getSpuName() + " - " + wpgjDurationDays + "天"
: order.getSpuName();
createReqDTO.setOrderTitle(orderTitle);
createReqDTO.setPayAmount(order.getPaymentPrice().intValue());
......@@ -916,7 +934,7 @@ public class ResourceOrderServiceImpl implements ResourceOrderService {
throw exception(RESOURCE_ORDER_DELIVERY_FIELDS_REQUIRED);
}
// 2. 取租赁天数
// 2. 取租赁天数(0=永久,不设置结束时间)
Integer durationDays = getDurationDaysFromSku(order.getSkuId());
if (durationDays == null) {
throw exception(RESOURCE_ORDER_SKU_NOT_EXISTS);
......@@ -924,6 +942,7 @@ public class ResourceOrderServiceImpl implements ResourceOrderService {
// 3. 算租赁时间
LocalDateTime now = LocalDateTime.now();
LocalDateTime rentEndTime = durationDays > 0 ? now.plusDays(durationDays) : null;
// 4. 写订单
ResourceOrderDO updateOrder = new ResourceOrderDO();
......@@ -934,7 +953,7 @@ public class ResourceOrderServiceImpl implements ResourceOrderService {
updateOrder.setDeliveryPassword(reqVO.getDeliveryPassword());
updateOrder.setDeliveredTime(now);
updateOrder.setRentStartTime(now);
updateOrder.setRentEndTime(now.plusDays(durationDays));
updateOrder.setRentEndTime(rentEndTime);
updateOrder.setDeliverOperatorId(operatorId);
updateOrder.setDeliverOperatorName(getOperatorName(operatorId));
resourceOrderMapper.updateById(updateOrder);
......
......@@ -212,10 +212,8 @@ wx:
# 芋道配置项,设置当前项目所有自定义的配置
computility:
new-api:
# 线上 NewAPI 地址(用于获取 /api/pricing 数据)
base-url: http://phslgld.hnluchuan.com:3000
# 线上 NewAPI 管理员 token(在 NewAPI 后台 → 用户管理 → 管理员账号详情获取)
admin-token: V33oMN4fI5H0wKSs0gXipXZ4WvU6DQ==
base-url: http://172.25.164.0:3000
admin-token: nfcJyVtYb6xtREHA2MyL4f4o5TDg/qU=
admin-user-id: 1
captcha:
enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试;
......
-- ============================================================
-- 算力资源 SKU「永久」租赁时长字典项
-- 库:new_computility
-- 文件:2026_09_17_add_compute_resource_duration_permanent.sql
--
-- 【背景】
-- 算力资源 SKU 原本强制要求租赁天数(30/90/365 等),不支持「永久」商品。
-- 本次放开:duration_days = 0 表示永久,下单时 rent_end_time 不设置,
-- expireFinishedOrders 定时任务依赖 BETWEEN NULL 跳过永久订单。
--
-- 【目标】
-- 给字典 compute_resource_duration 新增一条 value=0, label=永久 的选项,
-- 让管理端 SKU 表单「租赁时长」下拉框出现「永久」选项。
--
-- 【安全说明】
-- - INSERT IGNORE:已存在则跳过(id 自增会跳过,dict_type+value 唯一则跳过)
-- - 不会影响现有 30/90/365 等天数选项
-- - 不修改表结构
-- ============================================================
USE `new_computility`;
-- 新增「永久」字典项
INSERT IGNORE INTO `system_dict_data`
(`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
(0, '永久', '0', 'compute_resource_duration', 0, 'default', '', 'duration_days=0 表示永久租赁', '1', NOW(), '1', NOW(), b'0');
-- 验证:
-- SELECT * FROM system_dict_data WHERE dict_type = 'compute_resource_duration' ORDER BY sort;
-- 期望:除原有 30天/90天/365天 等项外,新增一条 value=0 label=永久 sort=0 的项
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment