Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
H
hanni-external-api
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
杨向龙
hanni-external-api
Commits
a04c5989
Commit
a04c5989
authored
Mar 23, 2026
by
yangxianglong
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
自采联营门店系统开发
parent
6f6dffbd
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
523 additions
and
89 deletions
+523
-89
AsyncConfig.java
src/main/java/cn/nhsoft/hanni/config/AsyncConfig.java
+31
-0
ItemController.java
.../java/cn/nhsoft/hanni/item/controller/ItemController.java
+12
-10
LemonToHanniItemConverter.java
...hsoft/hanni/item/converter/LemonToHanniItemConverter.java
+41
-2
ItemSyncLog.java
src/main/java/cn/nhsoft/hanni/item/model/ItemSyncLog.java
+15
-1
ItemSyncDetailRepository.java
...hsoft/hanni/item/repository/ItemSyncDetailRepository.java
+2
-1
ItemSyncDetailSpecification.java
...ft/hanni/item/repository/ItemSyncDetailSpecification.java
+50
-0
ItemSyncProgressService.java
...cn/nhsoft/hanni/item/service/ItemSyncProgressService.java
+43
-0
ItemSyncService.java
...in/java/cn/nhsoft/hanni/item/service/ItemSyncService.java
+9
-23
ItemSyncServiceImpl.java
...n/nhsoft/hanni/item/service/impl/ItemSyncServiceImpl.java
+137
-51
LemengTokenController.java
...nhsoft/hanni/lemeng/controller/LemengTokenController.java
+12
-0
LemengTokenStatusDTO.java
...java/cn/nhsoft/hanni/lemeng/dto/LemengTokenStatusDTO.java
+37
-0
LemengOAuthTokenRepository.java
...t/hanni/lemeng/repository/LemengOAuthTokenRepository.java
+2
-0
LemengTokenPersistService.java
...hsoft/hanni/lemeng/service/LemengTokenPersistService.java
+2
-1
LemengTokenStatusService.java
...nhsoft/hanni/lemeng/service/LemengTokenStatusService.java
+92
-0
JwtExpUtil.java
src/main/java/cn/nhsoft/hanni/lemeng/util/JwtExpUtil.java
+37
-0
mysql-item-sync.sql
src/main/resources/db/mysql-item-sync.sql
+1
-0
No files found.
src/main/java/cn/nhsoft/hanni/config/AsyncConfig.java
0 → 100644
View file @
a04c5989
package
cn
.
nhsoft
.
hanni
.
config
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.scheduling.annotation.EnableAsync
;
import
org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
;
import
java.util.concurrent.Executor
;
/**
* 商品同步等异步任务线程池
*/
@Configuration
@EnableAsync
public
class
AsyncConfig
{
public
static
final
String
ITEM_SYNC_EXECUTOR
=
"itemSyncExecutor"
;
@Bean
(
name
=
ITEM_SYNC_EXECUTOR
)
public
Executor
itemSyncExecutor
()
{
ThreadPoolTaskExecutor
ex
=
new
ThreadPoolTaskExecutor
();
ex
.
setCorePoolSize
(
2
);
ex
.
setMaxPoolSize
(
8
);
ex
.
setQueueCapacity
(
50
);
ex
.
setThreadNamePrefix
(
"item-sync-"
);
ex
.
setWaitForTasksToCompleteOnShutdown
(
true
);
ex
.
setAwaitTerminationSeconds
(
120
);
ex
.
initialize
();
return
ex
;
}
}
src/main/java/cn/nhsoft/hanni/item/controller/ItemController.java
View file @
a04c5989
...
@@ -35,16 +35,15 @@ public class ItemController {
...
@@ -35,16 +35,15 @@ public class ItemController {
@Resource
@Resource
private
ItemSyncLogRepository
itemSyncLogRepository
;
private
ItemSyncLogRepository
itemSyncLogRepository
;
@Operation
(
summary
=
"商品档案同步"
,
description
=
"
从乐檬获取商品档案,加工后传入汉尼系统;字段变更时调用汉尼更新接口
"
)
@Operation
(
summary
=
"商品档案同步"
,
description
=
"
异步执行:立即返回 syncLogId,可在同步记录中轮询进度(RUNNING + progressMessage)
"
)
@GetMapping
(
"/sync"
)
@GetMapping
(
"/sync"
)
public
Result
<
Void
>
syncItems
()
{
public
Result
<
Long
>
syncItems
()
{
itemSyncService
.
syncItems
();
return
Result
.
success
(
itemSyncService
.
syncItemsStart
());
return
Result
.
success
();
}
}
@Operation
(
summary
=
"按乐檬参数手动同步"
,
description
=
"与定时任务相同:按条件多页拉取直至不足一页,再同步到汉尼"
)
@Operation
(
summary
=
"按乐檬参数手动同步"
,
description
=
"与定时任务相同:按条件多页拉取直至不足一页,再同步到汉尼"
)
@GetMapping
(
"/sync/manual"
)
@GetMapping
(
"/sync/manual"
)
public
Result
<
Void
>
syncItemsManual
(
public
Result
<
Long
>
syncItemsManual
(
@Parameter
(
description
=
"页码"
,
required
=
true
)
@RequestParam
(
defaultValue
=
"1"
)
Integer
pageNo
,
@Parameter
(
description
=
"页码"
,
required
=
true
)
@RequestParam
(
defaultValue
=
"1"
)
Integer
pageNo
,
@Parameter
(
description
=
"分页大小(最大1000)"
,
required
=
true
)
@RequestParam
(
defaultValue
=
"100"
)
Integer
pageSize
,
@Parameter
(
description
=
"分页大小(最大1000)"
,
required
=
true
)
@RequestParam
(
defaultValue
=
"100"
)
Integer
pageSize
,
@Parameter
(
description
=
"最后修改时间(yyyy-MM-dd HH:mm:ss)"
)
@RequestParam
(
required
=
false
)
String
lastDownloadTime
,
@Parameter
(
description
=
"最后修改时间(yyyy-MM-dd HH:mm:ss)"
)
@RequestParam
(
required
=
false
)
String
lastDownloadTime
,
...
@@ -54,10 +53,10 @@ public class ItemController {
...
@@ -54,10 +53,10 @@ public class ItemController {
@Parameter
(
description
=
"是否过滤淘汰商品,默认 true(仅同步非淘汰)"
)
@RequestParam
(
required
=
false
)
Boolean
filterWeedOut
,
@Parameter
(
description
=
"是否过滤淘汰商品,默认 true(仅同步非淘汰)"
)
@RequestParam
(
required
=
false
)
Boolean
filterWeedOut
,
@Parameter
(
description
=
"商品编码,逗号分隔"
)
@RequestParam
(
required
=
false
)
String
itemNums
,
@Parameter
(
description
=
"商品编码,逗号分隔"
)
@RequestParam
(
required
=
false
)
String
itemNums
,
@Parameter
(
description
=
"商品代码,逗号分隔"
)
@RequestParam
(
required
=
false
)
String
itemCodes
)
{
@Parameter
(
description
=
"商品代码,逗号分隔"
)
@RequestParam
(
required
=
false
)
String
itemCodes
)
{
itemSyncService
.
syncItemsWithParams
(
LemonItemFindRequestMapper
.
fromQuery
(
Long
logId
=
itemSyncService
.
syncItemsWithParamsStart
(
LemonItemFindRequestMapper
.
fromQuery
(
pageNo
,
pageSize
,
lastDownloadTime
,
itemType
,
itemCategoryCode
,
pageNo
,
pageSize
,
lastDownloadTime
,
itemType
,
itemCategoryCode
,
filterSleep
,
filterWeedOut
,
itemNums
,
itemCodes
));
filterSleep
,
filterWeedOut
,
itemNums
,
itemCodes
));
return
Result
.
success
();
return
Result
.
success
(
logId
);
}
}
@Operation
(
summary
=
"从乐檬获取商品档案"
,
description
=
"调用乐檬 nhsoft.amazon.basic.item.find,仅查询当前页,不同步"
)
@Operation
(
summary
=
"从乐檬获取商品档案"
,
description
=
"调用乐檬 nhsoft.amazon.basic.item.find,仅查询当前页,不同步"
)
...
@@ -95,12 +94,15 @@ public class ItemController {
...
@@ -95,12 +94,15 @@ public class ItemController {
return
Result
.
success
(
dto
);
return
Result
.
success
(
dto
);
}
}
@Operation
(
summary
=
"商品同步明细"
,
description
=
"按同步日志 ID 分页查询本次任务每条商品的 ADD/UPDATE/SKIP 记录"
)
@Operation
(
summary
=
"商品同步明细"
,
description
=
"按同步日志 ID 分页查询本次任务每条商品的 ADD/UPDATE/SKIP 记录
;支持条码模糊、动作、成功状态筛选
"
)
@GetMapping
(
"/sync-logs/{logId}/details"
)
@GetMapping
(
"/sync-logs/{logId}/details"
)
public
Result
<
ItemSyncDetailPageDTO
>
getSyncLogDetails
(
public
Result
<
ItemSyncDetailPageDTO
>
getSyncLogDetails
(
@Parameter
(
description
=
"同步日志 id"
)
@PathVariable
(
"logId"
)
Long
logId
,
@Parameter
(
description
=
"同步日志 id"
)
@PathVariable
(
"logId"
)
Long
logId
,
@Parameter
(
description
=
"页码"
)
@RequestParam
(
defaultValue
=
"0"
)
Integer
page
,
@Parameter
(
description
=
"页码"
)
@RequestParam
(
defaultValue
=
"0"
)
Integer
page
,
@Parameter
(
description
=
"每页条数"
)
@RequestParam
(
defaultValue
=
"50"
)
Integer
size
)
{
@Parameter
(
description
=
"每页条数"
)
@RequestParam
(
defaultValue
=
"50"
)
Integer
size
,
return
Result
.
success
(
itemSyncService
.
getSyncDetails
(
logId
,
page
,
size
));
@Parameter
(
description
=
"条码(模糊匹配,忽略大小写)"
)
@RequestParam
(
required
=
false
)
String
barcode
,
@Parameter
(
description
=
"动作:ADD/UPDATE/SKIP_TIME/SKIP_NO_BARCODE"
)
@RequestParam
(
required
=
false
)
String
action
,
@Parameter
(
description
=
"成功:true 仅成功,false 非成功(含未记录)"
)
@RequestParam
(
required
=
false
)
Boolean
success
)
{
return
Result
.
success
(
itemSyncService
.
getSyncDetails
(
logId
,
page
,
size
,
barcode
,
action
,
success
));
}
}
}
}
src/main/java/cn/nhsoft/hanni/item/converter/LemonToHanniItemConverter.java
View file @
a04c5989
...
@@ -112,7 +112,9 @@ public class LemonToHanniItemConverter {
...
@@ -112,7 +112,9 @@ public class LemonToHanniItemConverter {
* 从乐檬商品对象解析「最后修改时间」字符串(多字段兼容)
* 从乐檬商品对象解析「最后修改时间」字符串(多字段兼容)
*/
*/
private
String
extractLemonLastModified
(
Map
<
String
,
Object
>
m
)
{
private
String
extractLemonLastModified
(
Map
<
String
,
Object
>
m
)
{
// 乐檬 item.find 常见为 item_last_edit_time,须优先匹配
String
[]
keys
=
{
String
[]
keys
=
{
"item_last_edit_time"
,
"item_modify_time"
,
"item_update_time"
,
"last_modify_time"
,
"last_update_time"
,
"item_modify_time"
,
"item_update_time"
,
"last_modify_time"
,
"last_update_time"
,
"modify_time"
,
"update_time"
,
"gmt_modified"
,
"item_last_modify_time"
,
"modify_time"
,
"update_time"
,
"gmt_modified"
,
"item_last_modify_time"
,
"last_download_time"
,
"item_last_update_time"
"last_download_time"
,
"item_last_update_time"
...
@@ -126,6 +128,43 @@ public class LemonToHanniItemConverter {
...
@@ -126,6 +128,43 @@ public class LemonToHanniItemConverter {
return
null
;
return
null
;
}
}
/**
* 等级:直段字段或 pos_item_grades[0] 子对象
*/
@SuppressWarnings
(
"unchecked"
)
private
String
extractGrade
(
Map
<
String
,
Object
>
m
)
{
String
s
=
getStr
(
m
,
"grade"
,
"item_grade"
,
"item_grade_name"
);
if
(
StringUtils
.
hasText
(
s
))
{
return
s
;
}
Object
pg
=
m
.
get
(
"pos_item_grades"
);
if
(
pg
instanceof
List
&&
!((
List
<?>)
pg
).
isEmpty
())
{
Object
first
=
((
List
<?>)
pg
).
get
(
0
);
if
(
first
instanceof
Map
)
{
Map
<
String
,
Object
>
gm
=
(
Map
<
String
,
Object
>)
first
;
Object
v
=
gm
.
get
(
"grade_name"
);
if
(
v
==
null
)
{
v
=
gm
.
get
(
"name"
);
}
if
(
v
!=
null
&&
StringUtils
.
hasText
(
v
.
toString
()))
{
return
v
.
toString
().
trim
();
}
}
}
return
""
;
}
/**
* 型号:乐檬多为 item_model;无则可用商品代码 item_code 兜底
*/
private
String
extractModel
(
Map
<
String
,
Object
>
m
)
{
String
s
=
getStr
(
m
,
"model"
,
"item_model"
,
"item_model_no"
,
"product_model"
);
if
(
StringUtils
.
hasText
(
s
))
{
return
s
;
}
return
getStr
(
m
,
"item_code"
);
}
private
HanniItemDTO
convertFromMap
(
Map
<
String
,
Object
>
m
)
{
private
HanniItemDTO
convertFromMap
(
Map
<
String
,
Object
>
m
)
{
HanniItemDTO
dto
=
new
HanniItemDTO
();
HanniItemDTO
dto
=
new
HanniItemDTO
();
dto
.
setBarcode
(
getStr
(
m
,
"item_barcode"
,
"barcode"
));
dto
.
setBarcode
(
getStr
(
m
,
"item_barcode"
,
"barcode"
));
...
@@ -137,8 +176,8 @@ public class LemonToHanniItemConverter {
...
@@ -137,8 +176,8 @@ public class LemonToHanniItemConverter {
dto
.
setParea
(
getStr
(
m
,
"item_place"
,
"parea"
,
"place"
));
dto
.
setParea
(
getStr
(
m
,
"item_place"
,
"parea"
,
"place"
));
dto
.
setIsweight
(
getBoolStr
(
m
,
"item_weight_flag"
,
"isweight"
));
dto
.
setIsweight
(
getBoolStr
(
m
,
"item_weight_flag"
,
"isweight"
));
dto
.
setPackratio
(
getStr
(
m
,
"item_purchase_rate"
,
"packratio"
,
"purchase_rate"
));
dto
.
setPackratio
(
getStr
(
m
,
"item_purchase_rate"
,
"packratio"
,
"purchase_rate"
));
dto
.
setGrade
(
getStr
(
m
,
"grade"
));
dto
.
setGrade
(
extractGrade
(
m
));
dto
.
setModel
(
getStr
(
m
,
"model"
));
dto
.
setModel
(
extractModel
(
m
));
dto
.
setGid
(
getLong
(
m
,
"item_num"
,
"gid"
,
"id"
));
dto
.
setGid
(
getLong
(
m
,
"item_num"
,
"gid"
,
"id"
));
return
dto
;
return
dto
;
}
}
...
...
src/main/java/cn/nhsoft/hanni/item/model/ItemSyncLog.java
View file @
a04c5989
...
@@ -35,12 +35,18 @@ public class ItemSyncLog {
...
@@ -35,12 +35,18 @@ public class ItemSyncLog {
private
Integer
hanniCount
;
private
Integer
hanniCount
;
/**
/**
* 同步状态:SUCCESS/FAIL
* 同步状态:SUCCESS/FAIL
/SKIP/RUNNING
*/
*/
@Column
(
name
=
"status"
,
length
=
16
)
@Column
(
name
=
"status"
,
length
=
16
)
private
String
status
;
private
String
status
;
/**
/**
* 进行中时的进度描述(独立事务提交,供前端轮询)
*/
@Column
(
name
=
"progress_message"
,
length
=
512
)
private
String
progressMessage
;
/**
* 汉尼接口响应内容
* 汉尼接口响应内容
*/
*/
@Column
(
name
=
"hanni_response"
,
columnDefinition
=
"TEXT"
)
@Column
(
name
=
"hanni_response"
,
columnDefinition
=
"TEXT"
)
...
@@ -120,6 +126,14 @@ public class ItemSyncLog {
...
@@ -120,6 +126,14 @@ public class ItemSyncLog {
this
.
status
=
status
;
this
.
status
=
status
;
}
}
public
String
getProgressMessage
()
{
return
progressMessage
;
}
public
void
setProgressMessage
(
String
progressMessage
)
{
this
.
progressMessage
=
progressMessage
;
}
public
String
getHanniResponse
()
{
public
String
getHanniResponse
()
{
return
hanniResponse
;
return
hanniResponse
;
}
}
...
...
src/main/java/cn/nhsoft/hanni/item/repository/ItemSyncDetailRepository.java
View file @
a04c5989
...
@@ -4,8 +4,9 @@ import cn.nhsoft.hanni.item.model.ItemSyncDetail;
...
@@ -4,8 +4,9 @@ import cn.nhsoft.hanni.item.model.ItemSyncDetail;
import
org.springframework.data.domain.Page
;
import
org.springframework.data.domain.Page
;
import
org.springframework.data.domain.Pageable
;
import
org.springframework.data.domain.Pageable
;
import
org.springframework.data.jpa.repository.JpaRepository
;
import
org.springframework.data.jpa.repository.JpaRepository
;
import
org.springframework.data.jpa.repository.JpaSpecificationExecutor
;
public
interface
ItemSyncDetailRepository
extends
JpaRepository
<
ItemSyncDetail
,
Long
>
{
public
interface
ItemSyncDetailRepository
extends
JpaRepository
<
ItemSyncDetail
,
Long
>
,
JpaSpecificationExecutor
<
ItemSyncDetail
>
{
Page
<
ItemSyncDetail
>
findBySyncLogIdOrderByIdAsc
(
Long
syncLogId
,
Pageable
pageable
);
Page
<
ItemSyncDetail
>
findBySyncLogIdOrderByIdAsc
(
Long
syncLogId
,
Pageable
pageable
);
}
}
src/main/java/cn/nhsoft/hanni/item/repository/ItemSyncDetailSpecification.java
0 → 100644
View file @
a04c5989
package
cn
.
nhsoft
.
hanni
.
item
.
repository
;
import
cn.nhsoft.hanni.item.model.ItemSyncDetail
;
import
org.springframework.data.jpa.domain.Specification
;
import
org.springframework.util.StringUtils
;
import
javax.persistence.criteria.Predicate
;
import
java.util.ArrayList
;
import
java.util.List
;
/**
* 同步明细分页查询条件(条码模糊、动作精确、成功状态)
*/
public
final
class
ItemSyncDetailSpecification
{
private
ItemSyncDetailSpecification
()
{
}
/**
* @param successFilter null 不筛选;true 仅成功;false 非成功(含 false 与 null)
*/
public
static
Specification
<
ItemSyncDetail
>
forSyncLog
(
Long
syncLogId
,
String
barcode
,
String
action
,
Boolean
successFilter
)
{
return
(
root
,
query
,
cb
)
->
{
List
<
Predicate
>
predicates
=
new
ArrayList
<>();
predicates
.
add
(
cb
.
equal
(
root
.
get
(
"syncLogId"
),
syncLogId
));
if
(
StringUtils
.
hasText
(
barcode
))
{
String
pattern
=
"%"
+
barcode
.
trim
().
toLowerCase
()
+
"%"
;
predicates
.
add
(
cb
.
like
(
cb
.
lower
(
root
.
get
(
"barcode"
)),
pattern
));
}
if
(
StringUtils
.
hasText
(
action
))
{
predicates
.
add
(
cb
.
equal
(
root
.
get
(
"action"
),
action
.
trim
()));
}
if
(
successFilter
!=
null
)
{
if
(
Boolean
.
TRUE
.
equals
(
successFilter
))
{
predicates
.
add
(
cb
.
isTrue
(
root
.
get
(
"success"
)));
}
else
{
predicates
.
add
(
cb
.
or
(
cb
.
isFalse
(
root
.
get
(
"success"
)),
cb
.
isNull
(
root
.
get
(
"success"
))
));
}
}
return
cb
.
and
(
predicates
.
toArray
(
new
Predicate
[
0
]));
};
}
}
src/main/java/cn/nhsoft/hanni/item/service/ItemSyncProgressService.java
0 → 100644
View file @
a04c5989
package
cn
.
nhsoft
.
hanni
.
item
.
service
;
import
cn.nhsoft.hanni.item.model.ItemSyncLog
;
import
cn.nhsoft.hanni.item.repository.ItemSyncLogRepository
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Propagation
;
import
org.springframework.transaction.annotation.Transactional
;
import
javax.annotation.Resource
;
/**
* 同步任务进度独立提交(REQUIRES_NEW),便于列表页轮询看到实时进度。
*/
@Service
public
class
ItemSyncProgressService
{
@Resource
private
ItemSyncLogRepository
itemSyncLogRepository
;
@Transactional
(
propagation
=
Propagation
.
REQUIRES_NEW
)
public
void
updateProgress
(
Long
logId
,
String
message
,
Integer
lemengCount
)
{
ItemSyncLog
log
=
itemSyncLogRepository
.
findById
(
logId
).
orElse
(
null
);
if
(
log
==
null
)
{
return
;
}
log
.
setProgressMessage
(
message
);
if
(
lemengCount
!=
null
)
{
log
.
setLemengCount
(
lemengCount
);
}
itemSyncLogRepository
.
saveAndFlush
(
log
);
}
@Transactional
(
propagation
=
Propagation
.
REQUIRES_NEW
)
public
void
markFailed
(
Long
logId
,
String
errorMsg
)
{
ItemSyncLog
log
=
itemSyncLogRepository
.
findById
(
logId
).
orElse
(
null
);
if
(
log
==
null
)
{
return
;
}
log
.
setStatus
(
"FAIL"
);
log
.
setErrorMsg
(
errorMsg
);
itemSyncLogRepository
.
saveAndFlush
(
log
);
}
}
src/main/java/cn/nhsoft/hanni/item/service/ItemSyncService.java
View file @
a04c5989
...
@@ -5,40 +5,26 @@ import cn.nhsoft.hanni.item.dto.LemonItemFindRequest;
...
@@ -5,40 +5,26 @@ import cn.nhsoft.hanni.item.dto.LemonItemFindRequest;
/**
/**
* 商品档案同步服务接口
* 商品档案同步服务接口
* <p>
* 负责从乐檬获取商品档案,加工后传入汉尼系统;
* 当养馋记系统字段变更时,调用汉尼系统更新接口
* </p>
*
* @author hanni-external-api
*/
*/
public
interface
ItemSyncService
{
public
interface
ItemSyncService
{
/**
/**
* 同步商品档案
* 定时/内部:异步启动同步(不返回日志 ID)
* <p>从乐檬获取商品档案,加工后传入汉尼系统</p>
*/
*/
void
syncItems
();
void
syncItems
();
/**
/**
* 从乐檬获取商品档案
* 触发同步并返回日志 ID(立即返回,任务在后台执行)
* <p>仅调用乐檬接口,不做加工和同步;请求方式 GET,参数 page_no、page_size 必填</p>
*
* @param request 查询参数(对应 nhsoft.amazon.basic.item.find 接口文档)
* @return 乐檬商品档案原始响应
*/
*/
String
getItemsFromLemon
(
LemonItemFindRequest
request
);
Long
syncItemsStart
(
);
/**
/**
* 按乐檬查询参数手动同步到汉尼
* 按乐檬参数异步启动同步并返回日志 ID
* <p>使用指定查询参数从乐檬获取商品,经转换、过滤、变更检测后同步到汉尼</p>
*
* @param request 乐檬商品档案查询参数(与 nhsoft.amazon.basic.item.find 一致)
*/
*/
void
syncItemsWithParams
(
LemonItemFindRequest
request
);
Long
syncItemsWithParamsStart
(
LemonItemFindRequest
request
);
/**
String
getItemsFromLemon
(
LemonItemFindRequest
request
);
* 分页查询某次同步任务的商品明细
*/
ItemSyncDetailPageDTO
getSyncDetails
(
Long
syncLogId
,
int
page
,
int
size
,
ItemSyncDetailPageDTO
getSyncDetails
(
Long
syncLogId
,
int
page
,
int
size
);
String
barcode
,
String
action
,
Boolean
success
);
}
}
src/main/java/cn/nhsoft/hanni/item/service/impl/ItemSyncServiceImpl.java
View file @
a04c5989
...
@@ -2,6 +2,7 @@ package cn.nhsoft.hanni.item.service.impl;
...
@@ -2,6 +2,7 @@ package cn.nhsoft.hanni.item.service.impl;
import
cn.nhsoft.hanni.client.HanniApiClient
;
import
cn.nhsoft.hanni.client.HanniApiClient
;
import
cn.nhsoft.hanni.client.LemonApiClient
;
import
cn.nhsoft.hanni.client.LemonApiClient
;
import
cn.nhsoft.hanni.config.AsyncConfig
;
import
cn.nhsoft.hanni.item.converter.LemonToHanniItemConverter
;
import
cn.nhsoft.hanni.item.converter.LemonToHanniItemConverter
;
import
cn.nhsoft.hanni.item.dto.HanniItemDTO
;
import
cn.nhsoft.hanni.item.dto.HanniItemDTO
;
import
cn.nhsoft.hanni.item.dto.ItemSyncDetailPageDTO
;
import
cn.nhsoft.hanni.item.dto.ItemSyncDetailPageDTO
;
...
@@ -11,16 +12,20 @@ import cn.nhsoft.hanni.item.model.ItemSyncDetail;
...
@@ -11,16 +12,20 @@ import cn.nhsoft.hanni.item.model.ItemSyncDetail;
import
cn.nhsoft.hanni.item.model.ItemSyncLog
;
import
cn.nhsoft.hanni.item.model.ItemSyncLog
;
import
cn.nhsoft.hanni.item.model.ItemSyncSnapshot
;
import
cn.nhsoft.hanni.item.model.ItemSyncSnapshot
;
import
cn.nhsoft.hanni.item.repository.ItemSyncDetailRepository
;
import
cn.nhsoft.hanni.item.repository.ItemSyncDetailRepository
;
import
cn.nhsoft.hanni.item.repository.ItemSyncDetailSpecification
;
import
cn.nhsoft.hanni.item.repository.ItemSyncLogRepository
;
import
cn.nhsoft.hanni.item.repository.ItemSyncLogRepository
;
import
cn.nhsoft.hanni.item.repository.ItemSyncSnapshotRepository
;
import
cn.nhsoft.hanni.item.repository.ItemSyncSnapshotRepository
;
import
cn.nhsoft.hanni.item.service.ItemSyncProgressService
;
import
cn.nhsoft.hanni.item.service.ItemSyncService
;
import
cn.nhsoft.hanni.item.service.ItemSyncService
;
import
cn.nhsoft.hanni.item.util.LemonItemResponseParser
;
import
cn.nhsoft.hanni.item.util.LemonItemResponseParser
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
lombok.extern.slf4j.Slf4j
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.BeanUtils
;
import
org.springframework.beans.BeanUtils
;
import
org.springframework.beans.factory.annotation.Qualifier
;
import
org.springframework.data.domain.Page
;
import
org.springframework.data.domain.Page
;
import
org.springframework.data.domain.PageRequest
;
import
org.springframework.data.domain.PageRequest
;
import
org.springframework.data.domain.Sort
;
import
org.springframework.data.domain.Sort
;
import
org.springframework.data.jpa.domain.Specification
;
import
org.springframework.stereotype.Service
;
import
org.springframework.stereotype.Service
;
import
org.springframework.util.StringUtils
;
import
org.springframework.util.StringUtils
;
import
org.springframework.transaction.PlatformTransactionManager
;
import
org.springframework.transaction.PlatformTransactionManager
;
...
@@ -35,9 +40,10 @@ import java.util.HashMap;
...
@@ -35,9 +40,10 @@ import java.util.HashMap;
import
java.util.List
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Map
;
import
java.util.Optional
;
import
java.util.Optional
;
import
java.util.concurrent.Executor
;
/**
/**
* 商品档案同步服务实现类
* 商品档案同步服务实现类
(异步执行 + 进度独立事务提交)
*/
*/
@Slf4j
@Slf4j
@Service
@Service
...
@@ -67,6 +73,13 @@ public class ItemSyncServiceImpl implements ItemSyncService {
...
@@ -67,6 +73,13 @@ public class ItemSyncServiceImpl implements ItemSyncService {
@Resource
@Resource
private
PlatformTransactionManager
platformTransactionManager
;
private
PlatformTransactionManager
platformTransactionManager
;
@Resource
private
ItemSyncProgressService
itemSyncProgressService
;
@Resource
@Qualifier
(
AsyncConfig
.
ITEM_SYNC_EXECUTOR
)
private
Executor
itemSyncExecutor
;
private
TransactionTemplate
transactionTemplate
;
private
TransactionTemplate
transactionTemplate
;
@PostConstruct
@PostConstruct
...
@@ -74,13 +87,22 @@ public class ItemSyncServiceImpl implements ItemSyncService {
...
@@ -74,13 +87,22 @@ public class ItemSyncServiceImpl implements ItemSyncService {
this
.
transactionTemplate
=
new
TransactionTemplate
(
platformTransactionManager
);
this
.
transactionTemplate
=
new
TransactionTemplate
(
platformTransactionManager
);
}
}
@FunctionalInterface
@Override
private
interface
SyncLogWork
{
public
void
syncItems
()
{
void
execute
(
ItemSyncLog
syncLog
)
throws
Exception
;
LemonItemFindRequest
findRequest
=
buildScheduledFindRequest
();
runSyncAsync
(
findRequest
,
"SCHEDULED"
);
}
}
/**
* 前端「立即同步」等 HTTP 触发:与定时任务共用增量参数,但类型记为 MANUAL(用户主动触发)。
* 仅 {@link #syncItems()} 定时任务记为 SCHEDULED。
*/
@Override
@Override
public
void
syncItems
()
{
public
Long
syncItemsStart
()
{
return
runSyncAsync
(
buildScheduledFindRequest
(),
"MANUAL"
);
}
private
LemonItemFindRequest
buildScheduledFindRequest
()
{
LemonItemFindRequest
findRequest
=
new
LemonItemFindRequest
();
LemonItemFindRequest
findRequest
=
new
LemonItemFindRequest
();
findRequest
.
setPageNo
(
1
);
findRequest
.
setPageNo
(
1
);
findRequest
.
setPageSize
(
100
);
findRequest
.
setPageSize
(
100
);
...
@@ -93,11 +115,11 @@ public class ItemSyncServiceImpl implements ItemSyncService {
...
@@ -93,11 +115,11 @@ public class ItemSyncServiceImpl implements ItemSyncService {
}
else
{
}
else
{
log
.
info
(
"首次全量同步,无 lastDownloadTime"
);
log
.
info
(
"首次全量同步,无 lastDownloadTime"
);
}
}
doSyncIncremental
(
findRequest
,
"SCHEDULED"
)
;
return
findRequest
;
}
}
@Override
@Override
public
void
syncItemsWithParams
(
LemonItemFindRequest
request
)
{
public
Long
syncItemsWithParamsStart
(
LemonItemFindRequest
request
)
{
if
(
request
==
null
)
{
if
(
request
==
null
)
{
request
=
new
LemonItemFindRequest
();
request
=
new
LemonItemFindRequest
();
}
}
...
@@ -107,25 +129,63 @@ public class ItemSyncServiceImpl implements ItemSyncService {
...
@@ -107,25 +129,63 @@ public class ItemSyncServiceImpl implements ItemSyncService {
if
(
request
.
getPageSize
()
==
null
)
{
if
(
request
.
getPageSize
()
==
null
)
{
request
.
setPageSize
(
100
);
request
.
setPageSize
(
100
);
}
}
doSyncIncremental
(
request
,
"MANUAL"
);
LemonItemFindRequest
copy
=
new
LemonItemFindRequest
();
}
BeanUtils
.
copyProperties
(
request
,
copy
);
return
runSyncAsync
(
copy
,
"MANUAL"
);
@Override
public
ItemSyncDetailPageDTO
getSyncDetails
(
Long
syncLogId
,
int
page
,
int
size
)
{
PageRequest
pageable
=
PageRequest
.
of
(
page
,
size
,
Sort
.
by
(
Sort
.
Direction
.
ASC
,
"id"
));
Page
<
ItemSyncDetail
>
p
=
itemSyncDetailRepository
.
findBySyncLogIdOrderByIdAsc
(
syncLogId
,
pageable
);
return
new
ItemSyncDetailPageDTO
(
p
.
getContent
(),
p
.
getTotalElements
());
}
}
private
void
doSyncIncremental
(
LemonItemFindRequest
request
,
String
syncType
)
{
/**
// 未显式设置时与定时任务一致:向乐檬传 filter_sleep/filter_weed_out(仅依赖接口查询参数)
* 异步执行:先落库 RUNNING 并 flush,再在线程池中跑同步;立即返回日志 ID。
*/
private
Long
runSyncAsync
(
LemonItemFindRequest
request
,
String
syncType
)
{
if
(
request
.
getFilterSleep
()
==
null
)
{
if
(
request
.
getFilterSleep
()
==
null
)
{
request
.
setFilterSleep
(
Boolean
.
TRUE
);
request
.
setFilterSleep
(
Boolean
.
TRUE
);
}
}
if
(
request
.
getFilterWeedOut
()
==
null
)
{
if
(
request
.
getFilterWeedOut
()
==
null
)
{
request
.
setFilterWeedOut
(
Boolean
.
TRUE
);
request
.
setFilterWeedOut
(
Boolean
.
TRUE
);
}
}
runWithSyncLog
(
syncType
,
syncLog
->
{
ItemSyncLog
syncLog
=
new
ItemSyncLog
();
syncLog
.
setSyncType
(
syncType
);
syncLog
.
setStartTime
(
new
Date
());
syncLog
.
setStatus
(
"RUNNING"
);
syncLog
.
setProgressMessage
(
"任务已启动,准备拉取乐檬..."
);
syncLog
=
itemSyncLogRepository
.
saveAndFlush
(
syncLog
);
Long
logId
=
syncLog
.
getId
();
LemonItemFindRequest
asyncReq
=
new
LemonItemFindRequest
();
BeanUtils
.
copyProperties
(
request
,
asyncReq
);
itemSyncExecutor
.
execute
(()
->
executeSyncJob
(
logId
,
asyncReq
));
return
logId
;
}
private
void
executeSyncJob
(
Long
logId
,
LemonItemFindRequest
request
)
{
long
startMs
=
System
.
currentTimeMillis
();
try
{
doSyncIncrementalBody
(
logId
,
request
);
}
catch
(
Exception
e
)
{
log
.
error
(
"商品同步失败 logId={}"
,
logId
,
e
);
itemSyncProgressService
.
markFailed
(
logId
,
e
.
getMessage
()
!=
null
?
e
.
getMessage
()
:
e
.
toString
());
}
finally
{
ItemSyncLog
sl
=
itemSyncLogRepository
.
findById
(
logId
).
orElse
(
null
);
if
(
sl
!=
null
)
{
sl
.
setEndTime
(
new
Date
());
sl
.
setDurationMs
(
System
.
currentTimeMillis
()
-
startMs
);
if
(
"RUNNING"
.
equals
(
sl
.
getStatus
()))
{
sl
.
setStatus
(
"FAIL"
);
if
(
sl
.
getErrorMsg
()
==
null
||
sl
.
getErrorMsg
().
isEmpty
())
{
sl
.
setErrorMsg
(
"同步未完成"
);
}
sl
.
setProgressMessage
(
null
);
}
itemSyncLogRepository
.
saveAndFlush
(
sl
);
}
}
}
private
void
doSyncIncrementalBody
(
Long
logId
,
LemonItemFindRequest
request
)
{
itemSyncProgressService
.
updateProgress
(
logId
,
"正在拉取乐檬商品(分页)..."
,
null
);
List
<
LemonItemRow
>
allRows
=
new
ArrayList
<>();
List
<
LemonItemRow
>
allRows
=
new
ArrayList
<>();
int
pageNo
=
request
.
getPageNo
()
!=
null
?
request
.
getPageNo
()
:
1
;
int
pageNo
=
request
.
getPageNo
()
!=
null
?
request
.
getPageNo
()
:
1
;
int
pageSize
=
request
.
getPageSize
()
!=
null
?
request
.
getPageSize
()
:
100
;
int
pageSize
=
request
.
getPageSize
()
!=
null
?
request
.
getPageSize
()
:
100
;
...
@@ -143,18 +203,26 @@ public class ItemSyncServiceImpl implements ItemSyncService {
...
@@ -143,18 +203,26 @@ public class ItemSyncServiceImpl implements ItemSyncService {
break
;
break
;
}
}
allRows
.
addAll
(
pageRows
);
allRows
.
addAll
(
pageRows
);
itemSyncProgressService
.
updateProgress
(
logId
,
String
.
format
(
"已拉取第 %d 页,本页约 %d 条,累计转换 %d 条"
,
pageNo
,
count
,
allRows
.
size
()),
lemengTotal
);
if
(
count
<
pageSize
)
{
if
(
count
<
pageSize
)
{
break
;
break
;
}
}
pageNo
++;
pageNo
++;
}
}
syncLog
.
setLemengCount
(
lemengTotal
);
itemSyncProgressService
.
updateProgress
(
logId
,
String
.
format
(
"拉取完成,乐檬返回约 %d 条,有效转换 %d 条;开始处理并推送汉尼"
,
lemengTotal
,
allRows
.
size
()),
lemengTotal
);
final
int
lemengTotalFinal
=
lemengTotal
;
transactionTemplate
.
execute
(
status
->
{
transactionTemplate
.
execute
(
status
->
{
processAndSyncToHanniFromList
(
allRows
,
syncLog
);
ItemSyncLog
managed
=
itemSyncLogRepository
.
findById
(
logId
)
.
orElseThrow
(()
->
new
IllegalStateException
(
"item_sync_log 不存在: "
+
logId
));
processAndSyncToHanniFromList
(
allRows
,
managed
,
lemengTotalFinal
);
return
null
;
return
null
;
});
});
});
}
}
private
static
LemonItemFindRequest
copyPageRequest
(
LemonItemFindRequest
src
,
int
pageNo
,
int
pageSize
)
{
private
static
LemonItemFindRequest
copyPageRequest
(
LemonItemFindRequest
src
,
int
pageNo
,
int
pageSize
)
{
...
@@ -165,38 +233,24 @@ public class ItemSyncServiceImpl implements ItemSyncService {
...
@@ -165,38 +233,24 @@ public class ItemSyncServiceImpl implements ItemSyncService {
return
p
;
return
p
;
}
}
private
void
runWithSyncLog
(
String
syncType
,
SyncLogWork
work
)
{
@Override
ItemSyncLog
syncLog
=
new
ItemSyncLog
();
public
ItemSyncDetailPageDTO
getSyncDetails
(
Long
syncLogId
,
int
page
,
int
size
,
syncLog
.
setSyncType
(
syncType
);
String
barcode
,
String
action
,
Boolean
success
)
{
syncLog
.
setStartTime
(
new
Date
());
PageRequest
pageable
=
PageRequest
.
of
(
page
,
size
,
Sort
.
by
(
Sort
.
Direction
.
ASC
,
"id"
));
syncLog
.
setStatus
(
"RUNNING"
);
Specification
<
ItemSyncDetail
>
spec
=
ItemSyncDetailSpecification
.
forSyncLog
(
syncLogId
,
barcode
,
action
,
success
);
syncLog
=
itemSyncLogRepository
.
saveAndFlush
(
syncLog
);
Page
<
ItemSyncDetail
>
p
=
itemSyncDetailRepository
.
findAll
(
spec
,
pageable
);
long
startMs
=
System
.
currentTimeMillis
();
return
new
ItemSyncDetailPageDTO
(
p
.
getContent
(),
p
.
getTotalElements
());
try
{
work
.
execute
(
syncLog
);
}
catch
(
Exception
e
)
{
log
.
error
(
"商品同步失败"
,
e
);
syncLog
.
setStatus
(
"FAIL"
);
syncLog
.
setErrorMsg
(
e
.
getMessage
());
}
finally
{
syncLog
.
setEndTime
(
new
Date
());
syncLog
.
setDurationMs
(
System
.
currentTimeMillis
()
-
startMs
);
if
(
"RUNNING"
.
equals
(
syncLog
.
getStatus
()))
{
syncLog
.
setStatus
(
"FAIL"
);
if
(
syncLog
.
getErrorMsg
()
==
null
||
syncLog
.
getErrorMsg
().
isEmpty
())
{
syncLog
.
setErrorMsg
(
"同步未完成"
);
}
}
itemSyncLogRepository
.
save
(
syncLog
);
}
}
}
private
void
processAndSyncToHanniFromList
(
List
<
LemonItemRow
>
rows
,
ItemSyncLog
syncLog
)
{
private
void
processAndSyncToHanniFromList
(
List
<
LemonItemRow
>
rows
,
ItemSyncLog
syncLog
,
int
lemengTotalCount
)
{
Long
logId
=
syncLog
.
getId
();
Long
logId
=
syncLog
.
getId
();
syncLog
.
setLemengCount
(
lemengTotalCount
);
if
(
rows
==
null
||
rows
.
isEmpty
())
{
if
(
rows
==
null
||
rows
.
isEmpty
())
{
syncLog
.
setHanniCount
(
0
);
syncLog
.
setHanniCount
(
0
);
syncLog
.
setStatus
(
"SKIP"
);
syncLog
.
setStatus
(
"SKIP"
);
syncLog
.
setErrorMsg
(
"转换后无有效商品数据"
);
syncLog
.
setErrorMsg
(
"转换后无有效商品数据"
);
itemSyncLogRepository
.
save
(
syncLog
);
return
;
return
;
}
}
...
@@ -224,6 +278,8 @@ public class ItemSyncServiceImpl implements ItemSyncService {
...
@@ -224,6 +278,8 @@ public class ItemSyncServiceImpl implements ItemSyncService {
d
.
setGid
(
item
.
getGid
());
d
.
setGid
(
item
.
getGid
());
d
.
setName
(
item
.
getName
());
d
.
setName
(
item
.
getName
());
d
.
setAction
(
ItemSyncDetail
.
ACTION_SKIP_NO_BARCODE
);
d
.
setAction
(
ItemSyncDetail
.
ACTION_SKIP_NO_BARCODE
);
d
.
setLemonLastModified
(
row
.
getLemonLastModified
());
d
.
setStoredLemonModified
(
null
);
d
.
setSuccess
(
true
);
d
.
setSuccess
(
true
);
d
.
setRemark
(
"缺少条码,不同步"
);
d
.
setRemark
(
"缺少条码,不同步"
);
details
.
add
(
d
);
details
.
add
(
d
);
...
@@ -264,6 +320,7 @@ public class ItemSyncServiceImpl implements ItemSyncService {
...
@@ -264,6 +320,7 @@ public class ItemSyncServiceImpl implements ItemSyncService {
updateDetailByBc
.
put
(
barcode
,
d
);
updateDetailByBc
.
put
(
barcode
,
d
);
}
else
{
}
else
{
d
.
setAction
(
ItemSyncDetail
.
ACTION_ADD
);
d
.
setAction
(
ItemSyncDetail
.
ACTION_ADD
);
d
.
setStoredLemonModified
(
null
);
toAdd
.
add
(
item
);
toAdd
.
add
(
item
);
addRows
.
add
(
row
);
addRows
.
add
(
row
);
addDetailByBc
.
put
(
barcode
,
d
);
addDetailByBc
.
put
(
barcode
,
d
);
...
@@ -271,9 +328,15 @@ public class ItemSyncServiceImpl implements ItemSyncService {
...
@@ -271,9 +328,15 @@ public class ItemSyncServiceImpl implements ItemSyncService {
details
.
add
(
d
);
details
.
add
(
d
);
}
}
itemSyncProgressService
.
updateProgress
(
logId
,
String
.
format
(
"分类完成:待推送汉尼新增 %d 条、更新 %d 条"
,
toAdd
.
size
(),
toUpdate
.
size
()),
null
);
StringBuilder
responseBuilder
=
new
StringBuilder
();
StringBuilder
responseBuilder
=
new
StringBuilder
();
if
(!
toAdd
.
isEmpty
())
{
if
(!
toAdd
.
isEmpty
())
{
itemSyncProgressService
.
updateProgress
(
logId
,
String
.
format
(
"正在调用汉尼新增接口…(%d 条)"
,
toAdd
.
size
()),
null
);
log
.
info
(
"新增 {} 条商品到汉尼"
,
toAdd
.
size
());
log
.
info
(
"新增 {} 条商品到汉尼"
,
toAdd
.
size
());
String
r
=
hanniApiClient
.
saveItems
(
toAdd
);
String
r
=
hanniApiClient
.
saveItems
(
toAdd
);
if
(
r
!=
null
)
{
if
(
r
!=
null
)
{
...
@@ -283,14 +346,19 @@ public class ItemSyncServiceImpl implements ItemSyncService {
...
@@ -283,14 +346,19 @@ public class ItemSyncServiceImpl implements ItemSyncService {
for
(
LemonItemRow
rr
:
addRows
)
{
for
(
LemonItemRow
rr
:
addRows
)
{
ItemSyncDetail
d
=
addDetailByBc
.
get
(
rr
.
getItem
().
getBarcode
());
ItemSyncDetail
d
=
addDetailByBc
.
get
(
rr
.
getItem
().
getBarcode
());
if
(
d
!=
null
)
{
if
(
d
!=
null
)
{
d
.
setLemonLastModified
(
rr
.
getLemonLastModified
());
d
.
setStoredLemonModified
(
null
);
d
.
setSuccess
(
true
);
d
.
setSuccess
(
true
);
d
.
setRemark
(
shortR
);
d
.
setRemark
(
shortR
);
}
}
upsertSnapshot
(
rr
);
upsertSnapshot
(
rr
);
}
}
itemSyncProgressService
.
updateProgress
(
logId
,
"汉尼新增接口已完成,正在处理快照"
,
null
);
}
}
if
(!
toUpdate
.
isEmpty
())
{
if
(!
toUpdate
.
isEmpty
())
{
itemSyncProgressService
.
updateProgress
(
logId
,
String
.
format
(
"正在调用汉尼更新接口…(%d 条)"
,
toUpdate
.
size
()),
null
);
log
.
info
(
"更新 {} 条变更商品到汉尼"
,
toUpdate
.
size
());
log
.
info
(
"更新 {} 条变更商品到汉尼"
,
toUpdate
.
size
());
String
r
=
hanniApiClient
.
updateItems
(
toUpdate
);
String
r
=
hanniApiClient
.
updateItems
(
toUpdate
);
if
(
r
!=
null
)
{
if
(
r
!=
null
)
{
...
@@ -298,19 +366,32 @@ public class ItemSyncServiceImpl implements ItemSyncService {
...
@@ -298,19 +366,32 @@ public class ItemSyncServiceImpl implements ItemSyncService {
}
}
String
shortR
=
truncateStr
(
r
,
500
);
String
shortR
=
truncateStr
(
r
,
500
);
for
(
LemonItemRow
rr
:
updateRows
)
{
for
(
LemonItemRow
rr
:
updateRows
)
{
ItemSyncDetail
d
=
updateDetailByBc
.
get
(
rr
.
getItem
().
getBarcode
());
String
bc
=
rr
.
getItem
().
getBarcode
();
ItemSyncDetail
d
=
updateDetailByBc
.
get
(
bc
);
if
(
d
!=
null
)
{
if
(
d
!=
null
)
{
d
.
setLemonLastModified
(
rr
.
getLemonLastModified
());
itemSyncSnapshotRepository
.
findByBarcode
(
bc
).
ifPresent
(
snap
->
d
.
setStoredLemonModified
(
snap
.
getLemonLastModified
()));
d
.
setSuccess
(
true
);
d
.
setSuccess
(
true
);
d
.
setRemark
(
shortR
);
d
.
setRemark
(
shortR
);
}
}
upsertSnapshot
(
rr
);
upsertSnapshot
(
rr
);
}
}
itemSyncProgressService
.
updateProgress
(
logId
,
"汉尼更新接口已完成,正在处理快照"
,
null
);
}
}
saveDetailsInChunks
(
details
);
if
(
toAdd
.
isEmpty
()
&&
toUpdate
.
isEmpty
())
{
itemSyncProgressService
.
updateProgress
(
logId
,
"无需推送汉尼(均为跳过),正在写入明细"
,
null
);
}
itemSyncProgressService
.
updateProgress
(
logId
,
String
.
format
(
"正在写入同步明细(共 %d 条)…"
,
details
.
size
()),
null
);
saveDetailsInChunks
(
details
,
logId
);
syncLog
.
setStatus
(
"SUCCESS"
);
syncLog
.
setStatus
(
"SUCCESS"
);
syncLog
.
setHanniResponse
(
truncateStr
(
responseBuilder
.
toString
(),
2000
));
syncLog
.
setHanniResponse
(
truncateStr
(
responseBuilder
.
toString
(),
2000
));
syncLog
.
setProgressMessage
(
"已完成"
);
itemSyncLogRepository
.
save
(
syncLog
);
}
}
private
static
String
normalizeMod
(
String
s
)
{
private
static
String
normalizeMod
(
String
s
)
{
...
@@ -337,11 +418,16 @@ public class ItemSyncServiceImpl implements ItemSyncService {
...
@@ -337,11 +418,16 @@ public class ItemSyncServiceImpl implements ItemSyncService {
itemSyncSnapshotRepository
.
save
(
s
);
itemSyncSnapshotRepository
.
save
(
s
);
}
}
private
void
saveDetailsInChunks
(
List
<
ItemSyncDetail
>
details
)
{
private
void
saveDetailsInChunks
(
List
<
ItemSyncDetail
>
details
,
Long
logId
)
{
int
batch
=
200
;
int
batch
=
200
;
for
(
int
i
=
0
;
i
<
details
.
size
();
i
+=
batch
)
{
int
total
=
details
.
size
();
int
end
=
Math
.
min
(
i
+
batch
,
details
.
size
());
for
(
int
i
=
0
;
i
<
total
;
i
+=
batch
)
{
int
end
=
Math
.
min
(
i
+
batch
,
total
);
itemSyncDetailRepository
.
saveAll
(
details
.
subList
(
i
,
end
));
itemSyncDetailRepository
.
saveAll
(
details
.
subList
(
i
,
end
));
if
(
logId
!=
null
&&
total
>
batch
)
{
itemSyncProgressService
.
updateProgress
(
logId
,
String
.
format
(
"正在写入同步明细 %d/%d …"
,
end
,
total
),
null
);
}
}
}
}
}
...
...
src/main/java/cn/nhsoft/hanni/lemeng/controller/LemengTokenController.java
View file @
a04c5989
...
@@ -3,14 +3,17 @@ package cn.nhsoft.hanni.lemeng.controller;
...
@@ -3,14 +3,17 @@ package cn.nhsoft.hanni.lemeng.controller;
import
cn.nhsoft.hanni.common.Result
;
import
cn.nhsoft.hanni.common.Result
;
import
cn.nhsoft.hanni.lemeng.client.LemengTokenRestClient
;
import
cn.nhsoft.hanni.lemeng.client.LemengTokenRestClient
;
import
cn.nhsoft.hanni.lemeng.config.LemengProperties
;
import
cn.nhsoft.hanni.lemeng.config.LemengProperties
;
import
cn.nhsoft.hanni.lemeng.dto.LemengTokenStatusDTO
;
import
cn.nhsoft.hanni.lemeng.dto.TokenRefreshResult
;
import
cn.nhsoft.hanni.lemeng.dto.TokenRefreshResult
;
import
cn.nhsoft.hanni.lemeng.service.LemengTokenPersistService
;
import
cn.nhsoft.hanni.lemeng.service.LemengTokenPersistService
;
import
cn.nhsoft.hanni.lemeng.service.LemengTokenStatusService
;
import
io.swagger.v3.oas.annotations.Operation
;
import
io.swagger.v3.oas.annotations.Operation
;
import
io.swagger.v3.oas.annotations.Parameter
;
import
io.swagger.v3.oas.annotations.Parameter
;
import
io.swagger.v3.oas.annotations.tags.Tag
;
import
io.swagger.v3.oas.annotations.tags.Tag
;
import
lombok.extern.slf4j.Slf4j
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.http.HttpStatus
;
import
org.springframework.http.HttpStatus
;
import
org.springframework.util.StringUtils
;
import
org.springframework.util.StringUtils
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.PostMapping
;
import
org.springframework.web.bind.annotation.PostMapping
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RequestParam
;
import
org.springframework.web.bind.annotation.RequestParam
;
...
@@ -39,6 +42,15 @@ public class LemengTokenController {
...
@@ -39,6 +42,15 @@ public class LemengTokenController {
@Resource
@Resource
private
LemengProperties
lemengProperties
;
private
LemengProperties
lemengProperties
;
@Resource
private
LemengTokenStatusService
lemengTokenStatusService
;
@Operation
(
summary
=
"Token 状态"
,
description
=
"当前 access_token 来源、JWT 是否过期、是否存在 refresh_token(不返回密钥内容)"
)
@GetMapping
(
"/status"
)
public
Result
<
LemengTokenStatusDTO
>
status
()
{
return
Result
.
success
(
lemengTokenStatusService
.
getStatus
());
}
@Operation
(
summary
=
"刷新 Token"
,
description
=
"POST /oauth/token,grant_type=refresh_token"
)
@Operation
(
summary
=
"刷新 Token"
,
description
=
"POST /oauth/token,grant_type=refresh_token"
)
@PostMapping
(
"/refresh"
)
@PostMapping
(
"/refresh"
)
public
Result
<
TokenRefreshResult
>
refreshToken
(
public
Result
<
TokenRefreshResult
>
refreshToken
(
...
...
src/main/java/cn/nhsoft/hanni/lemeng/dto/LemengTokenStatusDTO.java
0 → 100644
View file @
a04c5989
package
cn
.
nhsoft
.
hanni
.
lemeng
.
dto
;
import
lombok.AllArgsConstructor
;
import
lombok.Builder
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
/**
* 乐檬 Token 状态(校验展示用)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public
class
LemengTokenStatusDTO
{
/** 是否有可用的 access_token(内存或配置) */
private
boolean
hasAccessToken
;
/** 来源:memory / config / none */
private
String
source
;
/** 当前默认账套号(若有) */
private
String
systemBookCode
;
/** JWT exp(Unix 秒),无法解析时为 null */
private
Long
accessTokenExpiresAt
;
/** 按 JWT exp 判断是否仍有效 */
private
boolean
accessTokenValid
;
/** 内存或库中是否存在 refresh_token */
private
boolean
refreshTokenPresent
;
/** 说明 */
private
String
message
;
}
src/main/java/cn/nhsoft/hanni/lemeng/repository/LemengOAuthTokenRepository.java
View file @
a04c5989
...
@@ -7,4 +7,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
...
@@ -7,4 +7,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
* 乐檬 OAuth Token 表
* 乐檬 OAuth Token 表
*/
*/
public
interface
LemengOAuthTokenRepository
extends
JpaRepository
<
LemengOAuthToken
,
String
>
{
public
interface
LemengOAuthTokenRepository
extends
JpaRepository
<
LemengOAuthToken
,
String
>
{
boolean
existsByRefreshTokenIsNotNull
();
}
}
src/main/java/cn/nhsoft/hanni/lemeng/service/LemengTokenPersistService.java
View file @
a04c5989
...
@@ -70,7 +70,8 @@ public class LemengTokenPersistService implements ApplicationRunner {
...
@@ -70,7 +70,8 @@ public class LemengTokenPersistService implements ApplicationRunner {
if
(
expiresIn
!=
null
&&
expiresIn
>
0
)
{
if
(
expiresIn
!=
null
&&
expiresIn
>
0
)
{
entity
.
setExpiresAt
(
new
Date
(
System
.
currentTimeMillis
()
+
expiresIn
*
1000L
));
entity
.
setExpiresAt
(
new
Date
(
System
.
currentTimeMillis
()
+
expiresIn
*
1000L
));
}
}
lemengOAuthTokenRepository
.
save
(
entity
);
lemengOAuthTokenRepository
.
saveAndFlush
(
entity
);
log
.
info
(
"乐檬 Token 已写入数据库表 lemeng_oauth_token,账套: {}"
,
systemBookCode
);
tokenHolder
.
setToken
(
systemBookCode
,
accessToken
,
refreshToken
);
tokenHolder
.
setToken
(
systemBookCode
,
accessToken
,
refreshToken
);
}
}
...
...
src/main/java/cn/nhsoft/hanni/lemeng/service/LemengTokenStatusService.java
0 → 100644
View file @
a04c5989
package
cn
.
nhsoft
.
hanni
.
lemeng
.
service
;
import
cn.nhsoft.hanni.config.LemonApiProperties
;
import
cn.nhsoft.hanni.lemeng.dto.LemengTokenStatusDTO
;
import
cn.nhsoft.hanni.lemeng.model.LemengOAuthToken
;
import
cn.nhsoft.hanni.lemeng.repository.LemengOAuthTokenRepository
;
import
cn.nhsoft.hanni.lemeng.util.JwtExpUtil
;
import
cn.nhsoft.hanni.lemeng.util.TokenHolder
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
org.springframework.stereotype.Service
;
import
org.springframework.util.StringUtils
;
import
javax.annotation.Resource
;
import
java.util.Optional
;
/**
* 乐檬 Token 状态(用于前端校验展示)
*/
@Service
public
class
LemengTokenStatusService
{
@Resource
private
TokenHolder
tokenHolder
;
@Resource
private
LemonApiProperties
lemonApiProperties
;
@Resource
private
LemengOAuthTokenRepository
lemengOAuthTokenRepository
;
@Resource
private
ObjectMapper
objectMapper
;
public
LemengTokenStatusDTO
getStatus
()
{
String
token
=
tokenHolder
.
getAccessToken
();
String
source
=
"memory"
;
if
(!
StringUtils
.
hasText
(
token
))
{
token
=
lemonApiProperties
.
getAccessToken
();
source
=
"config"
;
}
if
(!
StringUtils
.
hasText
(
token
))
{
return
LemengTokenStatusDTO
.
builder
()
.
hasAccessToken
(
false
)
.
source
(
"none"
)
.
accessTokenValid
(
false
)
.
refreshTokenPresent
(
hasRefreshInDbOrMemory
(
null
))
.
message
(
"无 access_token:请完成 OAuth、POST /lemeng/token/set,或配置 lemon.api.access-token"
)
.
build
();
}
Long
expSec
=
JwtExpUtil
.
extractExpSeconds
(
token
,
objectMapper
);
long
nowMs
=
System
.
currentTimeMillis
();
boolean
valid
;
String
msg
;
if
(
expSec
==
null
)
{
valid
=
true
;
msg
=
"JWT 无 exp 或无法解析,假定可用;建议以实际调用乐檬接口结果为准"
;
}
else
{
valid
=
expSec
*
1000L
>
nowMs
+
10_000L
;
msg
=
valid
?
"access_token 未过期(按 JWT exp)"
:
"access_token 已过期(按 JWT exp),请刷新或重新授权"
;
}
String
book
=
tokenHolder
.
getDefaultSystemBookCode
();
if
(!
StringUtils
.
hasText
(
book
))
{
book
=
null
;
}
boolean
refreshPresent
=
hasRefreshInDbOrMemory
(
book
);
return
LemengTokenStatusDTO
.
builder
()
.
hasAccessToken
(
true
)
.
source
(
source
)
.
systemBookCode
(
book
)
.
accessTokenExpiresAt
(
expSec
)
.
accessTokenValid
(
valid
)
.
refreshTokenPresent
(
refreshPresent
)
.
message
(
msg
)
.
build
();
}
private
boolean
hasRefreshInDbOrMemory
(
String
systemBookCode
)
{
String
rt
=
tokenHolder
.
getRefreshToken
();
if
(
StringUtils
.
hasText
(
rt
))
{
return
true
;
}
if
(
StringUtils
.
hasText
(
systemBookCode
))
{
Optional
<
LemengOAuthToken
>
o
=
lemengOAuthTokenRepository
.
findById
(
systemBookCode
);
return
o
.
isPresent
()
&&
StringUtils
.
hasText
(
o
.
get
().
getRefreshToken
());
}
return
lemengOAuthTokenRepository
.
existsByRefreshTokenIsNotNull
();
}
}
src/main/java/cn/nhsoft/hanni/lemeng/util/JwtExpUtil.java
0 → 100644
View file @
a04c5989
package
cn
.
nhsoft
.
hanni
.
lemeng
.
util
;
import
com.fasterxml.jackson.databind.JsonNode
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
java.nio.charset.StandardCharsets
;
import
java.util.Base64
;
/**
* 解析 JWT payload 中的 exp(秒)
*/
public
final
class
JwtExpUtil
{
private
JwtExpUtil
()
{
}
public
static
Long
extractExpSeconds
(
String
jwt
,
ObjectMapper
objectMapper
)
{
if
(
jwt
==
null
||
!
jwt
.
contains
(
"."
))
{
return
null
;
}
try
{
String
[]
parts
=
jwt
.
split
(
"\\."
);
if
(
parts
.
length
<
2
)
{
return
null
;
}
byte
[]
decoded
=
Base64
.
getUrlDecoder
().
decode
(
parts
[
1
]);
JsonNode
node
=
objectMapper
.
readTree
(
new
String
(
decoded
,
StandardCharsets
.
UTF_8
));
JsonNode
exp
=
node
.
get
(
"exp"
);
if
(
exp
==
null
||
exp
.
isNull
())
{
return
null
;
}
return
exp
.
isNumber
()
?
exp
.
asLong
()
:
Long
.
parseLong
(
exp
.
asText
());
}
catch
(
Exception
e
)
{
return
null
;
}
}
}
src/main/resources/db/mysql-item-sync.sql
View file @
a04c5989
...
@@ -13,6 +13,7 @@ CREATE TABLE IF NOT EXISTS `item_sync_log` (
...
@@ -13,6 +13,7 @@ CREATE TABLE IF NOT EXISTS `item_sync_log` (
`lemeng_count`
INT
DEFAULT
NULL
COMMENT
'乐檬返回商品条数'
,
`lemeng_count`
INT
DEFAULT
NULL
COMMENT
'乐檬返回商品条数'
,
`hanni_count`
INT
DEFAULT
NULL
COMMENT
'本批处理条数'
,
`hanni_count`
INT
DEFAULT
NULL
COMMENT
'本批处理条数'
,
`status`
VARCHAR
(
16
)
DEFAULT
NULL
COMMENT
'SUCCESS/FAIL/SKIP/RUNNING'
,
`status`
VARCHAR
(
16
)
DEFAULT
NULL
COMMENT
'SUCCESS/FAIL/SKIP/RUNNING'
,
`progress_message`
VARCHAR
(
512
)
DEFAULT
NULL
COMMENT
'进行中时的进度描述(独立提交)'
,
`hanni_response`
TEXT
DEFAULT
NULL
COMMENT
'汉尼接口响应摘要'
,
`hanni_response`
TEXT
DEFAULT
NULL
COMMENT
'汉尼接口响应摘要'
,
`error_msg`
TEXT
DEFAULT
NULL
COMMENT
'失败原因'
,
`error_msg`
TEXT
DEFAULT
NULL
COMMENT
'失败原因'
,
`start_time`
DATETIME
(
3
)
DEFAULT
NULL
COMMENT
'开始时间'
,
`start_time`
DATETIME
(
3
)
DEFAULT
NULL
COMMENT
'开始时间'
,
...
...
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