Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
97fc1928
authored
Aug 10, 2026
by
renyizhao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
商品导入
parent
ddb18310
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
651 additions
and
2 deletions
+651
-2
computility-framework/computility-common/src/main/java/com/luhu/computility/framework/common/pojo/PageParam.java
+2
-0
computility-framework/computility-spring-boot-starter-mybatis/src/main/java/com/luhu/computility/framework/mybatis/core/dataobject/BaseDO.java
+2
-0
computility-module-compute/computility-module-compute-api/src/main/java/com/luhu/computility/module/compute/enums/ErrorCodeConstants.java
+7
-0
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/controller/admin/resourcespu/ResourceSpuImportController.java
+88
-0
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/controller/admin/resourcespu/vo/ResourceSpuImportReqVO.java
+20
-0
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/controller/admin/resourcespu/vo/ResourceSpuImportRespVO.java
+30
-0
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/controller/admin/resourcespu/vo/ResourceSpuImportRowVO.java
+28
-0
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/controller/admin/resourcespu/vo/ResourceSpuSaveReqVO.java
+1
-2
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/service/resourcespu/ResourceSpuImportService.java
+21
-0
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/service/resourcespu/ResourceSpuImportServiceImpl.java
+413
-0
sql/migration/2026_08_10_compute_resource_spu_location_optional.sql
+20
-0
sql/migration/2026_08_10_compute_resource_spu_pic_url_optional.sql
+19
-0
No files found.
computility-framework/computility-common/src/main/java/com/luhu/computility/framework/common/pojo/PageParam.java
View file @
97fc1928
...
@@ -2,6 +2,7 @@ package com.luhu.computility.framework.common.pojo;
...
@@ -2,6 +2,7 @@ package com.luhu.computility.framework.common.pojo;
import
io.swagger.v3.oas.annotations.media.Schema
;
import
io.swagger.v3.oas.annotations.media.Schema
;
import
lombok.Data
;
import
lombok.Data
;
import
lombok.experimental.Accessors
;
import
javax.validation.constraints.Min
;
import
javax.validation.constraints.Min
;
import
javax.validation.constraints.Max
;
import
javax.validation.constraints.Max
;
...
@@ -10,6 +11,7 @@ import java.io.Serializable;
...
@@ -10,6 +11,7 @@ import java.io.Serializable;
@Schema
(
description
=
"分页参数"
)
@Schema
(
description
=
"分页参数"
)
@Data
@Data
@Accessors
(
chain
=
true
)
public
class
PageParam
implements
Serializable
{
public
class
PageParam
implements
Serializable
{
private
static
final
Integer
PAGE_NO
=
1
;
private
static
final
Integer
PAGE_NO
=
1
;
...
...
computility-framework/computility-spring-boot-starter-mybatis/src/main/java/com/luhu/computility/framework/mybatis/core/dataobject/BaseDO.java
View file @
97fc1928
...
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableLogic;
...
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableLogic;
import
com.fasterxml.jackson.annotation.JsonIgnoreProperties
;
import
com.fasterxml.jackson.annotation.JsonIgnoreProperties
;
import
com.fhs.core.trans.vo.TransPojo
;
import
com.fhs.core.trans.vo.TransPojo
;
import
lombok.Data
;
import
lombok.Data
;
import
lombok.experimental.Accessors
;
import
org.apache.ibatis.type.JdbcType
;
import
org.apache.ibatis.type.JdbcType
;
import
java.io.Serializable
;
import
java.io.Serializable
;
...
@@ -20,6 +21,7 @@ import java.time.LocalDateTime;
...
@@ -20,6 +21,7 @@ import java.time.LocalDateTime;
* @author 芋道源码
* @author 芋道源码
*/
*/
@Data
@Data
@Accessors
(
chain
=
true
)
@JsonIgnoreProperties
(
value
=
"transMap"
)
// 由于 Easy-Trans 会添加 transMap 属性,避免 Jackson 在 Spring Cache 反序列化报错
@JsonIgnoreProperties
(
value
=
"transMap"
)
// 由于 Easy-Trans 会添加 transMap 属性,避免 Jackson 在 Spring Cache 反序列化报错
public
abstract
class
BaseDO
implements
Serializable
,
TransPojo
{
public
abstract
class
BaseDO
implements
Serializable
,
TransPojo
{
...
...
computility-module-compute/computility-module-compute-api/src/main/java/com/luhu/computility/module/compute/enums/ErrorCodeConstants.java
View file @
97fc1928
...
@@ -31,4 +31,10 @@ public interface ErrorCodeConstants {
...
@@ -31,4 +31,10 @@ public interface ErrorCodeConstants {
ErrorCode
RESOURCE_SPU_SPEC_REQUIRED
=
new
ErrorCode
(
1_030_011_000
,
"算力资源商品至少选择一个规格"
);
ErrorCode
RESOURCE_SPU_SPEC_REQUIRED
=
new
ErrorCode
(
1_030_011_000
,
"算力资源商品至少选择一个规格"
);
ErrorCode
RESOURCE_SPU_SPEC_NOT_EXISTS
=
new
ErrorCode
(
1_030_011_001
,
"算力资源商品规格不存在"
);
ErrorCode
RESOURCE_SPU_SPEC_NOT_EXISTS
=
new
ErrorCode
(
1_030_011_001
,
"算力资源商品规格不存在"
);
// ========== 阶段 8 - 批量导入相关错误码 ==========
ErrorCode
RESOURCE_SPU_IMPORT_LIST_IS_EMPTY
=
new
ErrorCode
(
1_030_012_000
,
"导入文件内容为空"
);
ErrorCode
RESOURCE_SPU_IMPORT_NAME_DUPLICATE
=
new
ErrorCode
(
1_030_012_001
,
"商品名称已存在"
);
ErrorCode
RESOURCE_SPU_IMPORT_CATEGORY_INVALID
=
new
ErrorCode
(
1_030_012_002
,
"商品分类不存在或已禁用"
);
ErrorCode
RESOURCE_SPU_IMPORT_HEADER_INVALID
=
new
ErrorCode
(
1_030_012_003
,
"导入文件表头不合法"
);
}
}
\ No newline at end of file
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/controller/admin/resourcespu/ResourceSpuImportController.java
0 → 100644
View file @
97fc1928
package
com
.
luhu
.
computility
.
module
.
compute
.
controller
.
admin
.
resourcespu
;
import
cn.hutool.core.collection.CollUtil
;
import
com.alibaba.excel.EasyExcel
;
import
com.luhu.computility.framework.common.biz.system.dict.dto.DictDataRespDTO
;
import
com.luhu.computility.framework.common.pojo.CommonResult
;
import
com.luhu.computility.framework.common.util.http.HttpUtils
;
import
com.luhu.computility.module.compute.controller.admin.resourcespu.vo.ResourceSpuImportRespVO
;
import
com.luhu.computility.module.compute.service.resourcespu.ResourceSpuImportService
;
import
com.luhu.computility.module.system.api.dict.DictDataApi
;
import
io.swagger.v3.oas.annotations.Operation
;
import
io.swagger.v3.oas.annotations.Parameter
;
import
io.swagger.v3.oas.annotations.tags.Tag
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.security.access.prepost.PreAuthorize
;
import
org.springframework.validation.annotation.Validated
;
import
org.springframework.web.bind.annotation.*
;
import
org.springframework.web.multipart.MultipartFile
;
import
javax.annotation.Resource
;
import
javax.servlet.http.HttpServletResponse
;
import
java.io.IOException
;
import
java.util.ArrayList
;
import
java.util.List
;
import
static
com
.
luhu
.
computility
.
framework
.
common
.
pojo
.
CommonResult
.
success
;
/**
* 算力资源 SPU 批量导入 Controller
*/
@Tag
(
name
=
"管理后台 - 算力资源 SPU 批量导入"
)
@RestController
@RequestMapping
(
"/compute/resource-spu"
)
@Validated
@Slf4j
public
class
ResourceSpuImportController
{
/** 字典类型:算力资源配置类别 */
private
static
final
String
DICT_TYPE_CONFIG_CATEGORY
=
"compute_resource_config_category"
;
/** 固定列头 */
private
static
final
List
<
String
>
FIXED_HEADERS
=
List
.
of
(
"商品名称"
,
"商品分类"
,
"算力来源"
,
"商品简介"
,
"封面图"
,
"商品销量"
,
"状态"
);
@Resource
private
ResourceSpuImportService
resourceSpuImportService
;
@Resource
private
DictDataApi
dictDataApi
;
@GetMapping
(
"/get-import-template"
)
@Operation
(
summary
=
"获得算力资源 SPU 导入模板"
)
public
void
importTemplate
(
HttpServletResponse
response
)
throws
IOException
{
// 1. 拿字典列表(按 sort 排序?这里用默认顺序)
List
<
DictDataRespDTO
>
dictList
=
dictDataApi
.
getDictDataList
(
DICT_TYPE_CONFIG_CATEGORY
);
// 2. 构造动态表头:固定列 + 字典 label
List
<
List
<
String
>>
head
=
new
ArrayList
<>();
for
(
String
fixed
:
FIXED_HEADERS
)
{
head
.
add
(
List
.
of
(
fixed
));
}
if
(
CollUtil
.
isNotEmpty
(
dictList
))
{
for
(
DictDataRespDTO
dict
:
dictList
)
{
head
.
add
(
List
.
of
(
dict
.
getLabel
()));
}
}
// 3. 设置响应头(必须在 doWrite 之前,否则浏览器下载文件可能没后缀名)
response
.
addHeader
(
"Content-Disposition"
,
"attachment;filename="
+
HttpUtils
.
encodeUtf8
(
"算力资源SPU导入模板.xls"
));
response
.
setContentType
(
"application/vnd.ms-excel;charset=UTF-8"
);
// 4. 输出 Excel(仅表头,无数据)
EasyExcel
.
write
(
response
.
getOutputStream
())
.
head
(
head
)
.
autoCloseStream
(
false
)
.
sheet
(
"算力资源SPU导入模板"
)
.
doWrite
(
new
ArrayList
<>());
}
@PostMapping
(
"/import"
)
@Operation
(
summary
=
"导入算力资源 SPU"
)
@Parameter
(
name
=
"file"
,
description
=
"Excel 文件"
,
required
=
true
)
@PreAuthorize
(
"@ss.hasPermission('compute:resource-spu:import')"
)
public
CommonResult
<
ResourceSpuImportRespVO
>
importExcel
(
@RequestParam
(
"file"
)
MultipartFile
file
)
throws
IOException
{
return
success
(
resourceSpuImportService
.
importSpuList
(
file
));
}
}
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/controller/admin/resourcespu/vo/ResourceSpuImportReqVO.java
0 → 100644
View file @
97fc1928
package
com
.
luhu
.
computility
.
module
.
compute
.
controller
.
admin
.
resourcespu
.
vo
;
import
io.swagger.v3.oas.annotations.media.Schema
;
import
lombok.Data
;
import
org.springframework.web.multipart.MultipartFile
;
import
javax.validation.constraints.NotNull
;
/**
* 算力资源 SPU 批量导入 Request VO
*/
@Schema
(
description
=
"管理后台 - 算力资源 SPU 批量导入"
)
@Data
public
class
ResourceSpuImportReqVO
{
@Schema
(
description
=
"Excel 文件"
,
requiredMode
=
Schema
.
RequiredMode
.
REQUIRED
)
@NotNull
(
message
=
"Excel 文件不能为空"
)
private
MultipartFile
file
;
}
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/controller/admin/resourcespu/vo/ResourceSpuImportRespVO.java
0 → 100644
View file @
97fc1928
package
com
.
luhu
.
computility
.
module
.
compute
.
controller
.
admin
.
resourcespu
.
vo
;
import
io.swagger.v3.oas.annotations.media.Schema
;
import
lombok.AllArgsConstructor
;
import
lombok.Builder
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
import
java.util.ArrayList
;
import
java.util.List
;
/**
* 算力资源 SPU 批量导入 Response VO
*/
@Schema
(
description
=
"管理后台 - 算力资源 SPU 批量导入"
)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public
class
ResourceSpuImportRespVO
{
@Schema
(
description
=
"成功创建的商品名称列表"
,
requiredMode
=
Schema
.
RequiredMode
.
REQUIRED
)
@Builder
.
Default
private
List
<
String
>
createNames
=
new
ArrayList
<>();
@Schema
(
description
=
"导入失败行集合"
,
requiredMode
=
Schema
.
RequiredMode
.
REQUIRED
)
@Builder
.
Default
private
List
<
ResourceSpuImportRowVO
>
failureRows
=
new
ArrayList
<>();
}
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/controller/admin/resourcespu/vo/ResourceSpuImportRowVO.java
0 → 100644
View file @
97fc1928
package
com
.
luhu
.
computility
.
module
.
compute
.
controller
.
admin
.
resourcespu
.
vo
;
import
io.swagger.v3.oas.annotations.media.Schema
;
import
lombok.AllArgsConstructor
;
import
lombok.Builder
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
/**
* 算力资源 SPU 导入 - 失败行 VO
*/
@Schema
(
description
=
"管理后台 - 算力资源 SPU 导入 - 失败行"
)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public
class
ResourceSpuImportRowVO
{
@Schema
(
description
=
"Excel 行号(从 2 开始,1 是表头)"
,
requiredMode
=
Schema
.
RequiredMode
.
REQUIRED
,
example
=
"5"
)
private
Integer
rowIndex
;
@Schema
(
description
=
"商品名称(填的那一行)"
,
example
=
"A100 算力集群"
)
private
String
name
;
@Schema
(
description
=
"失败原因"
,
requiredMode
=
Schema
.
RequiredMode
.
REQUIRED
,
example
=
"商品名称已存在"
)
private
String
message
;
}
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/controller/admin/resourcespu/vo/ResourceSpuSaveReqVO.java
View file @
97fc1928
...
@@ -29,8 +29,7 @@ public class ResourceSpuSaveReqVO {
...
@@ -29,8 +29,7 @@ public class ResourceSpuSaveReqVO {
@Schema
(
description
=
"算力来源"
,
example
=
"own"
)
@Schema
(
description
=
"算力来源"
,
example
=
"own"
)
private
String
source
;
private
String
source
;
@Schema
(
description
=
"商品封面图"
,
requiredMode
=
Schema
.
RequiredMode
.
REQUIRED
,
example
=
"https://www.iocoder.cn"
)
@Schema
(
description
=
"商品封面图"
,
example
=
"https://www.iocoder.cn"
)
@NotEmpty
(
message
=
"商品封面图不能为空"
)
private
String
picUrl
;
private
String
picUrl
;
@Schema
(
description
=
"商品轮播图地址,以逗号分隔,最多15张"
)
@Schema
(
description
=
"商品轮播图地址,以逗号分隔,最多15张"
)
...
...
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/service/resourcespu/ResourceSpuImportService.java
0 → 100644
View file @
97fc1928
package
com
.
luhu
.
computility
.
module
.
compute
.
service
.
resourcespu
;
import
com.luhu.computility.module.compute.controller.admin.resourcespu.vo.ResourceSpuImportRespVO
;
import
org.springframework.web.multipart.MultipartFile
;
import
java.io.IOException
;
/**
* 算力资源 SPU 批量导入 Service 接口
*/
public
interface
ResourceSpuImportService
{
/**
* 解析上传的 Excel 并批量导入算力资源 SPU
*
* @param file Excel 文件
* @return 导入结果(成功列表 + 失败行集合)
*/
ResourceSpuImportRespVO
importSpuList
(
MultipartFile
file
)
throws
IOException
;
}
computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/service/resourcespu/ResourceSpuImportServiceImpl.java
0 → 100644
View file @
97fc1928
package
com
.
luhu
.
computility
.
module
.
compute
.
service
.
resourcespu
;
import
cn.hutool.core.collection.CollUtil
;
import
cn.hutool.core.util.StrUtil
;
import
com.alibaba.excel.EasyExcel
;
import
com.alibaba.excel.context.AnalysisContext
;
import
com.alibaba.excel.event.AnalysisEventListener
;
import
com.baomidou.mybatisplus.core.toolkit.Wrappers
;
import
com.luhu.computility.framework.common.biz.system.dict.dto.DictDataRespDTO
;
import
com.luhu.computility.framework.mybatis.core.query.LambdaQueryWrapperX
;
import
com.luhu.computility.module.compute.controller.admin.resourcespu.vo.ResourceSpuImportRespVO
;
import
com.luhu.computility.module.compute.controller.admin.resourcespu.vo.ResourceSpuImportRowVO
;
import
com.luhu.computility.module.compute.controller.admin.resourcespu.vo.ResourceSpuSaveReqVO
;
import
com.luhu.computility.module.compute.dal.dataobject.resourcecategory.ResourceCategoryDO
;
import
com.luhu.computility.module.compute.dal.dataobject.resourceconfig.ResourceConfigDO
;
import
com.luhu.computility.module.compute.dal.dataobject.resourcespu.ResourceSpuDO
;
import
com.luhu.computility.module.compute.dal.mysql.resourceconfig.ResourceConfigMapper
;
import
com.luhu.computility.module.compute.dal.mysql.resourcespu.ResourceSpuMapper
;
import
com.luhu.computility.module.compute.enums.ResourceEnableStatus
;
import
com.luhu.computility.module.compute.enums.ResourceSpuStatus
;
import
com.luhu.computility.module.compute.service.resourcecategory.ResourceCategoryService
;
import
com.luhu.computility.module.system.api.dict.DictDataApi
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Service
;
import
org.springframework.validation.annotation.Validated
;
import
org.springframework.web.multipart.MultipartFile
;
import
javax.annotation.Resource
;
import
java.io.IOException
;
import
java.util.*
;
import
java.util.stream.Collectors
;
import
static
com
.
luhu
.
computility
.
framework
.
common
.
exception
.
util
.
ServiceExceptionUtil
.
exception
;
import
static
com
.
luhu
.
computility
.
module
.
compute
.
enums
.
ErrorCodeConstants
.*;
/**
* 算力资源 SPU 批量导入 Service 实现类
*
* <p>Excel 模板列结构:
* <ul>
* <li>固定列:商品名称 / 商品分类 / 封面图 / 商品销量 / 状态</li>
* <li>动态列:根据字典 compute_resource_config_category 的 label 动态生成(如 CPU / GPU / RAM / Storage)</li>
* </ul>
*
* <p>表头处理:第一行表头里"非固定列"的列名如果匹配字典 label,则视为规格列,对应的字典 value 作为 configCategory 写入。</p>
*
* <p>同名商品:按 name 全局判重,本批次内和数据库都重复都会报错(不支持 update)。</p>
*
* <p>规格值不存在:按 (configCategory, configOption) 自动新建。</p>
*/
@Service
@Validated
@Slf4j
public
class
ResourceSpuImportServiceImpl
implements
ResourceSpuImportService
{
@Resource
private
ResourceSpuService
resourceSpuService
;
@Resource
private
ResourceSpuMapper
resourceSpuMapper
;
@Resource
private
ResourceCategoryService
resourceCategoryService
;
@Resource
private
ResourceConfigMapper
resourceConfigMapper
;
@Resource
private
DictDataApi
dictDataApi
;
/** 固定列:商品名称 */
private
static
final
String
COL_NAME
=
"商品名称"
;
/** 固定列:商品分类 */
private
static
final
String
COL_CATEGORY
=
"商品分类"
;
/** 固定列:算力来源 */
private
static
final
String
COL_SOURCE
=
"算力来源"
;
/** 固定列:商品简介 */
private
static
final
String
COL_INTRO
=
"商品简介"
;
/** 固定列:封面图 */
private
static
final
String
COL_PIC_URL
=
"封面图"
;
/** 固定列:商品销量 */
private
static
final
String
COL_SALES
=
"商品销量"
;
/** 固定列:状态 */
private
static
final
String
COL_STATUS
=
"状态"
;
/** 字典类型:算力资源配置类别 */
private
static
final
String
DICT_TYPE_CONFIG_CATEGORY
=
"compute_resource_config_category"
;
@Override
public
ResourceSpuImportRespVO
importSpuList
(
MultipartFile
file
)
throws
IOException
{
// 1. 解析 Excel(自定义 listener 分别拿表头 + 数据行)
// EasyExcel 默认 headRowNumber=1,第一行通过 invokeHeadMap 接收;invoke 接收数据行
java
.
util
.
concurrent
.
atomic
.
AtomicReference
<
List
<
String
>>
headerRef
=
new
java
.
util
.
concurrent
.
atomic
.
AtomicReference
<>();
List
<
List
<
String
>>
dataRows
=
new
ArrayList
<>();
EasyExcel
.
read
(
file
.
getInputStream
(),
new
AnalysisEventListener
<
Map
<
Integer
,
String
>>()
{
@Override
public
void
invokeHeadMap
(
Map
<
Integer
,
String
>
headMap
,
AnalysisContext
context
)
{
if
(
headerRef
.
get
()
!=
null
)
{
return
;
}
int
maxIdx
=
headMap
.
keySet
().
stream
().
mapToInt
(
Integer:
:
intValue
).
max
().
orElse
(-
1
);
List
<
String
>
header
=
new
ArrayList
<>(
Collections
.
nCopies
(
maxIdx
+
1
,
null
));
headMap
.
forEach
((
k
,
v
)
->
{
if
(
k
<
header
.
size
())
{
header
.
set
(
k
,
v
);
}
});
headerRef
.
set
(
header
);
}
@Override
public
void
invoke
(
Map
<
Integer
,
String
>
row
,
AnalysisContext
context
)
{
// 跳过多余空行
if
(
row
.
values
().
stream
().
allMatch
(
StrUtil:
:
isBlank
))
{
return
;
}
// 转成 List<String>,按 key 顺序(key 即列索引)
List
<
String
>
list
=
new
ArrayList
<>(
Collections
.
nCopies
(
collectMaxIndex
(
row
)
+
1
,
null
));
row
.
forEach
((
k
,
v
)
->
{
if
(
k
<
list
.
size
())
{
list
.
set
(
k
,
v
);
}
});
dataRows
.
add
(
list
);
}
@Override
public
void
doAfterAllAnalysed
(
AnalysisContext
context
)
{
// no-op
}
private
int
collectMaxIndex
(
Map
<
Integer
,
String
>
row
)
{
return
row
.
keySet
().
stream
().
mapToInt
(
Integer:
:
intValue
).
max
().
orElse
(-
1
);
}
}).
sheet
().
doRead
();
List
<
String
>
header
=
headerRef
.
get
();
if
(
header
==
null
||
CollUtil
.
isEmpty
(
dataRows
))
{
throw
exception
(
RESOURCE_SPU_IMPORT_LIST_IS_EMPTY
);
}
// 3. 拿字典(按 DICT_TYPE_CONFIG_CATEGORY 拿 value-label 映射)
List
<
DictDataRespDTO
>
dictList
=
dictDataApi
.
getDictDataList
(
DICT_TYPE_CONFIG_CATEGORY
);
if
(
CollUtil
.
isEmpty
(
dictList
))
{
throw
exception
(
RESOURCE_SPU_IMPORT_HEADER_INVALID
);
}
Map
<
String
,
String
>
labelToValue
=
dictList
.
stream
()
.
collect
(
Collectors
.
toMap
(
DictDataRespDTO:
:
getLabel
,
DictDataRespDTO:
:
getValue
,
(
a
,
b
)
->
a
));
// 4. 解析列索引
Map
<
String
,
Integer
>
fixedColIdx
=
new
HashMap
<>();
Map
<
Integer
,
String
>
specColIdxToCategory
=
new
HashMap
<>();
for
(
int
i
=
0
;
i
<
header
.
size
();
i
++)
{
String
col
=
header
.
get
(
i
);
if
(
StrUtil
.
isBlank
(
col
))
{
continue
;
}
col
=
col
.
trim
();
switch
(
col
)
{
case
COL_NAME:
case
COL_CATEGORY:
case
COL_SOURCE:
case
COL_INTRO:
case
COL_PIC_URL:
case
COL_SALES:
case
COL_STATUS:
fixedColIdx
.
put
(
col
,
i
);
break
;
default
:
if
(
labelToValue
.
containsKey
(
col
))
{
specColIdxToCategory
.
put
(
i
,
labelToValue
.
get
(
col
));
}
break
;
}
}
// 5. 校验固定列必须存在
if
(!
fixedColIdx
.
containsKey
(
COL_NAME
)
||
!
fixedColIdx
.
containsKey
(
COL_CATEGORY
)
||
!
fixedColIdx
.
containsKey
(
COL_PIC_URL
))
{
throw
exception
(
RESOURCE_SPU_IMPORT_HEADER_INVALID
);
}
// 6. 遍历每行
ResourceSpuImportRespVO
respVO
=
ResourceSpuImportRespVO
.
builder
()
.
createNames
(
new
ArrayList
<>())
.
failureRows
(
new
ArrayList
<>())
.
build
();
// 用于检测本批次重复
Set
<
String
>
nameInBatch
=
new
HashSet
<>();
// 缓存已查询的 categoryId(避免重复查 DB)
Map
<
Long
,
Boolean
>
categoryValidCache
=
new
HashMap
<>();
// 缓存分类名 -> categoryId(用于文字分类输入)
Map
<
String
,
Long
>
categoryNameCache
=
new
HashMap
<>();
// 缓存已查询/已创建的 specConfig(key=configCategory:value, value=configId)
Map
<
String
,
Long
>
specCache
=
new
HashMap
<>();
for
(
int
rowIdx
=
0
;
rowIdx
<
dataRows
.
size
();
rowIdx
++)
{
List
<
String
>
row
=
dataRows
.
get
(
rowIdx
);
int
excelRow
=
rowIdx
+
2
;
// Excel 行号:表头占 1 行,数据行从第 2 行开始
String
rowName
=
getCellValue
(
row
,
fixedColIdx
.
get
(
COL_NAME
));
try
{
processRow
(
row
,
fixedColIdx
,
specColIdxToCategory
,
nameInBatch
,
categoryValidCache
,
categoryNameCache
,
specCache
,
respVO
);
}
catch
(
Exception
e
)
{
log
.
warn
(
"[importSpuList][第 {} 行导入失败:{}]"
,
excelRow
,
e
.
getMessage
());
respVO
.
getFailureRows
().
add
(
ResourceSpuImportRowVO
.
builder
()
.
rowIndex
(
excelRow
)
.
name
(
rowName
)
.
message
(
e
.
getMessage
())
.
build
());
}
}
return
respVO
;
}
/**
* 处理单行:校验 → 查重 → 查/建规格 → 创建 SPU + 中间表
*/
private
void
processRow
(
List
<
String
>
row
,
Map
<
String
,
Integer
>
fixedColIdx
,
Map
<
Integer
,
String
>
specColIdxToCategory
,
Set
<
String
>
nameInBatch
,
Map
<
Long
,
Boolean
>
categoryValidCache
,
Map
<
String
,
Long
>
categoryNameCache
,
Map
<
String
,
Long
>
specCache
,
ResourceSpuImportRespVO
respVO
)
{
// 1. 解析固定字段
String
name
=
getCellValue
(
row
,
fixedColIdx
.
get
(
COL_NAME
));
String
categoryIdStr
=
getCellValue
(
row
,
fixedColIdx
.
get
(
COL_CATEGORY
));
String
source
=
getCellValue
(
row
,
fixedColIdx
.
get
(
COL_SOURCE
));
String
intro
=
getCellValue
(
row
,
fixedColIdx
.
get
(
COL_INTRO
));
String
picUrl
=
getCellValue
(
row
,
fixedColIdx
.
get
(
COL_PIC_URL
));
String
salesStr
=
getCellValue
(
row
,
fixedColIdx
.
get
(
COL_SALES
));
String
statusStr
=
getCellValue
(
row
,
fixedColIdx
.
get
(
COL_STATUS
));
if
(
StrUtil
.
isBlank
(
name
))
{
throw
new
IllegalArgumentException
(
"商品名称不能为空"
);
}
if
(
StrUtil
.
isBlank
(
categoryIdStr
))
{
throw
new
IllegalArgumentException
(
"商品分类不能为空"
);
}
Long
categoryId
=
resolveCategoryId
(
categoryIdStr
.
trim
(),
categoryValidCache
,
categoryNameCache
);
if
(
categoryId
==
null
)
{
throw
new
IllegalArgumentException
(
"商品分类不存在:"
+
categoryIdStr
);
}
// 2. 校验分类
Boolean
categoryValid
=
categoryValidCache
.
computeIfAbsent
(
categoryId
,
this
::
validateCategoryId
);
if
(
Boolean
.
FALSE
.
equals
(
categoryValid
))
{
throw
exception
(
RESOURCE_SPU_IMPORT_CATEGORY_INVALID
);
}
// 3. 校验 name:本批次去重
if
(!
nameInBatch
.
add
(
name
))
{
throw
new
IllegalArgumentException
(
RESOURCE_SPU_IMPORT_NAME_DUPLICATE
.
getMsg
()
+
"(本批次重复)"
);
}
// 4. 校验 name:DB 去重
Long
nameCount
=
resourceSpuMapper
.
selectCount
(
new
LambdaQueryWrapperX
<
ResourceSpuDO
>()
.
eq
(
ResourceSpuDO:
:
getName
,
name
));
if
(
nameCount
!=
null
&&
nameCount
>
0
)
{
throw
exception
(
RESOURCE_SPU_IMPORT_NAME_DUPLICATE
);
}
// 5. 解析可选字段
Integer
sales
=
parseIntOrDefault
(
salesStr
,
0
);
Integer
status
=
parseStatusOrDefault
(
statusStr
,
ResourceSpuStatus
.
ONLINE
.
getValue
());
// 6. 处理规格列:每列对应 (configCategory, configOption),查/建 spec
List
<
Long
>
specConfigIds
=
new
ArrayList
<>();
for
(
Map
.
Entry
<
Integer
,
String
>
entry
:
specColIdxToCategory
.
entrySet
())
{
Integer
colIdx
=
entry
.
getKey
();
String
configCategory
=
entry
.
getValue
();
String
configOption
=
getCellValue
(
row
,
colIdx
);
if
(
StrUtil
.
isBlank
(
configOption
))
{
continue
;
}
// 单元格可能填多个值,用"、"分隔
String
[]
options
=
configOption
.
split
(
"[、,,]"
);
for
(
String
opt
:
options
)
{
if
(
StrUtil
.
isBlank
(
opt
))
{
continue
;
}
String
optTrim
=
opt
.
trim
();
Long
configId
=
getOrCreateSpecId
(
configCategory
,
optTrim
,
specCache
);
if
(
configId
!=
null
)
{
specConfigIds
.
add
(
configId
);
}
}
}
if
(
CollUtil
.
isEmpty
(
specConfigIds
))
{
throw
new
IllegalArgumentException
(
"至少需要一个规格"
);
}
// 7. 创建 SPU(复用 Service 内部事务)
ResourceSpuSaveReqVO
saveReqVO
=
new
ResourceSpuSaveReqVO
();
saveReqVO
.
setName
(
name
);
saveReqVO
.
setCategoryId
(
categoryId
);
saveReqVO
.
setSource
(
source
);
saveReqVO
.
setIntro
(
intro
);
saveReqVO
.
setPicUrl
(
picUrl
);
saveReqVO
.
setSales
(
sales
);
saveReqVO
.
setStatus
(
status
);
saveReqVO
.
setSpecConfigIds
(
specConfigIds
);
resourceSpuService
.
createResourceSpu
(
saveReqVO
);
respVO
.
getCreateNames
().
add
(
name
);
}
/**
* 校验分类是否存在
*/
private
Boolean
validateCategoryId
(
Long
categoryId
)
{
ResourceCategoryDO
category
=
resourceCategoryService
.
getResourceCategory
(
categoryId
);
return
category
!=
null
;
}
/**
* 解析商品分类:支持数字 ID 或分类名称
* @return 分类 ID,解析不到返回 null
*/
private
Long
resolveCategoryId
(
String
input
,
Map
<
Long
,
Boolean
>
categoryValidCache
,
Map
<
String
,
Long
>
categoryNameCache
)
{
// 1. 优先按数字 ID 解析
try
{
return
Long
.
parseLong
(
input
);
}
catch
(
NumberFormatException
ignore
)
{
// 不是数字,走名称匹配
}
// 2. 按名称查询(用缓存避免每次都查全表)
Long
cached
=
categoryNameCache
.
get
(
input
);
if
(
cached
!=
null
)
{
return
cached
;
}
List
<
ResourceCategoryDO
>
all
=
resourceCategoryService
.
getAllCategory
();
for
(
ResourceCategoryDO
c
:
all
)
{
if
(
input
.
equals
(
c
.
getName
()))
{
categoryNameCache
.
put
(
input
,
c
.
getId
());
categoryValidCache
.
put
(
c
.
getId
(),
Boolean
.
TRUE
);
return
c
.
getId
();
}
}
return
null
;
}
/**
* 查/建规格项:先按 (configCategory, configOption) 查 spec 表,不存在则新建
*/
private
Long
getOrCreateSpecId
(
String
configCategory
,
String
configOption
,
Map
<
String
,
Long
>
specCache
)
{
String
cacheKey
=
configCategory
+
"::"
+
configOption
;
Long
cached
=
specCache
.
get
(
cacheKey
);
if
(
cached
!=
null
)
{
return
cached
;
}
List
<
ResourceConfigDO
>
existing
=
resourceConfigMapper
.
selectList
(
Wrappers
.<
ResourceConfigDO
>
lambdaQuery
()
.
eq
(
ResourceConfigDO:
:
getConfigCategory
,
configCategory
)
.
eq
(
ResourceConfigDO:
:
getConfigOption
,
configOption
));
if
(
CollUtil
.
isNotEmpty
(
existing
))
{
Long
id
=
existing
.
get
(
0
).
getId
();
specCache
.
put
(
cacheKey
,
id
);
return
id
;
}
// 不存在则新建
ResourceConfigDO
newCfg
=
new
ResourceConfigDO
();
newCfg
.
setConfigCategory
(
configCategory
);
newCfg
.
setConfigOption
(
configOption
);
newCfg
.
setStatus
(
ResourceEnableStatus
.
ENABLE
.
getValue
());
newCfg
.
setSort
(
0
);
resourceConfigMapper
.
insert
(
newCfg
);
specCache
.
put
(
cacheKey
,
newCfg
.
getId
());
return
newCfg
.
getId
();
}
private
String
getCellValue
(
List
<
String
>
row
,
Integer
idx
)
{
if
(
idx
==
null
||
idx
>=
row
.
size
())
{
return
null
;
}
return
row
.
get
(
idx
);
}
private
Integer
parseIntOrDefault
(
String
s
,
Integer
defaultVal
)
{
if
(
StrUtil
.
isBlank
(
s
))
{
return
defaultVal
;
}
try
{
return
Integer
.
parseInt
(
s
.
trim
());
}
catch
(
NumberFormatException
e
)
{
return
defaultVal
;
}
}
private
Integer
parseStatusOrDefault
(
String
s
,
Integer
defaultVal
)
{
if
(
StrUtil
.
isBlank
(
s
))
{
return
defaultVal
;
}
String
trimmed
=
s
.
trim
();
// 优先按 label 匹配
for
(
ResourceSpuStatus
st
:
ResourceSpuStatus
.
values
())
{
if
(
st
.
getLabel
().
equals
(
trimmed
))
{
return
st
.
getValue
();
}
}
// 退化为数字
return
parseIntOrDefault
(
trimmed
,
defaultVal
);
}
}
sql/migration/2026_08_10_compute_resource_spu_location_optional.sql
0 → 100644
View file @
97fc1928
-- ============================================================
-- 算力资源商品改造 · 阶段 8 补丁
-- 目标:将服务器所在地 location 改为非必填字段
-- 适用库:new_computility
-- 前置:DTO/前端/导入校验已放开(location 允许为 null)
-- 业务上 location 是商家发货时填写,初始导入时无值
-- 配套:
-- - ResourceSpuSaveReqVO.location 去掉必填校验
-- - admin/src/views/compute/resourcespu/ResourceSpuForm.vue
-- 保留 location 字段由用户手动选择(单条保存仍可填)
-- - ResourceSpuImportServiceImpl.processRow 不强制设置 location
-- ============================================================
USE
`new_computility`
;
ALTER
TABLE
`compute_resource_spu`
MODIFY
COLUMN
`location`
VARCHAR
(
255
)
DEFAULT
NULL
COMMENT
'服务器所在地'
;
-- 验证:
-- SHOW COLUMNS FROM `compute_resource_spu` LIKE 'location';
sql/migration/2026_08_10_compute_resource_spu_pic_url_optional.sql
0 → 100644
View file @
97fc1928
-- ============================================================
-- 算力资源商品改造 · 阶段 8 补丁
-- 目标:将商品封面图 pic_url 改为非必填
-- 适用库:new_computility
-- 前置:DTO/前端/导入校验已放开(picUrl 允许为 null)
-- 配套:
-- - ResourceSpuSaveReqVO.picUrl 去掉 @NotEmpty
-- - admin/src/views/compute/resourcespu/ResourceSpuForm.vue
-- 去掉 picUrl 的 required 校验
-- - ResourceSpuImportServiceImpl.processRow 去掉封面图非空校验
-- ============================================================
USE
`new_computility`
;
ALTER
TABLE
`compute_resource_spu`
MODIFY
COLUMN
`pic_url`
VARCHAR
(
255
)
DEFAULT
NULL
COMMENT
'商品封面图'
;
-- 验证:
-- SHOW COLUMNS FROM `compute_resource_spu` LIKE 'pic_url';
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment