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
f180b5d0
Commit
f180b5d0
authored
Mar 20, 2026
by
yangxianglong
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
自采联营门店系统开发
parent
25c521e9
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
443 additions
and
15 deletions
+443
-15
HanniExternalApiApplication.java
...ain/java/cn/nhsoft/hanni/HanniExternalApiApplication.java
+2
-2
ItemController.java
.../java/cn/nhsoft/hanni/item/controller/ItemController.java
+11
-0
LemonToHanniItemConverter.java
...hsoft/hanni/item/converter/LemonToHanniItemConverter.java
+63
-0
ItemSyncDetailPageDTO.java
.../java/cn/nhsoft/hanni/item/dto/ItemSyncDetailPageDTO.java
+17
-0
LemonItemRow.java
src/main/java/cn/nhsoft/hanni/item/dto/LemonItemRow.java
+21
-0
ItemSyncDetail.java
src/main/java/cn/nhsoft/hanni/item/model/ItemSyncDetail.java
+68
-0
ItemSyncSnapshot.java
...ain/java/cn/nhsoft/hanni/item/model/ItemSyncSnapshot.java
+72
-0
ItemSyncDetailRepository.java
...hsoft/hanni/item/repository/ItemSyncDetailRepository.java
+11
-0
ItemSyncSnapshotRepository.java
...oft/hanni/item/repository/ItemSyncSnapshotRepository.java
+11
-0
ItemSyncService.java
...in/java/cn/nhsoft/hanni/item/service/ItemSyncService.java
+6
-2
ItemSyncServiceImpl.java
...n/nhsoft/hanni/item/service/impl/ItemSyncServiceImpl.java
+0
-0
LemengAuthController.java
.../nhsoft/hanni/lemeng/controller/LemengAuthController.java
+3
-3
LemengTokenController.java
...nhsoft/hanni/lemeng/controller/LemengTokenController.java
+5
-5
LemengTokenRefreshJob.java
...ava/cn/nhsoft/hanni/lemeng/job/LemengTokenRefreshJob.java
+5
-1
LemengOAuthToken.java
...n/java/cn/nhsoft/hanni/lemeng/model/LemengOAuthToken.java
+51
-0
LemengOAuthTokenRepository.java
...t/hanni/lemeng/repository/LemengOAuthTokenRepository.java
+10
-0
LemengTokenPersistService.java
...hsoft/hanni/lemeng/service/LemengTokenPersistService.java
+84
-0
TokenHolder.java
src/main/java/cn/nhsoft/hanni/lemeng/util/TokenHolder.java
+3
-2
No files found.
src/main/java/cn/nhsoft/hanni/HanniExternalApiApplication.java
View file @
f180b5d0
...
...
@@ -14,8 +14,8 @@ import org.springframework.scheduling.annotation.EnableScheduling;
*/
@EnableScheduling
@SpringBootApplication
@EntityScan
({
"cn.nhsoft.hanni.item"
})
@EnableJpaRepositories
({
"cn.nhsoft.hanni.item"
})
@EntityScan
({
"cn.nhsoft.hanni.item"
,
"cn.nhsoft.hanni.lemeng"
})
@EnableJpaRepositories
({
"cn.nhsoft.hanni.item"
,
"cn.nhsoft.hanni.lemeng.repository"
})
public
class
HanniExternalApiApplication
{
public
static
void
main
(
String
[]
args
)
{
...
...
src/main/java/cn/nhsoft/hanni/item/controller/ItemController.java
View file @
f180b5d0
package
cn
.
nhsoft
.
hanni
.
item
.
controller
;
import
cn.nhsoft.hanni.common.Result
;
import
cn.nhsoft.hanni.item.dto.ItemSyncDetailPageDTO
;
import
cn.nhsoft.hanni.item.dto.ItemSyncLogPageDTO
;
import
cn.nhsoft.hanni.item.model.ItemSyncLog
;
import
cn.nhsoft.hanni.item.repository.ItemSyncLogRepository
;
...
...
@@ -13,6 +14,7 @@ import org.springframework.data.domain.Page;
import
org.springframework.data.domain.PageRequest
;
import
org.springframework.data.domain.Sort
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.PathVariable
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RequestParam
;
import
org.springframework.web.bind.annotation.RestController
;
...
...
@@ -92,4 +94,13 @@ public class ItemController {
ItemSyncLogPageDTO
dto
=
new
ItemSyncLogPageDTO
(
resultPage
.
getContent
(),
resultPage
.
getTotalElements
());
return
Result
.
success
(
dto
);
}
@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
));
}
}
src/main/java/cn/nhsoft/hanni/item/converter/LemonToHanniItemConverter.java
View file @
f180b5d0
package
cn
.
nhsoft
.
hanni
.
item
.
converter
;
import
cn.nhsoft.hanni.item.dto.HanniItemDTO
;
import
cn.nhsoft.hanni.item.dto.LemonItemRow
;
import
cn.nhsoft.hanni.item.util.LemonItemResponseParser
;
import
org.springframework.util.StringUtils
;
import
com.fasterxml.jackson.core.type.TypeReference
;
import
com.fasterxml.jackson.databind.JsonNode
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
...
...
@@ -56,6 +58,34 @@ public class LemonToHanniItemConverter {
}
}
/**
* 转换为带乐檬最后修改时间的行(同步、跳过逻辑用)
*/
public
List
<
LemonItemRow
>
convertToRows
(
String
lemonResponse
)
{
if
(
lemonResponse
==
null
||
lemonResponse
.
trim
().
isEmpty
())
{
return
new
ArrayList
<>();
}
try
{
JsonNode
root
=
objectMapper
.
readTree
(
lemonResponse
);
JsonNode
itemsNode
=
LemonItemResponseParser
.
findItemsArray
(
root
);
if
(
itemsNode
==
null
||
!
itemsNode
.
isArray
())
{
log
.
warn
(
"乐檬返回中未找到商品列表数组,请检查接口返回结构"
);
return
new
ArrayList
<>();
}
List
<
LemonItemRow
>
result
=
new
ArrayList
<>();
for
(
JsonNode
node
:
itemsNode
)
{
LemonItemRow
row
=
convertSingleToRow
(
node
);
if
(
row
!=
null
)
{
result
.
add
(
row
);
}
}
return
result
;
}
catch
(
Exception
e
)
{
log
.
error
(
"乐檬商品数据转换失败"
,
e
);
return
new
ArrayList
<>();
}
}
private
HanniItemDTO
convertSingle
(
JsonNode
node
)
{
try
{
Map
<
String
,
Object
>
map
=
objectMapper
.
convertValue
(
node
,
new
TypeReference
<
Map
<
String
,
Object
>>()
{});
...
...
@@ -69,6 +99,39 @@ public class LemonToHanniItemConverter {
}
}
private
LemonItemRow
convertSingleToRow
(
JsonNode
node
)
{
try
{
Map
<
String
,
Object
>
map
=
objectMapper
.
convertValue
(
node
,
new
TypeReference
<
Map
<
String
,
Object
>>()
{});
if
(!
shouldIncludeItem
(
map
))
{
return
null
;
}
HanniItemDTO
dto
=
convertFromMap
(
map
);
String
lemonMod
=
extractLemonLastModified
(
map
);
return
new
LemonItemRow
(
dto
,
lemonMod
);
}
catch
(
Exception
e
)
{
log
.
warn
(
"单条商品转换失败: {}"
,
node
,
e
);
return
null
;
}
}
/**
* 从乐檬商品对象解析「最后修改时间」字符串(多字段兼容)
*/
private
String
extractLemonLastModified
(
Map
<
String
,
Object
>
m
)
{
String
[]
keys
=
{
"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"
};
for
(
String
k
:
keys
)
{
Object
v
=
m
.
get
(
k
);
if
(
v
!=
null
&&
StringUtils
.
hasText
(
v
.
toString
()))
{
return
v
.
toString
().
trim
();
}
}
return
null
;
}
/**
* 养馋记商品过滤:是否停购=否,且不为淘汰、删除的商品才传入汉尼
*/
...
...
src/main/java/cn/nhsoft/hanni/item/dto/ItemSyncDetailPageDTO.java
0 → 100644
View file @
f180b5d0
package
cn
.
nhsoft
.
hanni
.
item
.
dto
;
import
cn.nhsoft.hanni.item.model.ItemSyncDetail
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
import
java.util.List
;
@Data
@NoArgsConstructor
@AllArgsConstructor
public
class
ItemSyncDetailPageDTO
{
private
List
<
ItemSyncDetail
>
records
;
private
long
total
;
}
src/main/java/cn/nhsoft/hanni/item/dto/LemonItemRow.java
0 → 100644
View file @
f180b5d0
package
cn
.
nhsoft
.
hanni
.
item
.
dto
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
/**
* 乐檬单条商品转换结果 + 乐檬侧最后修改时间(用于跳过未变化商品)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public
class
LemonItemRow
{
private
HanniItemDTO
item
;
/**
* 乐檬商品最后修改时间原始字符串(与快照表按字符串相等比较,字段名兼容见转换器)
*/
private
String
lemonLastModified
;
}
src/main/java/cn/nhsoft/hanni/item/model/ItemSyncDetail.java
0 → 100644
View file @
f180b5d0
package
cn
.
nhsoft
.
hanni
.
item
.
model
;
import
lombok.Data
;
import
javax.persistence.*
;
import
java.util.Date
;
/**
* 单次同步任务中的商品级明细(可后台分页查询)
*/
@Data
@Entity
@Table
(
name
=
"item_sync_detail"
,
indexes
=
{
@Index
(
name
=
"idx_detail_sync_log_id"
,
columnList
=
"sync_log_id"
),
@Index
(
name
=
"idx_detail_log_barcode"
,
columnList
=
"sync_log_id,barcode"
)
})
public
class
ItemSyncDetail
{
public
static
final
String
ACTION_ADD
=
"ADD"
;
public
static
final
String
ACTION_UPDATE
=
"UPDATE"
;
public
static
final
String
ACTION_SKIP_TIME
=
"SKIP_TIME"
;
public
static
final
String
ACTION_SKIP_NO_BARCODE
=
"SKIP_NO_BARCODE"
;
@Id
@GeneratedValue
(
strategy
=
GenerationType
.
IDENTITY
)
private
Long
id
;
@Column
(
name
=
"sync_log_id"
,
nullable
=
false
)
private
Long
syncLogId
;
@Column
(
name
=
"barcode"
,
length
=
64
)
private
String
barcode
;
@Column
(
name
=
"gid"
)
private
Long
gid
;
@Column
(
name
=
"name"
,
length
=
512
)
private
String
name
;
/** ADD / UPDATE / SKIP_TIME / SKIP_NO_BARCODE */
@Column
(
name
=
"action"
,
length
=
32
)
private
String
action
;
/** 本次乐檬返回的修改时间 */
@Column
(
name
=
"lemon_last_modified"
,
length
=
64
)
private
String
lemonLastModified
;
/** 快照中上次记录的修改时间(SKIP_TIME 时有值) */
@Column
(
name
=
"stored_lemon_modified"
,
length
=
64
)
private
String
storedLemonModified
;
@Column
(
name
=
"success"
)
private
Boolean
success
;
@Column
(
name
=
"remark"
,
length
=
500
)
private
String
remark
;
@Column
(
name
=
"created_at"
)
@Temporal
(
TemporalType
.
TIMESTAMP
)
private
Date
createdAt
;
@PrePersist
public
void
prePersist
()
{
if
(
createdAt
==
null
)
{
createdAt
=
new
Date
();
}
}
}
src/main/java/cn/nhsoft/hanni/item/model/ItemSyncSnapshot.java
0 → 100644
View file @
f180b5d0
package
cn
.
nhsoft
.
hanni
.
item
.
model
;
import
lombok.Data
;
import
javax.persistence.*
;
import
java.math.BigDecimal
;
import
java.util.Date
;
/**
* 商品同步快照:仅存汉尼所需字段 + 乐檬最后修改时间;时间未变则可跳过推汉尼
*/
@Data
@Entity
@Table
(
name
=
"item_sync_snapshot"
,
indexes
=
{
@Index
(
name
=
"idx_snapshot_lemon_mod"
,
columnList
=
"lemon_last_modified"
)
})
public
class
ItemSyncSnapshot
{
@Id
@GeneratedValue
(
strategy
=
GenerationType
.
IDENTITY
)
private
Long
id
;
@Column
(
name
=
"barcode"
,
length
=
64
,
nullable
=
false
,
unique
=
true
)
private
String
barcode
;
@Column
(
name
=
"gid"
)
private
Long
gid
;
@Column
(
name
=
"name"
,
length
=
512
)
private
String
name
;
@Column
(
name
=
"spec"
,
length
=
255
)
private
String
spec
;
@Column
(
name
=
"saleunit"
,
length
=
64
)
private
String
saleunit
;
@Column
(
name
=
"price"
,
precision
=
18
,
scale
=
4
)
private
BigDecimal
price
;
@Column
(
name
=
"retailprice"
,
precision
=
18
,
scale
=
4
)
private
BigDecimal
retailprice
;
@Column
(
name
=
"parea"
,
length
=
255
)
private
String
parea
;
@Column
(
name
=
"isweight"
,
length
=
8
)
private
String
isweight
;
@Column
(
name
=
"packratio"
,
length
=
64
)
private
String
packratio
;
@Column
(
name
=
"grade"
,
length
=
64
)
private
String
grade
;
@Column
(
name
=
"model"
,
length
=
128
)
private
String
model
;
/** 乐檬侧最后修改时间(接口返回原始字符串,与本次拉取比较) */
@Column
(
name
=
"lemon_last_modified"
,
length
=
64
)
private
String
lemonLastModified
;
@Column
(
name
=
"updated_at"
)
@Temporal
(
TemporalType
.
TIMESTAMP
)
private
Date
updatedAt
;
@PrePersist
@PreUpdate
public
void
touch
()
{
this
.
updatedAt
=
new
Date
();
}
}
src/main/java/cn/nhsoft/hanni/item/repository/ItemSyncDetailRepository.java
0 → 100644
View file @
f180b5d0
package
cn
.
nhsoft
.
hanni
.
item
.
repository
;
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
;
public
interface
ItemSyncDetailRepository
extends
JpaRepository
<
ItemSyncDetail
,
Long
>
{
Page
<
ItemSyncDetail
>
findBySyncLogIdOrderByIdAsc
(
Long
syncLogId
,
Pageable
pageable
);
}
src/main/java/cn/nhsoft/hanni/item/repository/ItemSyncSnapshotRepository.java
0 → 100644
View file @
f180b5d0
package
cn
.
nhsoft
.
hanni
.
item
.
repository
;
import
cn.nhsoft.hanni.item.model.ItemSyncSnapshot
;
import
org.springframework.data.jpa.repository.JpaRepository
;
import
java.util.Optional
;
public
interface
ItemSyncSnapshotRepository
extends
JpaRepository
<
ItemSyncSnapshot
,
Long
>
{
Optional
<
ItemSyncSnapshot
>
findByBarcode
(
String
barcode
);
}
src/main/java/cn/nhsoft/hanni/item/service/ItemSyncService.java
View file @
f180b5d0
package
cn
.
nhsoft
.
hanni
.
item
.
service
;
import
cn.nhsoft.hanni.item.dto.ItemSyncDetailPageDTO
;
import
cn.nhsoft.hanni.item.dto.LemonItemFindRequest
;
import
java.util.List
;
/**
* 商品档案同步服务接口
* <p>
...
...
@@ -37,4 +36,9 @@ public interface ItemSyncService {
* @param request 乐檬商品档案查询参数(与 nhsoft.amazon.basic.item.find 一致)
*/
void
syncItemsWithParams
(
LemonItemFindRequest
request
);
/**
* 分页查询某次同步任务的商品明细
*/
ItemSyncDetailPageDTO
getSyncDetails
(
Long
syncLogId
,
int
page
,
int
size
);
}
src/main/java/cn/nhsoft/hanni/item/service/impl/ItemSyncServiceImpl.java
View file @
f180b5d0
This diff is collapsed.
Click to expand it.
src/main/java/cn/nhsoft/hanni/lemeng/controller/LemengAuthController.java
View file @
f180b5d0
...
...
@@ -3,7 +3,7 @@ package cn.nhsoft.hanni.lemeng.controller;
import
cn.nhsoft.hanni.lemeng.client.LemengTokenRestClient
;
import
cn.nhsoft.hanni.lemeng.config.LemengProperties
;
import
cn.nhsoft.hanni.lemeng.dto.TokenRefreshResult
;
import
cn.nhsoft.hanni.lemeng.
util.TokenHolder
;
import
cn.nhsoft.hanni.lemeng.
service.LemengTokenPersistService
;
import
io.swagger.v3.oas.annotations.Operation
;
import
io.swagger.v3.oas.annotations.Parameter
;
import
io.swagger.v3.oas.annotations.tags.Tag
;
...
...
@@ -32,7 +32,7 @@ public class LemengAuthController {
private
LemengProperties
lemengProperties
;
@Resource
private
TokenHolder
tokenHolder
;
private
LemengTokenPersistService
lemengTokenPersistService
;
@Operation
(
summary
=
"OAuth2 授权回调"
,
description
=
"使用 code 换取 access_token 并写入 TokenHolder;无 code 时跳转 Swagger"
)
@GetMapping
(
"/code"
)
...
...
@@ -61,7 +61,7 @@ public class LemengAuthController {
log
.
error
(
"无法解析账套号,请配置 lemeng.app.system-book-code"
);
return
"redirect:/swagger-ui.html?lemeng_auth=merchant"
;
}
tokenHolder
.
setToken
(
book
,
result
.
getAccessToken
(),
result
.
getRefreshToke
n
());
lemengTokenPersistService
.
saveAndCache
(
book
,
result
.
getAccessToken
(),
result
.
getRefreshToken
(),
result
.
getExpiresI
n
());
log
.
info
(
"OAuth 授权成功,账套: {}"
,
book
);
}
catch
(
Exception
e
)
{
log
.
error
(
"乐檬 OAuth 获取 Token 失败"
,
e
);
...
...
src/main/java/cn/nhsoft/hanni/lemeng/controller/LemengTokenController.java
View file @
f180b5d0
...
...
@@ -4,7 +4,7 @@ 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.TokenRefreshResult
;
import
cn.nhsoft.hanni.lemeng.
util.TokenHolder
;
import
cn.nhsoft.hanni.lemeng.
service.LemengTokenPersistService
;
import
io.swagger.v3.oas.annotations.Operation
;
import
io.swagger.v3.oas.annotations.Parameter
;
import
io.swagger.v3.oas.annotations.tags.Tag
;
...
...
@@ -34,7 +34,7 @@ public class LemengTokenController {
private
LemengTokenRestClient
lemengTokenRestClient
;
@Resource
private
TokenHolder
tokenHolder
;
private
LemengTokenPersistService
lemengTokenPersistService
;
@Resource
private
LemengProperties
lemengProperties
;
...
...
@@ -59,7 +59,7 @@ public class LemengTokenController {
if
(!
StringUtils
.
hasText
(
book
))
{
return
Result
.
error
(
"无法解析账套号,请配置 lemeng.app.system-book-code 或检查 token"
);
}
tokenHolder
.
setToken
(
book
,
result
.
getAccessToken
(),
result
.
getRefreshToke
n
());
lemengTokenPersistService
.
saveAndCache
(
book
,
result
.
getAccessToken
(),
result
.
getRefreshToken
(),
result
.
getExpiresI
n
());
return
Result
.
success
(
result
);
}
catch
(
HttpClientErrorException
e
)
{
HttpStatus
status
=
e
.
getStatusCode
();
...
...
@@ -85,7 +85,7 @@ public class LemengTokenController {
}
}
@Operation
(
summary
=
"手动设置 Token"
,
description
=
"将 access_token、refresh_token 写入
内存 TokenHolder
,供乐檬商品查询使用"
)
@Operation
(
summary
=
"手动设置 Token"
,
description
=
"将 access_token、refresh_token 写入
数据库并同步到内存
,供乐檬商品查询使用"
)
@PostMapping
(
"/set"
)
public
Result
<
String
>
setToken
(
@Parameter
(
description
=
"账套号"
,
required
=
true
)
@RequestParam
String
systemBookCode
,
...
...
@@ -94,7 +94,7 @@ public class LemengTokenController {
if
(!
StringUtils
.
hasText
(
systemBookCode
)
||
!
StringUtils
.
hasText
(
accessToken
)
||
!
StringUtils
.
hasText
(
refreshToken
))
{
return
Result
.
error
(
"systemBookCode、accessToken、refreshToken 均不能为空"
);
}
tokenHolder
.
setToken
(
systemBookCode
,
accessToken
,
refreshToken
);
lemengTokenPersistService
.
saveAndCache
(
systemBookCode
,
accessToken
,
refreshToken
);
log
.
info
(
"手动设置 Token 成功,账套号: {}"
,
systemBookCode
);
return
Result
.
success
(
systemBookCode
);
}
...
...
src/main/java/cn/nhsoft/hanni/lemeng/job/LemengTokenRefreshJob.java
View file @
f180b5d0
...
...
@@ -3,6 +3,7 @@ package cn.nhsoft.hanni.lemeng.job;
import
cn.nhsoft.hanni.lemeng.client.LemengTokenRestClient
;
import
cn.nhsoft.hanni.lemeng.config.LemengProperties
;
import
cn.nhsoft.hanni.lemeng.dto.TokenRefreshResult
;
import
cn.nhsoft.hanni.lemeng.service.LemengTokenPersistService
;
import
cn.nhsoft.hanni.lemeng.util.TokenHolder
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
;
...
...
@@ -28,6 +29,9 @@ public class LemengTokenRefreshJob {
private
TokenHolder
tokenHolder
;
@Resource
private
LemengTokenPersistService
lemengTokenPersistService
;
@Resource
private
LemengProperties
lemengProperties
;
@Scheduled
(
fixedDelay
=
50
*
60
*
1000
,
initialDelay
=
60
*
1000
)
...
...
@@ -59,7 +63,7 @@ public class LemengTokenRefreshJob {
if
(!
StringUtils
.
hasText
(
book
))
{
book
=
systemBookCode
;
}
tokenHolder
.
setToken
(
book
,
result
.
getAccessToken
(),
result
.
getRefreshToke
n
());
lemengTokenPersistService
.
saveAndCache
(
book
,
result
.
getAccessToken
(),
result
.
getRefreshToken
(),
result
.
getExpiresI
n
());
log
.
info
(
"定时刷新 token 成功,账套: {}"
,
book
);
}
catch
(
Exception
ex
)
{
log
.
error
(
"定时刷新账套 {} token 失败"
,
systemBookCode
,
ex
);
...
...
src/main/java/cn/nhsoft/hanni/lemeng/model/LemengOAuthToken.java
0 → 100644
View file @
f180b5d0
package
cn
.
nhsoft
.
hanni
.
lemeng
.
model
;
import
lombok.Getter
;
import
lombok.Setter
;
import
javax.persistence.Column
;
import
javax.persistence.Entity
;
import
javax.persistence.Id
;
import
javax.persistence.PrePersist
;
import
javax.persistence.PreUpdate
;
import
javax.persistence.Table
;
import
javax.persistence.Temporal
;
import
javax.persistence.TemporalType
;
import
java.util.Date
;
/**
* 乐檬 OAuth Token 持久化(access / refresh),按账套唯一
*/
@Getter
@Setter
@Entity
@Table
(
name
=
"lemeng_oauth_token"
)
public
class
LemengOAuthToken
{
@Id
@Column
(
name
=
"system_book_code"
,
length
=
64
,
nullable
=
false
)
private
String
systemBookCode
;
@Column
(
name
=
"access_token"
,
columnDefinition
=
"TEXT"
,
nullable
=
false
)
private
String
accessToken
;
@Column
(
name
=
"refresh_token"
,
columnDefinition
=
"TEXT"
)
private
String
refreshToken
;
/**
* access_token 预计过期时间(根据 OAuth 返回的 expires_in 推算,可能为空)
*/
@Column
(
name
=
"expires_at"
)
@Temporal
(
TemporalType
.
TIMESTAMP
)
private
Date
expiresAt
;
@Column
(
name
=
"updated_at"
)
@Temporal
(
TemporalType
.
TIMESTAMP
)
private
Date
updatedAt
;
@PrePersist
@PreUpdate
public
void
touchUpdatedAt
()
{
this
.
updatedAt
=
new
Date
();
}
}
src/main/java/cn/nhsoft/hanni/lemeng/repository/LemengOAuthTokenRepository.java
0 → 100644
View file @
f180b5d0
package
cn
.
nhsoft
.
hanni
.
lemeng
.
repository
;
import
cn.nhsoft.hanni.lemeng.model.LemengOAuthToken
;
import
org.springframework.data.jpa.repository.JpaRepository
;
/**
* 乐檬 OAuth Token 表
*/
public
interface
LemengOAuthTokenRepository
extends
JpaRepository
<
LemengOAuthToken
,
String
>
{
}
src/main/java/cn/nhsoft/hanni/lemeng/service/LemengTokenPersistService.java
0 → 100644
View file @
f180b5d0
package
cn
.
nhsoft
.
hanni
.
lemeng
.
service
;
import
cn.nhsoft.hanni.lemeng.model.LemengOAuthToken
;
import
cn.nhsoft.hanni.lemeng.repository.LemengOAuthTokenRepository
;
import
cn.nhsoft.hanni.lemeng.util.TokenHolder
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.boot.ApplicationArguments
;
import
org.springframework.boot.ApplicationRunner
;
import
org.springframework.core.Ordered
;
import
org.springframework.core.annotation.Order
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
import
org.springframework.util.StringUtils
;
import
javax.annotation.Resource
;
import
java.util.Date
;
import
java.util.List
;
/**
* 乐檬 Token 数据库持久化,并与内存 {@link TokenHolder} 同步。
* <p>启动时从库加载到内存;OAuth / 手动设置 / 定时刷新写入库并更新缓存。</p>
*/
@Slf4j
@Order
(
Ordered
.
HIGHEST_PRECEDENCE
)
@Service
public
class
LemengTokenPersistService
implements
ApplicationRunner
{
@Resource
private
LemengOAuthTokenRepository
lemengOAuthTokenRepository
;
@Resource
private
TokenHolder
tokenHolder
;
@Override
@Transactional
(
readOnly
=
true
)
public
void
run
(
ApplicationArguments
args
)
{
reloadFromDatabaseIntoMemory
();
}
/**
* 从数据库加载全部账套 Token 到 {@link TokenHolder}
*/
@Transactional
(
readOnly
=
true
)
public
void
reloadFromDatabaseIntoMemory
()
{
List
<
LemengOAuthToken
>
all
=
lemengOAuthTokenRepository
.
findAll
();
int
loaded
=
0
;
for
(
LemengOAuthToken
row
:
all
)
{
if
(
row
!=
null
&&
StringUtils
.
hasText
(
row
.
getSystemBookCode
())
&&
StringUtils
.
hasText
(
row
.
getAccessToken
()))
{
tokenHolder
.
setToken
(
row
.
getSystemBookCode
(),
row
.
getAccessToken
(),
row
.
getRefreshToken
());
loaded
++;
}
}
log
.
info
(
"乐檬 OAuth Token 已从数据库加载到内存,记录数: {},有效账套: {}"
,
all
.
size
(),
loaded
);
}
/**
* 持久化并刷新内存缓存
*
* @param expiresIn OAuth 返回的过期秒数,可为 null
*/
@Transactional
public
void
saveAndCache
(
String
systemBookCode
,
String
accessToken
,
String
refreshToken
,
Integer
expiresIn
)
{
if
(!
StringUtils
.
hasText
(
systemBookCode
)
||
!
StringUtils
.
hasText
(
accessToken
))
{
return
;
}
LemengOAuthToken
entity
=
lemengOAuthTokenRepository
.
findById
(
systemBookCode
).
orElse
(
new
LemengOAuthToken
());
entity
.
setSystemBookCode
(
systemBookCode
);
entity
.
setAccessToken
(
accessToken
);
entity
.
setRefreshToken
(
StringUtils
.
hasText
(
refreshToken
)
?
refreshToken
:
null
);
if
(
expiresIn
!=
null
&&
expiresIn
>
0
)
{
entity
.
setExpiresAt
(
new
Date
(
System
.
currentTimeMillis
()
+
expiresIn
*
1000L
));
}
lemengOAuthTokenRepository
.
save
(
entity
);
tokenHolder
.
setToken
(
systemBookCode
,
accessToken
,
refreshToken
);
}
/**
* 无 expires_in 时持久化(如手动 set)
*/
@Transactional
public
void
saveAndCache
(
String
systemBookCode
,
String
accessToken
,
String
refreshToken
)
{
saveAndCache
(
systemBookCode
,
accessToken
,
refreshToken
,
null
);
}
}
src/main/java/cn/nhsoft/hanni/lemeng/util/TokenHolder.java
View file @
f180b5d0
...
...
@@ -8,8 +8,9 @@ import java.util.Map;
import
java.util.concurrent.ConcurrentHashMap
;
/**
* 内存 Token 存储,供 RestTemplate 方式商品查询及定时刷新使用
* 通过 /lemeng/token/set 设置后,LemonApiClient 从此读取
* 进程内内存 Token 缓存,供 RestTemplate 商品查询及定时刷新使用。
* <p>写入请通过 {@link cn.nhsoft.hanni.lemeng.service.LemengTokenPersistService},以保证与数据库同步;
* 服务启动时会从表 {@code lemeng_oauth_token} 加载到此。</p>
*/
@Component
public
class
TokenHolder
{
...
...
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