Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yd-notice
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
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xingmin
yd-notice
Commits
785bbfc4
Commit
785bbfc4
authored
Oct 12, 2026
by
zhangxingmin
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/dev' into prod
parents
03839f6b
82141c7e
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
975 additions
and
13 deletions
+975
-13
yd-notice-api/src/main/java/com/yd/notice/api/controller/SubscribeRecordController.java
+46
-0
yd-notice-api/src/main/java/com/yd/notice/api/service/ApiSubscribeRecordService.java
+26
-0
yd-notice-api/src/main/java/com/yd/notice/api/service/impl/ApiSubscribeRecordServiceImpl.java
+226
-0
yd-notice-feign/src/main/java/com/yd/notice/feign/dto/SubscribeRecordDTO.java
+34
-0
yd-notice-feign/src/main/java/com/yd/notice/feign/request/SubscribeBatchRequest.java
+20
-0
yd-notice-feign/src/main/java/com/yd/notice/feign/request/SubscribeRemainingRequest.java
+22
-0
yd-notice-feign/src/main/java/com/yd/notice/feign/response/QuerySfpUserResponse.java
+46
-0
yd-notice-feign/src/main/java/com/yd/notice/feign/response/SubscribeRemainingResponse.java
+29
-0
yd-notice-service/pom.xml
+0
-2
yd-notice-service/src/main/java/com/yd/notice/service/dao/SubscribeRecordMapper.java
+49
-0
yd-notice-service/src/main/java/com/yd/notice/service/model/SubscribeRecord.java
+134
-0
yd-notice-service/src/main/java/com/yd/notice/service/send/MiniprogramMessageSender.java
+24
-10
yd-notice-service/src/main/java/com/yd/notice/service/service/ISubscribeRecordService.java
+31
-0
yd-notice-service/src/main/java/com/yd/notice/service/service/impl/SubscribeRecordServiceImpl.java
+58
-0
yd-notice-service/src/main/java/com/yd/notice/service/utils/MyBatisPlusCodeGenerator.java
+1
-1
yd-notice-service/src/main/java/com/yd/notice/service/utils/SfpTokenUtil.java
+224
-0
yd-notice-service/src/main/resources/mappers/SubscribeRecordMapper.xml
+5
-0
No files found.
yd-notice-api/src/main/java/com/yd/notice/api/controller/SubscribeRecordController.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
api
.
controller
;
import
com.yd.common.result.Result
;
import
com.yd.notice.api.service.ApiSubscribeRecordService
;
import
com.yd.notice.feign.request.SubscribeBatchRequest
;
import
com.yd.notice.feign.request.SubscribeRemainingRequest
;
import
com.yd.notice.feign.response.SubscribeRemainingResponse
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.validation.annotation.Validated
;
import
org.springframework.web.bind.annotation.*
;
import
javax.validation.Valid
;
/**
* 小程序订阅记录管理
*
* @author zxm
* @since 2026-04-02
*/
@Slf4j
@RestController
@RequestMapping
(
"/subscribe"
)
@Validated
public
class
SubscribeRecordController
{
@Autowired
private
ApiSubscribeRecordService
apiSubscribeRecordService
;
/**
* 批量保存订阅记录(用户授权后调用)
*/
@PostMapping
(
"/record/batch"
)
public
Result
<
Void
>
batchSave
(
@Valid
@RequestBody
SubscribeBatchRequest
request
)
{
return
apiSubscribeRecordService
.
batchSave
(
request
);
}
/**
* 查询用户对某个微信模板的剩余有效订阅次数
*/
@GetMapping
(
"/remaining"
)
public
Result
<
SubscribeRemainingResponse
>
getRemaining
(
@Valid
SubscribeRemainingRequest
request
)
{
return
apiSubscribeRecordService
.
getRemaining
(
request
);
}
}
\ No newline at end of file
yd-notice-api/src/main/java/com/yd/notice/api/service/ApiSubscribeRecordService.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
api
.
service
;
import
com.yd.common.result.Result
;
import
com.yd.notice.feign.request.SubscribeBatchRequest
;
import
com.yd.notice.feign.request.SubscribeRemainingRequest
;
import
com.yd.notice.feign.response.SubscribeRemainingResponse
;
/**
* 小程序订阅记录 API 服务接口
*
* @author zxm
* @since 2026-04-02
*/
public
interface
ApiSubscribeRecordService
{
/**
* 批量保存订阅记录
*/
Result
<
Void
>
batchSave
(
SubscribeBatchRequest
request
);
/**
* 查询用户对某个微信模板的剩余有效订阅次数
*/
Result
<
SubscribeRemainingResponse
>
getRemaining
(
SubscribeRemainingRequest
request
);
}
\ No newline at end of file
yd-notice-api/src/main/java/com/yd/notice/api/service/impl/ApiSubscribeRecordServiceImpl.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
api
.
service
.
impl
;
import
com.alibaba.fastjson2.JSON
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.yd.common.result.Result
;
import
com.yd.common.result.JsonResult
;
import
com.yd.notice.api.service.ApiSubscribeRecordService
;
import
com.yd.notice.feign.dto.SubscribeRecordDTO
;
import
com.yd.notice.feign.request.SubscribeBatchRequest
;
import
com.yd.notice.feign.request.SubscribeRemainingRequest
;
import
com.yd.notice.feign.response.QuerySfpUserResponse
;
import
com.yd.notice.feign.response.SubscribeRemainingResponse
;
import
com.yd.notice.service.model.NotificationTemplate
;
import
com.yd.notice.service.model.SubscribeRecord
;
import
com.yd.notice.service.service.INotificationTemplateService
;
import
com.yd.notice.service.service.ISubscribeRecordService
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.core.env.Environment
;
import
org.springframework.http.HttpEntity
;
import
org.springframework.http.HttpHeaders
;
import
org.springframework.http.MediaType
;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.stereotype.Service
;
import
org.springframework.util.StringUtils
;
import
org.springframework.web.client.RestTemplate
;
import
org.springframework.web.context.request.RequestContextHolder
;
import
org.springframework.web.context.request.ServletRequestAttributes
;
import
javax.annotation.Resource
;
import
javax.servlet.http.HttpServletRequest
;
import
java.time.LocalDateTime
;
import
java.util.ArrayList
;
import
java.util.HashMap
;
import
java.util.List
;
import
java.util.Map
;
/**
* 小程序订阅记录 API 服务实现类
*
* @author zxm
* @since 2026-04-02
*/
@Slf4j
@Service
public
class
ApiSubscribeRecordServiceImpl
implements
ApiSubscribeRecordService
{
@Autowired
private
ISubscribeRecordService
subscribeRecordService
;
@Autowired
private
INotificationTemplateService
templateService
;
@Autowired
private
RestTemplate
restTemplate
;
@Resource
private
Environment
env
;
/**
* 根据当前环境获取 ajb 服务基础地址
*/
private
String
getAjbBaseUrl
()
{
String
activeProfile
=
env
.
getProperty
(
"spring.profiles.active"
,
"dev"
);
log
.
info
(
"当前环境: {}, 获取 ajb 服务地址"
,
activeProfile
);
if
(
"prod"
.
equalsIgnoreCase
(
activeProfile
)
||
"production"
.
equalsIgnoreCase
(
activeProfile
))
{
// 生产环境 ajb 服务地址(请根据实际部署确认)
return
"https://hoservice.ydhomeoffice.cn/hoserviceApi"
;
}
else
{
// 测试/开发环境
return
"https://mdev.anjibao.cn/sfpApi"
;
}
}
/**
* 从请求头中获取 Token,通过远程解析获取用户的完整信息(包含 openid 和 userId)
*
* @return QuerySfpUserResponse 用户信息
* @throws RuntimeException 如果获取失败
*/
private
QuerySfpUserResponse
getUserInfoFromToken
()
{
// 获取当前请求上下文
ServletRequestAttributes
attributes
=
(
ServletRequestAttributes
)
RequestContextHolder
.
getRequestAttributes
();
if
(
attributes
==
null
)
{
log
.
error
(
"无法获取当前请求上下文"
);
throw
new
RuntimeException
(
"无法获取当前请求上下文"
);
}
HttpServletRequest
request
=
attributes
.
getRequest
();
String
authorization
=
request
.
getHeader
(
"X-Authorization"
);
if
(!
StringUtils
.
hasText
(
authorization
))
{
log
.
error
(
"请求头 X-Authorization 为空"
);
throw
new
RuntimeException
(
"未提供认证 Token"
);
}
// 直接使用原始 authorization 值,不做任何处理,远程接口会自动剥离前缀
String
token
=
authorization
;
log
.
info
(
"调用远程解析 Token 接口,原始 authorization: {}"
,
authorization
);
// 根据环境获取 ajb 服务地址
String
ajbBaseUrl
=
getAjbBaseUrl
();
String
parseUrl
=
ajbBaseUrl
+
"/user/parse/token"
;
HttpHeaders
headers
=
new
HttpHeaders
();
headers
.
setContentType
(
MediaType
.
APPLICATION_JSON
);
Map
<
String
,
String
>
requestBody
=
new
HashMap
<>();
requestBody
.
put
(
"token"
,
token
);
HttpEntity
<
Map
<
String
,
String
>>
entity
=
new
HttpEntity
<>(
requestBody
,
headers
);
log
.
info
(
"调用远程解析 Token 接口, url={}, token前50字符: {}"
,
parseUrl
,
token
.
substring
(
0
,
Math
.
min
(
50
,
token
.
length
()))
+
"..."
);
try
{
ResponseEntity
<
JsonResult
>
responseEntity
=
restTemplate
.
postForEntity
(
parseUrl
,
entity
,
JsonResult
.
class
);
if
(
responseEntity
.
getStatusCode
().
is2xxSuccessful
())
{
JsonResult
body
=
responseEntity
.
getBody
();
if
(
body
!=
null
&&
body
.
isSuccess
())
{
Object
data
=
body
.
getData
();
if
(
data
!=
null
)
{
QuerySfpUserResponse
userInfo
=
JSON
.
parseObject
(
JSON
.
toJSONString
(
data
),
QuerySfpUserResponse
.
class
);
if
(
userInfo
.
getId
()
==
null
)
{
log
.
error
(
"远程解析返回的用户ID为空"
);
throw
new
RuntimeException
(
"用户信息不完整"
);
}
log
.
info
(
"远程解析 Token 成功,用户ID: {}, openid: {}"
,
userInfo
.
getId
(),
userInfo
.
getWxOpenId
());
return
userInfo
;
}
}
}
log
.
error
(
"远程解析 Token 失败, response={}"
,
responseEntity
);
throw
new
RuntimeException
(
"获取用户信息失败"
);
}
catch
(
Exception
e
)
{
log
.
error
(
"调用远程解析 Token 接口异常"
,
e
);
throw
new
RuntimeException
(
"获取用户信息异常: "
+
e
.
getMessage
());
}
}
@Override
public
Result
<
Void
>
batchSave
(
SubscribeBatchRequest
request
)
{
log
.
info
(
"批量保存订阅记录, request={}"
,
JSON
.
toJSONString
(
request
));
List
<
SubscribeRecordDTO
>
dtoList
=
request
.
getRecords
();
if
(
dtoList
==
null
||
dtoList
.
isEmpty
())
{
return
Result
.
fail
(
"订阅记录列表为空"
);
}
// 从 Token 中获取当前用户的完整信息(所有记录共用同一个用户)
QuerySfpUserResponse
userInfo
;
try
{
userInfo
=
getUserInfoFromToken
();
}
catch
(
Exception
e
)
{
log
.
error
(
"获取用户信息失败"
,
e
);
return
Result
.
fail
(
"获取用户信息失败:"
+
e
.
getMessage
());
}
String
openid
=
userInfo
.
getWxOpenId
();
String
userId
=
userInfo
.
getId
().
toString
();
// Long -> String
List
<
SubscribeRecord
>
records
=
new
ArrayList
<>();
for
(
SubscribeRecordDTO
dto
:
dtoList
)
{
// 1. 根据 wxTemplateId 查询模板信息
NotificationTemplate
template
=
templateService
.
getOne
(
new
LambdaQueryWrapper
<
NotificationTemplate
>()
.
eq
(
NotificationTemplate:
:
getExtraTemplate
,
dto
.
getWxTemplateId
())
.
eq
(
NotificationTemplate:
:
getStatus
,
1
)
.
orderByDesc
(
NotificationTemplate:
:
getCreateTime
)
.
last
(
"LIMIT 1"
)
);
if
(
template
==
null
)
{
log
.
error
(
"模板不存在, wxTemplateId={}"
,
dto
.
getWxTemplateId
());
return
Result
.
fail
(
"模板不存在: "
+
dto
.
getWxTemplateId
());
}
// 2. 构建实体对象
SubscribeRecord
record
=
new
SubscribeRecord
();
record
.
setProjectType
(
dto
.
getProjectType
());
record
.
setWxTemplateId
(
dto
.
getWxTemplateId
());
record
.
setOpenid
(
openid
);
record
.
setUserId
(
userId
);
// 设置用户ID
record
.
setTemplateBizId
(
template
.
getTemplateBizId
());
record
.
setChannelBizId
(
template
.
getChannelBizId
());
record
.
setStatus
(
0
);
record
.
setSubscribeTime
(
LocalDateTime
.
now
());
record
.
setExpireTime
(
LocalDateTime
.
now
().
plusDays
(
7
));
records
.
add
(
record
);
}
boolean
success
=
subscribeRecordService
.
batchSave
(
records
);
if
(
success
)
{
log
.
info
(
"批量保存订阅记录成功, 数量={}, 用户ID={}"
,
records
.
size
(),
userId
);
return
Result
.
success
();
}
else
{
log
.
error
(
"批量保存订阅记录失败"
);
return
Result
.
fail
(
"批量保存失败"
);
}
}
@Override
public
Result
<
SubscribeRemainingResponse
>
getRemaining
(
SubscribeRemainingRequest
request
)
{
log
.
info
(
"查询剩余次数, request={}"
,
JSON
.
toJSONString
(
request
));
// 从 Token 中获取当前用户的 openid
String
openid
;
try
{
QuerySfpUserResponse
userInfo
=
getUserInfoFromToken
();
openid
=
userInfo
.
getWxOpenId
();
}
catch
(
Exception
e
)
{
log
.
error
(
"获取用户 openid 失败"
,
e
);
return
Result
.
fail
(
"获取用户信息失败:"
+
e
.
getMessage
());
}
String
wxTemplateId
=
request
.
getWxTemplateId
();
int
count
=
subscribeRecordService
.
countRemainingByWxTemplateId
(
openid
,
wxTemplateId
);
SubscribeRemainingResponse
response
=
new
SubscribeRemainingResponse
();
response
.
setOpenid
(
openid
);
response
.
setWxTemplateId
(
wxTemplateId
);
response
.
setRemainingCount
(
count
);
log
.
info
(
"查询剩余次数结果, count={}"
,
count
);
return
Result
.
success
(
response
);
}
}
\ No newline at end of file
yd-notice-feign/src/main/java/com/yd/notice/feign/dto/SubscribeRecordDTO.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
feign
.
dto
;
import
lombok.Data
;
import
javax.validation.constraints.NotBlank
;
/**
* 小程序订阅记录 DTO
*
* @author zxm
* @since 2026-04-02
*/
@Data
public
class
SubscribeRecordDTO
{
/**
* 项目类型(SFP、CFPP等)
*/
@NotBlank
(
message
=
"项目类型不能为空"
)
private
String
projectType
;
/**
* 微信侧实际模板ID(必填,用于查询模板表)
*/
@NotBlank
(
message
=
"wxTemplateId模板ID不能为空"
)
private
String
wxTemplateId
;
// /**
// * 用户小程序openid(必填)
// */
// @NotBlank(message = "用户小程序openid不能为空")
// private String openid;
}
\ No newline at end of file
yd-notice-feign/src/main/java/com/yd/notice/feign/request/SubscribeBatchRequest.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
feign
.
request
;
import
com.yd.notice.feign.dto.SubscribeRecordDTO
;
import
lombok.Data
;
import
javax.validation.Valid
;
import
javax.validation.constraints.NotEmpty
;
import
java.util.List
;
@Data
public
class
SubscribeBatchRequest
{
/**
* 批量订阅记录列表
*/
@NotEmpty
(
message
=
"订阅记录列表不能为空"
)
@Valid
private
List
<
SubscribeRecordDTO
>
records
;
}
\ No newline at end of file
yd-notice-feign/src/main/java/com/yd/notice/feign/request/SubscribeRemainingRequest.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
feign
.
request
;
import
lombok.Data
;
import
javax.validation.constraints.NotBlank
;
/**
* 查询剩余订阅次数请求
*
* @author zxm
* @since 2026-04-02
*/
@Data
public
class
SubscribeRemainingRequest
{
// @NotBlank(message = "openid不能为空")
// private String openid;
@NotBlank
(
message
=
"wxTemplateId模板ID不能为空"
)
private
String
wxTemplateId
;
}
\ No newline at end of file
yd-notice-feign/src/main/java/com/yd/notice/feign/response/QuerySfpUserResponse.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
feign
.
response
;
import
lombok.Data
;
import
java.util.Date
;
@Data
public
class
QuerySfpUserResponse
{
private
Long
id
;
//serial idPRIauto_increment
private
String
name
;
//userName
private
String
mobile
;
//手机号
private
Integer
gender
;
//性别,1,男,2,女
private
String
age
;
//年龄
private
String
manageMobile
;
//管理经纪人手机号(保单托管)
private
Date
loginDate
;
//登录时间(用于判断该用户是否登录过期 30天, 登录成功更新,退出或者切换账号更新)
private
Integer
loginState
;
//是否登录(0=No, 1=Yes)
private
String
customerSource
;
//客户来源
private
String
level
;
//用户等级
private
String
levelName
;
//用户等级名称
private
Integer
isVip
;
//是否vip
private
Date
vipExpireDate
;
//vip到期时间
private
Date
inactivateDate
;
//激活到期时间
private
Integer
remainingTimes
;
//剩余次数
private
Integer
isMax
;
//是否达到最大购买次数 0-否 1-是
private
Integer
userType
;
//用户类型
private
Long
roleId
;
//用户角色ID
private
String
systemType
;
//系统类型(tvm、sfp、policyManage)
private
String
tenantProjectRoleIds
;
//租户项目角色ID列表
private
String
flag
;
//标识
private
String
remark
;
//备注
private
String
wxNickname
;
//微信昵称
private
String
wxOpenId
;
//用户小程序登录的openid
private
String
wxOpenIdGzh
;
//用户公众号登录的openid
private
Integer
isBlacklist
;
//是否黑名单 0-否 1-是
private
Integer
isActive
;
//是否启用(0=No, 1=Yes)
private
Date
createdAt
;
//创建时间
private
String
createdBy
;
//创建人
private
Date
updatedAt
;
//修改时间
private
String
updatedBy
;
//修改人
/**
* 律家保-法税卡激活接口,请求⽅用户唯⼀识别号 (用于跳转到律家保页面的挂参)
*/
private
String
ljbRefId
;
}
yd-notice-feign/src/main/java/com/yd/notice/feign/response/SubscribeRemainingResponse.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
feign
.
response
;
import
lombok.Data
;
/**
* 查询剩余订阅次数响应
*
* @author zxm
* @since 2026-04-02
*/
@Data
public
class
SubscribeRemainingResponse
{
/**
* 微信用户openid
*/
private
String
openid
;
/**
* 模板ID
*/
private
String
wxTemplateId
;
/**
* 用户对某个微信模板的剩余有效订阅次数
*/
private
Integer
remainingCount
;
}
\ No newline at end of file
yd-notice-service/pom.xml
View file @
785bbfc4
...
@@ -125,6 +125,5 @@
...
@@ -125,6 +125,5 @@
<artifactId>
weixin-java-miniapp
</artifactId>
<artifactId>
weixin-java-miniapp
</artifactId>
<version>
4.6.0
</version>
<version>
4.6.0
</version>
</dependency>
</dependency>
</dependencies>
</dependencies>
</project>
</project>
\ No newline at end of file
yd-notice-service/src/main/java/com/yd/notice/service/dao/SubscribeRecordMapper.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
service
.
dao
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.yd.notice.service.model.SubscribeRecord
;
import
org.apache.ibatis.annotations.Mapper
;
import
org.apache.ibatis.annotations.Param
;
import
org.apache.ibatis.annotations.Select
;
import
org.apache.ibatis.annotations.Update
;
/**
* 订阅记录 Mapper
*
* @author zxm
* @since 2026-04-02
*/
@Mapper
public
interface
SubscribeRecordMapper
extends
BaseMapper
<
SubscribeRecord
>
{
/**
* 统计剩余有效次数(未过期、未消耗)
*/
@Select
(
"SELECT COUNT(*) FROM subscribe_record "
+
"WHERE openid = #{openid} "
+
"AND wx_template_id = #{wxTemplateId} "
+
"AND status = 0 "
+
"AND expire_time > NOW() "
+
"AND is_deleted = 0"
)
int
countRemainingByWxTemplateId
(
@Param
(
"openid"
)
String
openid
,
@Param
(
"wxTemplateId"
)
String
wxTemplateId
);
/**
* 消耗一条有效记录(取最早的一条)
*/
@Update
(
"UPDATE subscribe_record SET status = 1, consume_time = NOW() "
+
"WHERE id = ( "
+
" SELECT id FROM ( "
+
" SELECT id FROM subscribe_record "
+
" WHERE openid = #{openid} "
+
" AND wx_template_id = #{wxTemplateId} "
+
" AND status = 0 "
+
" AND expire_time > NOW() "
+
" AND is_deleted = 0 "
+
" ORDER BY subscribe_time ASC LIMIT 1 "
+
" ) AS tmp "
+
")"
)
int
consumeOneByWxTemplateId
(
@Param
(
"openid"
)
String
openid
,
@Param
(
"wxTemplateId"
)
String
wxTemplateId
);
}
\ No newline at end of file
yd-notice-service/src/main/java/com/yd/notice/service/model/SubscribeRecord.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
service
.
model
;
import
com.baomidou.mybatisplus.annotation.IdType
;
import
com.baomidou.mybatisplus.annotation.TableField
;
import
com.baomidou.mybatisplus.annotation.TableId
;
import
com.baomidou.mybatisplus.annotation.TableName
;
import
java.io.Serializable
;
import
java.time.LocalDateTime
;
import
lombok.Getter
;
import
lombok.Setter
;
/**
* <p>
* 小程序订阅记录表
* </p>
*
* @author zxm
* @since 2026-08-27
*/
@Getter
@Setter
@TableName
(
"subscribe_record"
)
public
class
SubscribeRecord
implements
Serializable
{
private
static
final
long
serialVersionUID
=
1L
;
/**
* 主键ID
*/
@TableId
(
value
=
"id"
,
type
=
IdType
.
AUTO
)
private
Long
id
;
/**
* 项目类型(SFP、CFPP等)
*/
@TableField
(
"project_type"
)
private
String
projectType
;
/**
* 项目唯一业务ID
*/
@TableField
(
"project_biz_id"
)
private
String
projectBizId
;
/**
* 渠道业务ID(关联channel_config)
*/
@TableField
(
"channel_biz_id"
)
private
String
channelBizId
;
/**
* 模板业务ID(关联notification_template)
*/
@TableField
(
"template_biz_id"
)
private
String
templateBizId
;
/**
* 微信侧实际模板ID(extra_template冗余,便于查询)
*/
@TableField
(
"wx_template_id"
)
private
String
wxTemplateId
;
/**
* 用户openid
*/
@TableField
(
"openid"
)
private
String
openid
;
/**
* 用户ID
*/
@TableField
(
"user_id"
)
private
String
userId
;
/**
* 0-有效(未消耗) 1-已消耗
*/
@TableField
(
"status"
)
private
Integer
status
;
/**
* 授权时间
*/
@TableField
(
"subscribe_time"
)
private
LocalDateTime
subscribeTime
;
/**
* 过期时间(订阅时间+7天)
*/
@TableField
(
"expire_time"
)
private
LocalDateTime
expireTime
;
/**
* 消耗时间
*/
@TableField
(
"consume_time"
)
private
LocalDateTime
consumeTime
;
/**
* 通用备注
*/
@TableField
(
"remark"
)
private
String
remark
;
/**
* 删除标识: 0-正常, 1-删除
*/
@TableField
(
"is_deleted"
)
private
Integer
isDeleted
;
/**
* 创建人ID
*/
@TableField
(
"creator_id"
)
private
String
creatorId
;
/**
* 更新人ID
*/
@TableField
(
"updater_id"
)
private
String
updaterId
;
/**
* 创建时间
*/
@TableField
(
"create_time"
)
private
LocalDateTime
createTime
;
/**
* 更新时间
*/
@TableField
(
"update_time"
)
private
LocalDateTime
updateTime
;
}
yd-notice-service/src/main/java/com/yd/notice/service/send/MiniprogramMessageSender.java
View file @
785bbfc4
...
@@ -11,6 +11,7 @@ import com.yd.notice.service.model.NotificationTask;
...
@@ -11,6 +11,7 @@ import com.yd.notice.service.model.NotificationTask;
import
com.yd.notice.service.model.NotificationTemplate
;
import
com.yd.notice.service.model.NotificationTemplate
;
import
com.yd.notice.service.service.IChannelConfigService
;
import
com.yd.notice.service.service.IChannelConfigService
;
import
com.yd.notice.service.service.INotificationTemplateService
;
import
com.yd.notice.service.service.INotificationTemplateService
;
import
com.yd.notice.service.service.ISubscribeRecordService
;
// 新增导入
import
lombok.RequiredArgsConstructor
;
import
lombok.RequiredArgsConstructor
;
import
lombok.extern.slf4j.Slf4j
;
import
lombok.extern.slf4j.Slf4j
;
import
me.chanjar.weixin.common.error.WxErrorException
;
import
me.chanjar.weixin.common.error.WxErrorException
;
...
@@ -27,6 +28,7 @@ public class MiniprogramMessageSender implements MessageSender {
...
@@ -27,6 +28,7 @@ public class MiniprogramMessageSender implements MessageSender {
private
final
IChannelConfigService
channelConfigService
;
private
final
IChannelConfigService
channelConfigService
;
private
final
INotificationTemplateService
templateService
;
private
final
INotificationTemplateService
templateService
;
private
final
ISubscribeRecordService
subscribeRecordService
;
// 新增注入
// 不可重试的错误码(永久失败)
// 不可重试的错误码(永久失败)
private
static
final
String
USER_REFUSE_ERROR
=
"43101"
;
// 用户拒绝接收
private
static
final
String
USER_REFUSE_ERROR
=
"43101"
;
// 用户拒绝接收
...
@@ -60,19 +62,17 @@ public class MiniprogramMessageSender implements MessageSender {
...
@@ -60,19 +62,17 @@ public class MiniprogramMessageSender implements MessageSender {
}
}
// 3. 解析 content_template 并渲染占位符
// 3. 解析 content_template 并渲染占位符
// task.getContent() 已经是渲染后的完整 JSON 字符串(由 TemplateRenderUtil 提前处理)
// 但为了标准化,我们在这里再次解析,顺便提取 page 跳转路径
String
renderedContent
=
task
.
getContent
();
String
renderedContent
=
task
.
getContent
();
JSONObject
contentJson
=
JSONObject
.
parseObject
(
renderedContent
);
JSONObject
contentJson
=
JSONObject
.
parseObject
(
renderedContent
);
log
.
info
(
"进入小程序消息发送器=>消息内容JSON:{}"
,
JSON
.
toJSONString
(
contentJson
));
log
.
info
(
"进入小程序消息发送器=>消息内容JSON:{}"
,
JSON
.
toJSONString
(
contentJson
));
// 提取 page(跳转路径),并从 data 中移除
// 提取 page(跳转路径),并从 data 中移除
String
page
=
contentJson
.
getString
(
"page"
);
String
page
=
contentJson
.
getString
(
"page"
);
log
.
info
(
"进入小程序消息发送器=>page:{}"
,
page
);
log
.
info
(
"进入小程序消息发送器=>page:{}"
,
page
);
contentJson
.
remove
(
"page"
);
contentJson
.
remove
(
"page"
);
log
.
info
(
"进入小程序消息发送器=>移除page:{}"
,
JSON
.
toJSONString
(
contentJson
));
log
.
info
(
"进入小程序消息发送器=>移除page:{}"
,
JSON
.
toJSONString
(
contentJson
));
// 4. 构建微信订阅消息数据结构
(新版本使用 List<MsgData>)
// 4. 构建微信订阅消息数据结构
List
<
WxMaSubscribeMessage
.
MsgData
>
dataList
=
new
ArrayList
<>();
List
<
WxMaSubscribeMessage
.
MsgData
>
dataList
=
new
ArrayList
<>();
for
(
Map
.
Entry
<
String
,
Object
>
entry
:
contentJson
.
entrySet
())
{
for
(
Map
.
Entry
<
String
,
Object
>
entry
:
contentJson
.
entrySet
())
{
String
key
=
entry
.
getKey
();
String
key
=
entry
.
getKey
();
...
@@ -92,25 +92,38 @@ public class MiniprogramMessageSender implements MessageSender {
...
@@ -92,25 +92,38 @@ public class MiniprogramMessageSender implements MessageSender {
.
toUser
(
task
.
getReceiver
())
.
toUser
(
task
.
getReceiver
())
.
templateId
(
wxTemplateId
)
.
templateId
(
wxTemplateId
)
.
data
(
dataList
)
.
data
(
dataList
)
.
page
(
page
)
// 如果 page 为空,builder 也会处理
.
page
(
page
)
.
build
();
.
build
();
// log.info("进入小程序消息发送器=>构建消息体WxMaSubscribeMessage:{}", JSON.toJSONString(message));
// 6. 调用微信 SDK 发送
// 6. 调用微信 SDK 发送
WxMaService
wxMaService
=
buildWxMaService
(
appid
,
secret
);
WxMaService
wxMaService
=
buildWxMaService
(
appid
,
secret
);
wxMaService
.
getMsgService
().
sendSubscribeMsg
(
message
);
wxMaService
.
getMsgService
().
sendSubscribeMsg
(
message
);
log
.
info
(
"微信小程序订阅消息发送成功, taskBizId={}, openid={}"
,
task
.
getTaskBizId
(),
task
.
getReceiver
());
log
.
info
(
"微信小程序订阅消息发送成功, taskBizId={}, openid={}"
,
task
.
getTaskBizId
(),
task
.
getReceiver
());
// ========== 新增:发送成功后消耗一条订阅记录 ==========
try
{
String
openid
=
task
.
getReceiver
();
// 使用从模板中获取的 wxTemplateId
boolean
consumed
=
subscribeRecordService
.
consumeOneByWxTemplateId
(
openid
,
wxTemplateId
);
if
(
consumed
)
{
log
.
info
(
"消耗订阅记录成功, openid={}, wxTemplateId={}"
,
openid
,
wxTemplateId
);
}
else
{
log
.
warn
(
"发送成功但无可用订阅记录可消耗, openid={}, wxTemplateId={}"
,
openid
,
wxTemplateId
);
}
}
catch
(
Exception
e
)
{
// 消耗失败不影响发送结果,仅记录日志
log
.
error
(
"消耗订阅记录异常, openid={}, wxTemplateId={}"
,
task
.
getReceiver
(),
wxTemplateId
,
e
);
}
return
SendResult
.
success
();
return
SendResult
.
success
();
}
catch
(
WxErrorException
e
)
{
}
catch
(
WxErrorException
e
)
{
// 微信 API 抛出的具体异常(含错误码)
Integer
errorCode
=
e
.
getError
().
getErrorCode
();
Integer
errorCode
=
e
.
getError
().
getErrorCode
();
// 正确获取错误码
String
errCode
=
errorCode
!=
null
?
errorCode
.
toString
()
:
"UNKNOWN"
;
String
errCode
=
errorCode
!=
null
?
errorCode
.
toString
()
:
"UNKNOWN"
;
String
errMsg
=
e
.
getMessage
();
String
errMsg
=
e
.
getMessage
();
log
.
info
(
"微信小程序发送失败, taskBizId={}, errCode={}, errMsg={}"
,
task
.
getTaskBizId
(),
errCode
,
errMsg
);
log
.
info
(
"微信小程序发送失败, taskBizId={}, errCode={}, errMsg={}"
,
task
.
getTaskBizId
(),
errCode
,
errMsg
);
// 处理不可重试错误
if
(
USER_REFUSE_ERROR
.
equals
(
errCode
)
||
TEMPLATE_INVALID
.
equals
(
errCode
))
{
if
(
USER_REFUSE_ERROR
.
equals
(
errCode
)
||
TEMPLATE_INVALID
.
equals
(
errCode
))
{
return
SendResult
.
failNonRetryable
(
errCode
,
errMsg
);
return
SendResult
.
failNonRetryable
(
errCode
,
errMsg
);
}
}
...
@@ -133,6 +146,6 @@ public class MiniprogramMessageSender implements MessageSender {
...
@@ -133,6 +146,6 @@ public class MiniprogramMessageSender implements MessageSender {
@Override
@Override
public
String
getSupportedChannelType
()
{
public
String
getSupportedChannelType
()
{
return
"miniprogram"
;
// 必须与 channel_config.channel_type 一致
return
"miniprogram"
;
}
}
}
}
\ No newline at end of file
yd-notice-service/src/main/java/com/yd/notice/service/service/ISubscribeRecordService.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
service
.
service
;
import
com.baomidou.mybatisplus.extension.service.IService
;
import
com.yd.notice.service.model.SubscribeRecord
;
import
java.util.List
;
/**
* 订阅记录服务接口
*
* @author zxm
* @since 2026-04-02
*/
public
interface
ISubscribeRecordService
extends
IService
<
SubscribeRecord
>
{
/**
* 批量保存订阅记录
*/
boolean
batchSave
(
List
<
SubscribeRecord
>
records
);
/**
* 统计剩余有效次数(通过 wxTemplateId)
*/
int
countRemainingByWxTemplateId
(
String
openid
,
String
wxTemplateId
);
/**
* 消耗一条有效记录(通过 wxTemplateId)
*/
boolean
consumeOneByWxTemplateId
(
String
openid
,
String
wxTemplateId
);
}
\ No newline at end of file
yd-notice-service/src/main/java/com/yd/notice/service/service/impl/SubscribeRecordServiceImpl.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
service
.
service
.
impl
;
import
com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
;
import
com.yd.notice.service.dao.SubscribeRecordMapper
;
import
com.yd.notice.service.model.SubscribeRecord
;
import
com.yd.notice.service.service.ISubscribeRecordService
;
import
lombok.RequiredArgsConstructor
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
import
org.springframework.util.StringUtils
;
import
java.util.List
;
/**
* 订阅记录服务实现类
*
* @author zxm
* @since 2026-04-02
*/
@Slf4j
@Service
@RequiredArgsConstructor
public
class
SubscribeRecordServiceImpl
extends
ServiceImpl
<
SubscribeRecordMapper
,
SubscribeRecord
>
implements
ISubscribeRecordService
{
@Override
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
boolean
batchSave
(
List
<
SubscribeRecord
>
records
)
{
if
(
records
==
null
||
records
.
isEmpty
())
{
return
false
;
}
return
saveBatch
(
records
);
}
@Override
public
int
countRemainingByWxTemplateId
(
String
openid
,
String
wxTemplateId
)
{
if
(!
StringUtils
.
hasText
(
openid
)
||
!
StringUtils
.
hasText
(
wxTemplateId
))
{
return
0
;
}
return
baseMapper
.
countRemainingByWxTemplateId
(
openid
,
wxTemplateId
);
}
@Override
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
boolean
consumeOneByWxTemplateId
(
String
openid
,
String
wxTemplateId
)
{
if
(!
StringUtils
.
hasText
(
openid
)
||
!
StringUtils
.
hasText
(
wxTemplateId
))
{
return
false
;
}
int
affected
=
baseMapper
.
consumeOneByWxTemplateId
(
openid
,
wxTemplateId
);
if
(
affected
>
0
)
{
log
.
info
(
"消耗订阅记录成功, openid={}, wxTemplateId={}"
,
openid
,
wxTemplateId
);
}
else
{
log
.
warn
(
"无可消耗的有效订阅记录, openid={}, wxTemplateId={}"
,
openid
,
wxTemplateId
);
}
return
affected
>
0
;
}
}
\ No newline at end of file
yd-notice-service/src/main/java/com/yd/notice/service/utils/MyBatisPlusCodeGenerator.java
View file @
785bbfc4
...
@@ -21,7 +21,7 @@ public class MyBatisPlusCodeGenerator {
...
@@ -21,7 +21,7 @@ public class MyBatisPlusCodeGenerator {
})
})
.
strategyConfig
(
builder
->
{
.
strategyConfig
(
builder
->
{
builder
.
addInclude
(
builder
.
addInclude
(
"
channel_config"
,
"notification_record"
,
"notification_task"
,
"notification_template
"
"
subscribe_record
"
)
)
.
entityBuilder
()
.
entityBuilder
()
.
enableLombok
()
.
enableLombok
()
...
...
yd-notice-service/src/main/java/com/yd/notice/service/utils/SfpTokenUtil.java
0 → 100644
View file @
785bbfc4
package
com
.
yd
.
notice
.
service
.
utils
;
import
io.jsonwebtoken.*
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Service
;
import
javax.annotation.PostConstruct
;
import
java.util.Date
;
import
java.util.HashMap
;
import
java.util.Map
;
/**
* SFP 项目专用 JWT Token 工具类
* 完全在代码内部获取环境标识(System.getProperty),硬编码不同环境的配置
* 支持生成 Token 和解析 Token(获取 userId、customerId 等)
*
* @author yourname
*/
@Slf4j
@Service
public
class
SfpTokenUtil
{
// JWT 标准声明 Key
private
static
final
String
CLAIM_KEY_SUBJECT
=
"sub"
;
private
static
final
String
CLAIM_KEY_CREATED
=
"created"
;
// 自定义声明 Key(与 JwtTokenUtil 保持一致)
private
static
final
String
CLAIM_KEY_USERID
=
"UserId"
;
private
static
final
String
CLAIM_KEY_CUSTOMERID
=
"CustomerId"
;
// 静态配置字段
private
static
String
seal
;
private
static
String
issuer
;
private
static
Long
expiration
;
/**
* 初始化:通过 System.getProperty 获取环境,硬编码对应的配置
* 该方法在 Spring 容器启动完成后自动执行
*/
@PostConstruct
public
void
init
()
{
// 从系统属性中获取 spring.profiles.active,如果获取不到则默认为 "default"
String
activeProfile
=
System
.
getProperty
(
"spring.profiles.active"
,
"default"
);
// 判断是否为生产环境(可自定义判断规则,如包含 "prod" 或 "production")
boolean
isProd
=
"prod"
.
equalsIgnoreCase
(
activeProfile
)
||
"production"
.
equalsIgnoreCase
(
activeProfile
)
||
activeProfile
.
toLowerCase
().
contains
(
"prod"
);
if
(
isProd
)
{
// 生产环境配置(请替换为实际的生产密钥)
seal
=
"SFP2023!@#"
;
issuer
=
"sfpfamilyfinancialplanning"
;
expiration
=
2592000L
;
// 24小时
log
.
info
(
"SfpTokenUtil 加载生产环境配置, seal: {}, issuer: {}, expiration: {} 秒"
,
seal
,
issuer
,
expiration
);
}
else
{
// 测试/开发环境配置(请替换为实际的测试密钥)
seal
=
"zhb123!@#"
;
issuer
=
"zuihuibi"
;
expiration
=
2592000L
;
log
.
info
(
"SfpTokenUtil 加载测试环境配置, seal: {},issuer: {}, expiration: {} 秒"
,
seal
,
issuer
,
expiration
);
}
}
// ======================== 生成 Token ========================
/**
* 生成 JWT Token(自动添加 issuer 前缀,如 "zuihuibi {token}")
*
* @param ticket 用户标识(通常为登录名或唯一标识)
* @param userId 用户 ID(字符串)
* @param customerId 客户 ID(可选)
* @return 完整 Token 字符串(含前缀)
*/
public
static
String
generateToken
(
String
ticket
,
String
userId
,
String
customerId
)
{
Map
<
String
,
Object
>
claims
=
new
HashMap
<>();
claims
.
put
(
CLAIM_KEY_SUBJECT
,
ticket
);
claims
.
put
(
CLAIM_KEY_USERID
,
userId
);
if
(
customerId
!=
null
)
{
claims
.
put
(
CLAIM_KEY_CUSTOMERID
,
customerId
);
}
claims
.
put
(
CLAIM_KEY_CREATED
,
new
Date
());
String
token
=
Jwts
.
builder
()
.
setClaims
(
claims
)
.
setExpiration
(
new
Date
(
System
.
currentTimeMillis
()
+
expiration
*
1000
))
.
signWith
(
SignatureAlgorithm
.
HS512
,
seal
)
.
compact
();
return
issuer
+
" "
+
token
;
}
// ======================== 解析 Token ========================
/**
* 从 Token 中解析 Claims(包含所有声明信息)
*
* @param token 原始 Token(可能包含 issuer 前缀)
* @return Map, key 包含 "resultCode"(SUCCESS/EXPIRED/INVALID)和 "claims"(解析成功时为 Claims 对象)
*/
public
static
Map
<
String
,
Object
>
getClaimsFromToken
(
String
token
)
{
Map
<
String
,
Object
>
map
=
new
HashMap
<>();
String
resultCode
=
"SUCCESS"
;
Claims
claims
=
null
;
log
.
info
(
"SfpTokenUtil.getClaimsFromToken - 原始 token (前50字符): {}"
,
token
!=
null
?
token
.
substring
(
0
,
Math
.
min
(
50
,
token
.
length
()))
+
"..."
:
"null"
);
try
{
String
realToken
=
token
;
if
(
issuer
!=
null
&&
token
.
startsWith
(
issuer
+
" "
))
{
realToken
=
token
.
substring
(
issuer
.
length
()
+
1
);
log
.
info
(
"SfpTokenUtil.getClaimsFromToken - 已剥离前缀 '{} ',realToken 前50字符: {}"
,
issuer
,
realToken
.
substring
(
0
,
Math
.
min
(
50
,
realToken
.
length
()))
+
"..."
);
}
else
{
log
.
info
(
"SfpTokenUtil.getClaimsFromToken - token 不以 '{} ' 开头,未剥离"
,
issuer
);
}
log
.
info
(
"SfpTokenUtil.getClaimsFromToken - 当前使用的 seal 前缀: {}"
,
seal
!=
null
?
seal
.
substring
(
0
,
Math
.
min
(
4
,
seal
.
length
()))
+
"..."
:
"null"
);
Jws
<
Claims
>
jwsClaims
=
Jwts
.
parser
()
.
setSigningKey
(
seal
)
.
parseClaimsJws
(
realToken
);
claims
=
jwsClaims
.
getBody
();
log
.
info
(
"SfpTokenUtil.getClaimsFromToken - 解析成功,claims: {}"
,
claims
);
}
catch
(
ExpiredJwtException
e
)
{
resultCode
=
"EXPIRED"
;
log
.
warn
(
"SfpTokenUtil.getClaimsFromToken - Token 已过期: {}"
,
e
.
getMessage
());
}
catch
(
UnsupportedJwtException
|
MalformedJwtException
|
SignatureException
|
IllegalArgumentException
e
)
{
resultCode
=
"INVALID"
;
log
.
warn
(
"SfpTokenUtil.getClaimsFromToken - Token 无效, 异常类型: {}, 消息: {}"
,
e
.
getClass
().
getSimpleName
(),
e
.
getMessage
());
}
catch
(
Exception
e
)
{
resultCode
=
"INVALID"
;
log
.
error
(
"SfpTokenUtil.getClaimsFromToken - 未知异常: "
,
e
);
}
map
.
put
(
"resultCode"
,
resultCode
);
map
.
put
(
"claims"
,
claims
);
return
map
;
}
/**
* 从 Token 中获取用户 ID(String 形式)
*
* @param token 原始 Token
* @return 用户 ID 字符串,解析失败返回 null
*/
public
static
String
getUserIdFromToken
(
String
token
)
{
Map
<
String
,
Object
>
map
=
getClaimsFromToken
(
token
);
String
resultCode
=
(
String
)
map
.
get
(
"resultCode"
);
if
(
"SUCCESS"
.
equals
(
resultCode
))
{
Claims
claims
=
(
Claims
)
map
.
get
(
"claims"
);
if
(
claims
!=
null
)
{
Object
userId
=
claims
.
get
(
CLAIM_KEY_USERID
);
if
(
userId
!=
null
)
{
log
.
info
(
"SfpTokenUtil.getUserIdFromToken - 成功获取 UserId: {}"
,
userId
);
return
userId
.
toString
();
}
else
{
log
.
warn
(
"SfpTokenUtil.getUserIdFromToken - Claims 中缺少 '{}' 字段"
,
CLAIM_KEY_USERID
);
}
}
else
{
log
.
warn
(
"SfpTokenUtil.getUserIdFromToken - Claims 为空"
);
}
}
else
{
log
.
warn
(
"SfpTokenUtil.getUserIdFromToken - 解析失败,resultCode: {}"
,
resultCode
);
}
return
null
;
}
/**
* 从 Token 中获取客户 ID
*
* @param token 原始 Token
* @return 客户 ID 字符串,解析失败返回 null
*/
public
static
String
getCustomerIdFromToken
(
String
token
)
{
Map
<
String
,
Object
>
map
=
getClaimsFromToken
(
token
);
if
(
"SUCCESS"
.
equals
(
map
.
get
(
"resultCode"
)))
{
Claims
claims
=
(
Claims
)
map
.
get
(
"claims"
);
if
(
claims
!=
null
)
{
Object
customerId
=
claims
.
get
(
CLAIM_KEY_CUSTOMERID
);
return
customerId
!=
null
?
customerId
.
toString
()
:
null
;
}
}
return
null
;
}
/**
* 验证 Token 是否有效(未过期、格式正确)
*
* @param token 原始 Token
* @return true 表示有效,false 表示无效或过期
*/
public
static
boolean
validateToken
(
String
token
)
{
Map
<
String
,
Object
>
map
=
getClaimsFromToken
(
token
);
return
"SUCCESS"
.
equals
(
map
.
get
(
"resultCode"
));
}
/**
* 检查 Token 是否过期
*
* @param token 原始 Token
* @return true 表示已过期,false 表示未过期或解析失败
*/
public
static
boolean
isTokenExpired
(
String
token
)
{
Map
<
String
,
Object
>
map
=
getClaimsFromToken
(
token
);
String
resultCode
=
(
String
)
map
.
get
(
"resultCode"
);
if
(
"EXPIRED"
.
equals
(
resultCode
))
{
return
true
;
}
if
(
"SUCCESS"
.
equals
(
resultCode
))
{
Claims
claims
=
(
Claims
)
map
.
get
(
"claims"
);
if
(
claims
!=
null
)
{
Date
expirationDate
=
claims
.
getExpiration
();
return
expirationDate
!=
null
&&
expirationDate
.
before
(
new
Date
());
}
}
return
false
;
}
}
\ No newline at end of file
yd-notice-service/src/main/resources/mappers/SubscribeRecordMapper.xml
0 → 100644
View file @
785bbfc4
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper
namespace=
"com.yd.notice.service.dao.SubscribeRecordMapper"
>
</mapper>
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