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
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
386 additions
and
38 deletions
+386
-38
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
+0
-0
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 {
@Resource
private
ItemSyncLogRepository
itemSyncLogRepository
;
@Operation
(
summary
=
"商品档案同步"
,
description
=
"
从乐檬获取商品档案,加工后传入汉尼系统;字段变更时调用汉尼更新接口
"
)
@Operation
(
summary
=
"商品档案同步"
,
description
=
"
异步执行:立即返回 syncLogId,可在同步记录中轮询进度(RUNNING + progressMessage)
"
)
@GetMapping
(
"/sync"
)
public
Result
<
Void
>
syncItems
()
{
itemSyncService
.
syncItems
();
return
Result
.
success
();
public
Result
<
Long
>
syncItems
()
{
return
Result
.
success
(
itemSyncService
.
syncItemsStart
());
}
@Operation
(
summary
=
"按乐檬参数手动同步"
,
description
=
"与定时任务相同:按条件多页拉取直至不足一页,再同步到汉尼"
)
@GetMapping
(
"/sync/manual"
)
public
Result
<
Void
>
syncItemsManual
(
public
Result
<
Long
>
syncItemsManual
(
@Parameter
(
description
=
"页码"
,
required
=
true
)
@RequestParam
(
defaultValue
=
"1"
)
Integer
pageNo
,
@Parameter
(
description
=
"分页大小(最大1000)"
,
required
=
true
)
@RequestParam
(
defaultValue
=
"100"
)
Integer
pageSize
,
@Parameter
(
description
=
"最后修改时间(yyyy-MM-dd HH:mm:ss)"
)
@RequestParam
(
required
=
false
)
String
lastDownloadTime
,
...
...
@@ -54,10 +53,10 @@ public class ItemController {
@Parameter
(
description
=
"是否过滤淘汰商品,默认 true(仅同步非淘汰)"
)
@RequestParam
(
required
=
false
)
Boolean
filterWeedOut
,
@Parameter
(
description
=
"商品编码,逗号分隔"
)
@RequestParam
(
required
=
false
)
String
itemNums
,
@Parameter
(
description
=
"商品代码,逗号分隔"
)
@RequestParam
(
required
=
false
)
String
itemCodes
)
{
itemSyncService
.
syncItemsWithParams
(
LemonItemFindRequestMapper
.
fromQuery
(
Long
logId
=
itemSyncService
.
syncItemsWithParamsStart
(
LemonItemFindRequestMapper
.
fromQuery
(
pageNo
,
pageSize
,
lastDownloadTime
,
itemType
,
itemCategoryCode
,
filterSleep
,
filterWeedOut
,
itemNums
,
itemCodes
));
return
Result
.
success
();
return
Result
.
success
(
logId
);
}
@Operation
(
summary
=
"从乐檬获取商品档案"
,
description
=
"调用乐檬 nhsoft.amazon.basic.item.find,仅查询当前页,不同步"
)
...
...
@@ -95,12 +94,15 @@ public class ItemController {
return
Result
.
success
(
dto
);
}
@Operation
(
summary
=
"商品同步明细"
,
description
=
"按同步日志 ID 分页查询本次任务每条商品的 ADD/UPDATE/SKIP 记录"
)
@Operation
(
summary
=
"商品同步明细"
,
description
=
"按同步日志 ID 分页查询本次任务每条商品的 ADD/UPDATE/SKIP 记录
;支持条码模糊、动作、成功状态筛选
"
)
@GetMapping
(
"/sync-logs/{logId}/details"
)
public
Result
<
ItemSyncDetailPageDTO
>
getSyncLogDetails
(
@Parameter
(
description
=
"同步日志 id"
)
@PathVariable
(
"logId"
)
Long
logId
,
@Parameter
(
description
=
"页码"
)
@RequestParam
(
defaultValue
=
"0"
)
Integer
page
,
@Parameter
(
description
=
"每页条数"
)
@RequestParam
(
defaultValue
=
"50"
)
Integer
size
)
{
return
Result
.
success
(
itemSyncService
.
getSyncDetails
(
logId
,
page
,
size
));
@Parameter
(
description
=
"每页条数"
)
@RequestParam
(
defaultValue
=
"50"
)
Integer
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 {
* 从乐檬商品对象解析「最后修改时间」字符串(多字段兼容)
*/
private
String
extractLemonLastModified
(
Map
<
String
,
Object
>
m
)
{
// 乐檬 item.find 常见为 item_last_edit_time,须优先匹配
String
[]
keys
=
{
"item_last_edit_time"
,
"item_modify_time"
,
"item_update_time"
,
"last_modify_time"
,
"last_update_time"
,
"modify_time"
,
"update_time"
,
"gmt_modified"
,
"item_last_modify_time"
,
"last_download_time"
,
"item_last_update_time"
...
...
@@ -126,6 +128,43 @@ public class LemonToHanniItemConverter {
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
)
{
HanniItemDTO
dto
=
new
HanniItemDTO
();
dto
.
setBarcode
(
getStr
(
m
,
"item_barcode"
,
"barcode"
));
...
...
@@ -137,8 +176,8 @@ public class LemonToHanniItemConverter {
dto
.
setParea
(
getStr
(
m
,
"item_place"
,
"parea"
,
"place"
));
dto
.
setIsweight
(
getBoolStr
(
m
,
"item_weight_flag"
,
"isweight"
));
dto
.
setPackratio
(
getStr
(
m
,
"item_purchase_rate"
,
"packratio"
,
"purchase_rate"
));
dto
.
setGrade
(
getStr
(
m
,
"grade"
));
dto
.
setModel
(
getStr
(
m
,
"model"
));
dto
.
setGrade
(
extractGrade
(
m
));
dto
.
setModel
(
extractModel
(
m
));
dto
.
setGid
(
getLong
(
m
,
"item_num"
,
"gid"
,
"id"
));
return
dto
;
}
...
...
src/main/java/cn/nhsoft/hanni/item/model/ItemSyncLog.java
View file @
a04c5989
...
...
@@ -35,12 +35,18 @@ public class ItemSyncLog {
private
Integer
hanniCount
;
/**
* 同步状态:SUCCESS/FAIL
* 同步状态:SUCCESS/FAIL
/SKIP/RUNNING
*/
@Column
(
name
=
"status"
,
length
=
16
)
private
String
status
;
/**
* 进行中时的进度描述(独立事务提交,供前端轮询)
*/
@Column
(
name
=
"progress_message"
,
length
=
512
)
private
String
progressMessage
;
/**
* 汉尼接口响应内容
*/
@Column
(
name
=
"hanni_response"
,
columnDefinition
=
"TEXT"
)
...
...
@@ -120,6 +126,14 @@ public class ItemSyncLog {
this
.
status
=
status
;
}
public
String
getProgressMessage
()
{
return
progressMessage
;
}
public
void
setProgressMessage
(
String
progressMessage
)
{
this
.
progressMessage
=
progressMessage
;
}
public
String
getHanniResponse
()
{
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;
import
org.springframework.data.domain.Page
;
import
org.springframework.data.domain.Pageable
;
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
);
}
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;
/**
* 商品档案同步服务接口
* <p>
* 负责从乐檬获取商品档案,加工后传入汉尼系统;
* 当养馋记系统字段变更时,调用汉尼系统更新接口
* </p>
*
* @author hanni-external-api
*/
public
interface
ItemSyncService
{
/**
* 同步商品档案
* <p>从乐檬获取商品档案,加工后传入汉尼系统</p>
* 定时/内部:异步启动同步(不返回日志 ID)
*/
void
syncItems
();
/**
* 从乐檬获取商品档案
* <p>仅调用乐檬接口,不做加工和同步;请求方式 GET,参数 page_no、page_size 必填</p>
*
* @param request 查询参数(对应 nhsoft.amazon.basic.item.find 接口文档)
* @return 乐檬商品档案原始响应
* 触发同步并返回日志 ID(立即返回,任务在后台执行)
*/
String
getItemsFromLemon
(
LemonItemFindRequest
request
);
Long
syncItemsStart
(
);
/**
* 按乐檬查询参数手动同步到汉尼
* <p>使用指定查询参数从乐檬获取商品,经转换、过滤、变更检测后同步到汉尼</p>
*
* @param request 乐檬商品档案查询参数(与 nhsoft.amazon.basic.item.find 一致)
* 按乐檬参数异步启动同步并返回日志 ID
*/
void
syncItemsWithParams
(
LemonItemFindRequest
request
);
Long
syncItemsWithParamsStart
(
LemonItemFindRequest
request
);
/**
* 分页查询某次同步任务的商品明细
*/
ItemSyncDetailPageDTO
getSyncDetails
(
Long
syncLogId
,
int
page
,
int
size
);
String
getItemsFromLemon
(
LemonItemFindRequest
request
);
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
This diff is collapsed.
Click to expand it.
src/main/java/cn/nhsoft/hanni/lemeng/controller/LemengTokenController.java
View file @
a04c5989
...
...
@@ -3,14 +3,17 @@ package cn.nhsoft.hanni.lemeng.controller;
import
cn.nhsoft.hanni.common.Result
;
import
cn.nhsoft.hanni.lemeng.client.LemengTokenRestClient
;
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.service.LemengTokenPersistService
;
import
cn.nhsoft.hanni.lemeng.service.LemengTokenStatusService
;
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.http.HttpStatus
;
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.RequestMapping
;
import
org.springframework.web.bind.annotation.RequestParam
;
...
...
@@ -39,6 +42,15 @@ public class LemengTokenController {
@Resource
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"
)
@PostMapping
(
"/refresh"
)
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;
* 乐檬 OAuth Token 表
*/
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 {
if
(
expiresIn
!=
null
&&
expiresIn
>
0
)
{
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
);
}
...
...
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` (
`lemeng_count`
INT
DEFAULT
NULL
COMMENT
'乐檬返回商品条数'
,
`hanni_count`
INT
DEFAULT
NULL
COMMENT
'本批处理条数'
,
`status`
VARCHAR
(
16
)
DEFAULT
NULL
COMMENT
'SUCCESS/FAIL/SKIP/RUNNING'
,
`progress_message`
VARCHAR
(
512
)
DEFAULT
NULL
COMMENT
'进行中时的进度描述(独立提交)'
,
`hanni_response`
TEXT
DEFAULT
NULL
COMMENT
'汉尼接口响应摘要'
,
`error_msg`
TEXT
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