Compare commits

...

12 Commits

Author SHA1 Message Date
zk 1c248f8e48 微信支付回调 2026-06-17 10:16:07 +08:00
zk 231aef7d74 修改offerpai微信支付配置 2026-06-16 21:39:36 +08:00
zk 79309ffa28 微信支付账号配置测试 2026-06-16 21:08:32 +08:00
zk a798c82835 微信支付账号配置测试 2026-06-16 21:05:55 +08:00
zk 310d99cfc3 AI岗位推荐,修改候选岗位池2000 2026-06-05 15:06:59 +08:00
zk f318e44481 短信相关逻辑,仅仅接口状态标记 2026-06-04 17:22:41 +08:00
zk e3ed3e94d4 添加 招聘分类 字段 2026-06-04 15:58:07 +08:00
zk 4e2d9009c1 修海字段备注 2026-06-04 10:55:29 +08:00
zk e4384b003f 岗位表 修改查询相关字段 2026-06-04 10:45:33 +08:00
zk 28179e2a4d 更新发送短信逻辑 2026-06-03 10:20:18 +08:00
zk f9af836285 添加哈希字段 2026-06-02 17:59:52 +08:00
zk 98a838295d 放开取当前用户有效路由菜单( 2026-06-02 17:19:12 +08:00
22 changed files with 180 additions and 81 deletions
@@ -26,8 +26,8 @@ public class LoginController {
private LoginService loginService;
@PostMapping("/sms/sendCode")
public boolean sendCode(@RequestParam @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") String mobileNumber) {
return loginService.sendCode(mobileNumber);
public void sendCode(@RequestParam @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") String mobileNumber) {
loginService.sendCode(mobileNumber);
}
@PostMapping("/login/smsLogin")
@@ -1,6 +1,7 @@
package org.jiayunet.controller;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jiayunet.pojo.vo.RouteMenuVo;
import org.jiayunet.service.RouteMenuService;
import org.jiayunet.tool.UserSecurityTool;
@@ -15,6 +16,7 @@ import java.util.List;
*
* @author zk
*/
@Slf4j
@RestController
@RequestMapping("/route")
@AllArgsConstructor
@@ -24,10 +26,16 @@ public class RouteMenuController {
/**
* 获取当前用户有效路由菜单(树形结构)
* <p>支持未登录访问,未登录时返回全量菜单但仅openAccess标记为可用</p>
*/
@GetMapping("/menu")
public List<RouteMenuVo> getUserRoutes() {
Long userId = UserSecurityTool.getUserId();
Long userId = 0L;
try {
userId = UserSecurityTool.getUserId();
} catch (Exception e) {
log.debug("未登录用户访问路由菜单");
}
return routeMenuService.getUserRoutes(userId);
}
}
@@ -2,6 +2,7 @@ package org.jiayunet.pojo.dto.job;
import lombok.Data;
import java.time.Instant;
import java.util.List;
/**
@@ -41,6 +42,15 @@ public class JobDetailDto {
/** 要求的行业经验名称 */
private String requiredIndustryName;
/** 发布日期 */
private Instant expireAt;
/** 脚本名字 */
private String pyname;
/** 招聘分类 0=社招 1=校招 2=实习 3=其他 */
private Integer recruitCategory;
// ========== 公司信息 ==========
/** 公司ID */
private Long companyId;
@@ -2,6 +2,7 @@ package org.jiayunet.pojo.dto.job;
import lombok.Data;
import java.time.Instant;
import java.util.List;
/**
@@ -54,6 +55,15 @@ public class JobDto {
/** 岗位状态(0=有效 1=已下架 2=已过期) */
private Integer status;
/** 发布日期 */
private Instant expireAt;
/** 脚本名字 */
private String pyname;
/** 招聘分类 0=社招 1=校招 2=实习 3=其他 */
private Integer recruitCategory;
/** 匹配总分(0-100 */
private Integer matchScore;
@@ -30,4 +30,9 @@ public class JobIntentionDto {
* 就业类型:0=全职,1=实习
*/
private Integer employmentType;
/**
* 招聘分类: 0=社招, 1=校招, 2=实习, 3=其他
*/
private Integer recruitCategory;
}
@@ -30,4 +30,9 @@ public class JobIntentionParam {
* 就业类型:0=全职,1=实习
*/
private Integer employmentType;
/**
* 招聘分类: 0=社招, 1=校招, 2=实习, 3=其他
*/
private Integer recruitCategory;
}
@@ -33,6 +33,9 @@ public class JobQueryParam extends PageParam {
/** 排除岗位ID列表(用于推荐时排除已推荐过的) */
private List<Long> excludeJobIds;
/** 招聘分类 0=社招 1=校招 2=实习 3=其他 */
private Integer recruitCategory;
/** 岗位状态过滤(0=有效 1=已下架 2=已过期,可多选,null或空=查所有) */
private List<Integer> statusFilter;
}
@@ -55,6 +55,7 @@ public class JobIntentionService {
po.setRegionCodes(param.getRegionCodes() != null ? param.getRegionCodes() : java.util.Collections.emptyList());
po.setIndustryIds(param.getIndustryIds() != null ? param.getIndustryIds() : java.util.Collections.emptyList());
po.setEmploymentType(param.getEmploymentType());
po.setRecruitCategory(param.getRecruitCategory());
po.setCreateTime(Instant.now());
po.setUpdateTime(Instant.now());
intentionMapper.insert(po);
@@ -63,6 +64,7 @@ public class JobIntentionService {
po.setRegionCodes(param.getRegionCodes() != null ? param.getRegionCodes() : java.util.Collections.emptyList());
po.setIndustryIds(param.getIndustryIds() != null ? param.getIndustryIds() : java.util.Collections.emptyList());
po.setEmploymentType(param.getEmploymentType());
po.setRecruitCategory(param.getRecruitCategory());
po.setUpdateTime(Instant.now());
intentionMapper.updateById(po);
}
@@ -112,7 +112,7 @@ public class JobService {
List<UserJobDislike> dislikes = userJobDislikeMapper.selectList(new LambdaQueryWrapper<UserJobDislike>().eq(UserJobDislike::getUserId, userId));
// 3. 提取排除列表
List<Long> excludeJobIds = new ArrayList<>(dislikes.stream().map(UserJobDislike::getJobId).filter(Objects::nonNull).distinct().collect(Collectors.toList()));
List<Long> excludeJobIds = dislikes.stream().map(UserJobDislike::getJobId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (param.getExcludeJobIds() != null && !param.getExcludeJobIds().isEmpty()) {
excludeJobIds.addAll(param.getExcludeJobIds());
}
@@ -127,7 +127,7 @@ public class JobService {
// 6. 执行分页查询
Page<JobListItemVo> pageParam = param.toPage();
pageParam.setSearchCount(false);
Page<JobListItemVo> page = jobMapper.selectJobPage(pageParam, param.getJobIds(),param.getStatusFilter(), param.getKeyword(), expandedRegionCodes, expandedCategoryIds, expandedIndustryIds, param.getEmploymentType(), excludeJobIds, excludeCompanyIds, excludeRegionCodes, excludeIndustryIds);
Page<JobListItemVo> page = jobMapper.selectJobPage(pageParam, param.getJobIds(),param.getStatusFilter(), param.getKeyword(), expandedRegionCodes, expandedCategoryIds, expandedIndustryIds, param.getEmploymentType(), excludeJobIds, excludeCompanyIds, excludeRegionCodes, excludeIndustryIds, param.getRecruitCategory());
// 7. 查询收藏状态
List<Long> jobIds = page.getRecords().stream().map(JobListItemVo::getId).collect(Collectors.toList());
@@ -292,6 +292,9 @@ public class JobService {
dto.setTags(job.getTags());
dto.setSkillTags(job.getSkillTags());
dto.setSourceUrl(job.getSourceUrl());
dto.setExpireAt(job.getExpireAt());
dto.setPyname(job.getPyname());
dto.setRecruitCategory(job.getRecruitCategory());
dto.setCategoryName(categoryName);
dto.setRequiredIndustryName(requiredIndustryName);
dto.setCompanyId(company.getId());
@@ -611,7 +614,7 @@ public class JobService {
/**
* 求职助手岗位推荐
* <p>1. 查求职意向构造筛选条件(无意向则不设条件) 2. 排除已推荐的+已投递的 3. 取前35条候选 4. AI精筛返回8-10个</p>
* <p>1. 查求职意向构造筛选条件(无意向则不设条件) 2. 排除已推荐的+已投递的 3. 取2000条候选 4. 随机取100条 5. AI精筛返回8-10个</p>
*/
public JobAgentRecommendDto recommendJobs(JobAgentRecommendParam param, Long userId) {
StopWatch sw = new StopWatch("recommendJobs");
@@ -624,12 +627,13 @@ public class JobService {
// 2. 构造查询参数
JobQueryParam queryParam = new JobQueryParam();
queryParam.setPageNum(1);
queryParam.setPageSize(100);
queryParam.setPageSize(2000);
if (intention != null) {
queryParam.setCategoryIds(intention.getCategoryIds());
queryParam.setRegionCodes(intention.getRegionCodes());
queryParam.setIndustryIds(intention.getIndustryIds());
queryParam.setEmploymentType(intention.getEmploymentType());
queryParam.setRecruitCategory(intention.getRecruitCategory());
}
// 3. 合并排除列表:入参传的 + 已投递/待投递的
@@ -643,7 +647,7 @@ public class JobService {
applications.forEach(a -> excludeIds.add(a.getJobId()));
queryParam.setExcludeJobIds(excludeIds);
// 4. 查询候选岗位(完整列表数据
// 4. 查询候选池(2000条
sw.start("查候选岗位");
PageResult<JobDto> candidates = listJobs(queryParam, userId);
sw.stop();
@@ -654,27 +658,34 @@ public class JobService {
return dto;
}
// 5. 批量查岗位详情(title + description + requirement
// 5. 随机取100条作为AI候选
List<JobDto> candidateList = new ArrayList<>(candidates.getList());
Collections.shuffle(candidateList);
if (candidateList.size() > 100) {
candidateList = candidateList.subList(0, 100);
}
// 6. 批量查岗位详情(title + description + requirement
sw.start("查岗位详情");
List<Long> candidateJobIds = candidates.getList().stream().map(JobDto::getId).collect(Collectors.toList());
List<Long> candidateJobIds = candidateList.stream().map(JobDto::getId).collect(Collectors.toList());
List<Job> jobDetails = jobMapper.selectList(new LambdaQueryWrapper<Job>().in(Job::getId, candidateJobIds).select(Job::getId, Job::getTitle, Job::getDescription, Job::getRequirement));
Map<Long, Job> jobDetailMap = jobDetails.stream().collect(Collectors.toMap(Job::getId, j -> j));
sw.stop();
// 6. 构造别名映射(短序号 → 真实ID)和候选岗位映射
// 7. 构造别名映射(短序号 → 真实ID)和候选岗位映射
Map<Integer, Long> aliasToRealId = new HashMap<>();
Map<Long, JobDto> candidateMap = new HashMap<>();
int seq = 1;
for (JobDto dto : candidates.getList()) {
for (JobDto dto : candidateList) {
aliasToRealId.put(seq, dto.getId());
candidateMap.put(dto.getId(), dto);
seq++;
}
// 7. 构造AI输入(用短序号替代19位雪花ID,减少token消耗)
// 8. 构造AI输入(用短序号替代19位雪花ID,减少token消耗)
StringBuilder jobInfo = new StringBuilder();
seq = 1;
for (JobDto dto : candidates.getList()) {
for (JobDto dto : candidateList) {
Job detail = jobDetailMap.get(dto.getId());
jobInfo.append("ID:").append(seq++)
.append("\n标题:").append(dto.getTitle())
@@ -692,7 +703,7 @@ public class JobService {
"只返回JSON,不要其他内容。";
String userMessage = "【用户偏好(仅供参考)】\n" + preferenceInfo + "\n\n【候选岗位】\n" + jobInfo;
// 8. 调用AI
// 9. 调用AI
sw.start("AI调用");
String aiResponse = aiChatAbility.chat("job-recommend", systemPrompt, userMessage);
String json = AiResponseCleanTool.clean(aiResponse);
@@ -700,7 +711,7 @@ public class JobService {
log.info("recommendJobs耗时统计:\n{}", sw.prettyPrint());
// 9. 解析AI返回,别名映射回真实ID,过滤出选中的岗位
// 10. 解析AI返回,别名映射回真实ID,过滤出选中的岗位
try {
JsonNode root = HttpTool.objectMapper.readTree(json);
String summary = root.path("summary").asText("为你精选了合适的岗位");
@@ -66,16 +66,16 @@ public class LoginService {
/**
* 发送短信验证码
* <p>1. 生成6位随机验证码 2. 委托SmsService发送</p>
* <p>1. 生成6位随机验证码 2. 委托SmsService发送,失败抛异常</p>
*/
public boolean sendCode(String mobileNumber) {
public void sendCode(String mobileNumber) {
Assert.hasText(mobileNumber, "手机号不能为空");
UniversalSmsVariable variable = new UniversalSmsVariable();
String code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000));
variable.setCode(code);
return smsService.send(mobileNumber, SmsTemplateEnum.UNIVERSAL, variable);
smsService.send(mobileNumber, SmsTemplateEnum.UNIVERSAL, variable);
}
/**
@@ -49,7 +49,7 @@ public class MemberProductService {
@Autowired
private MessageService messageService;
//@Autowired
@Autowired
private WxNativePayAbility wxNativePayAbility;
@Autowired
@@ -151,6 +151,7 @@ public class MemberProductService {
* 微信Native支付下单
*/
private String prepayWechat(String orderNo, MemberProduct product) {
PayWechatFlow flow = new PayWechatFlow();
flow.setOrderType("member");
flow.setOrderNo(orderNo);
@@ -22,7 +22,7 @@ import java.time.Instant;
*
* @author zk
*/
//@Service
@Service
@Primary
@Slf4j
@AllArgsConstructor
@@ -23,14 +23,14 @@ email:
# 微信支付
wx_pay:
# status close:关闭 open:打开
status: close
app_id: your_app_id
merchant_id: ${MERCHANT_ID:your_merchant_id}
private_key: ${WX_PRIVATE_KEY:your_private_key}
merchant_serial_number: ${MERCHANT_SERIAL_NUMBER:your_merchant_serial_number}
api_v3_key: ${API_V3_KEY:your_api_v3_key}
public_key: ${WX_PUBLIC_KEY:your_public_key}
public_key_id: ${PUBLIC_KEY_ID:your_public_key_id}
status: open
app_id: wx12cee9ea7c6a2069
merchant_id: ${MERCHANT_ID:1374348102}
private_key: ${WX_PRIVATE_KEY:MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQcL1GGv5kr4KVacUwUAZzz7Zh2wsn5rJZyTm8MiRlR3P/3kkiZqT4gHkyhLjfS80LJh4vm5mslY2N4ZeJR0KNmRX4Zl17AzIcxpfp1790Dsbc6YBJw5UCaalJRmf8Wg4Dy9VfcUpOTwfxNz2QsiL18mtrEk69oF/7V/E1rlI1zYRoi+zei12LftYnBATKIW3/yqKZpCmPHHjG2l5wmrUDlOfEZhKaFc4wTyXJOum6kGwBFv+QxsYH7qDaTfUFGqww1n0wX5ItTDqUN/ETNgANkRMp3TUyd65ch1eWUA26Hy693e+GZ6wdSzam7xl8m6nsiVM143NBrW1PNvEZ5mFhAgMBAAECggEAF5/vk/aTRs4tusmWM/eeA5pTTZwKE6euJ0cLaC02utPybdm/6EpCAHhvRC6O1aA9yTm0yiiUrl71AFPFXSXagToMp27SwZKHJ4PKzBJL/Nd/6qH8jF/Cj9dx/IAiYRPD6uI7eOnaRi3m9IwSSkgmdkHXsvcMPanjtxUsnaz4XGiOLN9JjrAM/vk4lrgzQaDqagxPkM1+mXuFYB6RJsd8IS2tOBwUIVGkadSLZcqtEJw29Bku/QtsJiBE/Xk7kCY+Le8Jrtw2QH0BthbfZuKQvGChulK6BBqaVaEZDohO82rn3Fovqcag2V0za1FoJa1sAHKkQMfTajQZiWHNFf/IwQKBgQD/VcPNfqPG7ilPxFOaEQoV4iACA6WxEWwiqixsouW2WZ4iXJZd4OczbCAqkd9QjJ3CZtZJMS8Zmyx9W1o26Tre/y9v4A0vYmb8ldSsxoCIbTh4Un4+tT9KeXhznfV+bOq7eqZNY8UJ8pEw99WdriOLS0EtoyChiN48vIVT08k8HQKBgQDQ+7WQ5okHjYrAs3Is5nCshRpfnfQDn6qV87ztWIG/6o54AZ1hRZjVGRH9NB2aK0jWiHbr7ELZufGXgt+UbbLq9wV/QYF+nZDvlZi0LcLaOvlpP7Z9fcAYVVxpSlTbRSUsSrqYwyjQw0NwOrhQIzJxsXUzp5MdMo23e9UYqZ3PFQKBgQDUcYd4cuXRnlbBB5iHl6XMj1gSVPaHXPeb3/sWaHK4RdhMvrxdX8L+Bfi9cqFbY6PfG7EvNLz7kSBzLI7jISraX3gYnVJbGoSVpBRPrNlEqZtSgVCI3ETMskSF7edUSUsEsgesXFaU8D3dgFc72qe4PeT9E70Sqc4+upw0IwJMAQKBgDQD8pU2TDUNDdQY1fpHARRvrLjP38RnIVZhbEzHEsRp5TwJatqjL/8aVlfneNa+n0qGfNML4ze+CFvlvzB6fWXuRff+nfSd001Otsth+HXI5/tCWHGzsvRbirzKO4S4GSCFGRyctrP/ZfGxK98GY6/Ys4s/0mYaBxv8PnX9AcJhAoGAF6AWpeB1+IlNj+M2FZAZigGBsIsS4PMTOp5oBHqjh4RpPJa8haESaNMDbAkLIlBwWsqzchQQOvVap8hv9ft8cyoQ/995xADQHJTSn5wkZzpDXQiNPwRoxmd852Kln0WQ88XmbbgZfeYMlDN3ovA3JC3NNBZRflAmVYAQrKsF+fM=}
merchant_serial_number: ${MERCHANT_SERIAL_NUMBER:50425CBE34C658C6E9A2F4F9DD0E85DC5F00DDD9}
api_v3_key: ${API_V3_KEY:Youlibao201508Youlibao2015082022}
public_key: ${WX_PUBLIC_KEY:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw7q/wZEL3ecfOGD9DfhEEoSS8TsLm+PLqHe5YbYfuRsk+Umdv7zfpncPcXa7xjXCiy9Y5bSFH0U6qnz0IChkl8hbYcwNQ6WWDFfSuYBsglPZcbDbDCk8cMRSmbusHKBfKTSJS5JBzaD3G2d7b9QkTOejOJNb4eYC7JkjTC/cPiWZztFtDP3tIismqvg6UHPw4AkHc+YX2yPF+e6sVlsPbn2xwzKZW+NU+JkQTi2uUeC1icSeF0uDVxvK3SZMQqkhw75h8zBhAZ+F9B30u4ExYBJtIMubaruSUERUFLb2JiQK4tECqX9lJa43nfXP3pHw6tDJicoA4yJ5AypkWI2+xwIDAQAB}
public_key_id: ${PUBLIC_KEY_ID:PUB_KEY_ID_0113743481022025060400112207001600}
notify_domain: ${API_NOTIFY_DOMAIN:http://127.0.0.1:8080/api}
# 支付宝支付
@@ -79,7 +79,7 @@ app:
#开放接口
ignore:
urls: "/public/**,/job/list,/job/detail"
urls: "/public/**,/job/list,/job/detail,/route/menu"
# 简历配置
resume:
@@ -23,15 +23,15 @@ email:
# 微信支付
wx_pay:
# status close:关闭 open:打开
status: close
app_id: your_app_id
merchant_id: ${MERCHANT_ID:your_merchant_id}
private_key: ${WX_PRIVATE_KEY:your_private_key}
merchant_serial_number: ${MERCHANT_SERIAL_NUMBER:your_merchant_serial_number}
api_v3_key: ${API_V3_KEY:your_api_v3_key}
public_key: ${WX_PUBLIC_KEY:your_public_key}
public_key_id: ${PUBLIC_KEY_ID:your_public_key_id}
notify_domain: ${API_NOTIFY_DOMAIN:http://127.0.0.1:8080/api}
status: open
app_id: wx12cee9ea7c6a2069
merchant_id: ${MERCHANT_ID:1374348102}
private_key: ${WX_PRIVATE_KEY:MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQcL1GGv5kr4KVacUwUAZzz7Zh2wsn5rJZyTm8MiRlR3P/3kkiZqT4gHkyhLjfS80LJh4vm5mslY2N4ZeJR0KNmRX4Zl17AzIcxpfp1790Dsbc6YBJw5UCaalJRmf8Wg4Dy9VfcUpOTwfxNz2QsiL18mtrEk69oF/7V/E1rlI1zYRoi+zei12LftYnBATKIW3/yqKZpCmPHHjG2l5wmrUDlOfEZhKaFc4wTyXJOum6kGwBFv+QxsYH7qDaTfUFGqww1n0wX5ItTDqUN/ETNgANkRMp3TUyd65ch1eWUA26Hy693e+GZ6wdSzam7xl8m6nsiVM143NBrW1PNvEZ5mFhAgMBAAECggEAF5/vk/aTRs4tusmWM/eeA5pTTZwKE6euJ0cLaC02utPybdm/6EpCAHhvRC6O1aA9yTm0yiiUrl71AFPFXSXagToMp27SwZKHJ4PKzBJL/Nd/6qH8jF/Cj9dx/IAiYRPD6uI7eOnaRi3m9IwSSkgmdkHXsvcMPanjtxUsnaz4XGiOLN9JjrAM/vk4lrgzQaDqagxPkM1+mXuFYB6RJsd8IS2tOBwUIVGkadSLZcqtEJw29Bku/QtsJiBE/Xk7kCY+Le8Jrtw2QH0BthbfZuKQvGChulK6BBqaVaEZDohO82rn3Fovqcag2V0za1FoJa1sAHKkQMfTajQZiWHNFf/IwQKBgQD/VcPNfqPG7ilPxFOaEQoV4iACA6WxEWwiqixsouW2WZ4iXJZd4OczbCAqkd9QjJ3CZtZJMS8Zmyx9W1o26Tre/y9v4A0vYmb8ldSsxoCIbTh4Un4+tT9KeXhznfV+bOq7eqZNY8UJ8pEw99WdriOLS0EtoyChiN48vIVT08k8HQKBgQDQ+7WQ5okHjYrAs3Is5nCshRpfnfQDn6qV87ztWIG/6o54AZ1hRZjVGRH9NB2aK0jWiHbr7ELZufGXgt+UbbLq9wV/QYF+nZDvlZi0LcLaOvlpP7Z9fcAYVVxpSlTbRSUsSrqYwyjQw0NwOrhQIzJxsXUzp5MdMo23e9UYqZ3PFQKBgQDUcYd4cuXRnlbBB5iHl6XMj1gSVPaHXPeb3/sWaHK4RdhMvrxdX8L+Bfi9cqFbY6PfG7EvNLz7kSBzLI7jISraX3gYnVJbGoSVpBRPrNlEqZtSgVCI3ETMskSF7edUSUsEsgesXFaU8D3dgFc72qe4PeT9E70Sqc4+upw0IwJMAQKBgDQD8pU2TDUNDdQY1fpHARRvrLjP38RnIVZhbEzHEsRp5TwJatqjL/8aVlfneNa+n0qGfNML4ze+CFvlvzB6fWXuRff+nfSd001Otsth+HXI5/tCWHGzsvRbirzKO4S4GSCFGRyctrP/ZfGxK98GY6/Ys4s/0mYaBxv8PnX9AcJhAoGAF6AWpeB1+IlNj+M2FZAZigGBsIsS4PMTOp5oBHqjh4RpPJa8haESaNMDbAkLIlBwWsqzchQQOvVap8hv9ft8cyoQ/995xADQHJTSn5wkZzpDXQiNPwRoxmd852Kln0WQ88XmbbgZfeYMlDN3ovA3JC3NNBZRflAmVYAQrKsF+fM=}
merchant_serial_number: ${MERCHANT_SERIAL_NUMBER:50425CBE34C658C6E9A2F4F9DD0E85DC5F00DDD9}
api_v3_key: ${API_V3_KEY:Youlibao201508Youlibao2015082022}
public_key: ${WX_PUBLIC_KEY:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw7q/wZEL3ecfOGD9DfhEEoSS8TsLm+PLqHe5YbYfuRsk+Umdv7zfpncPcXa7xjXCiy9Y5bSFH0U6qnz0IChkl8hbYcwNQ6WWDFfSuYBsglPZcbDbDCk8cMRSmbusHKBfKTSJS5JBzaD3G2d7b9QkTOejOJNb4eYC7JkjTC/cPiWZztFtDP3tIismqvg6UHPw4AkHc+YX2yPF+e6sVlsPbn2xwzKZW+NU+JkQTi2uUeC1icSeF0uDVxvK3SZMQqkhw75h8zBhAZ+F9B30u4ExYBJtIMubaruSUERUFLb2JiQK4tECqX9lJa43nfXP3pHw6tDJicoA4yJ5AypkWI2+xwIDAQAB}
public_key_id: ${PUBLIC_KEY_ID:PUB_KEY_ID_0113743481022025060400112207001600}
notify_domain: ${API_NOTIFY_DOMAIN:http://8.148.237.97:10202/api}
# 支付宝支付
alipay:
@@ -79,7 +79,7 @@ app:
#开放接口
ignore:
urls: "/public/**,/job/list,/job/detail"
urls: "/public/**,/job/list,/job/detail,/route/menu"
# 简历配置
resume:
@@ -79,7 +79,7 @@ app:
#开放接口
ignore:
urls: "/public/**,/job/list,/job/detail"
urls: "/public/**,/job/list,/job/detail,/route/menu"
# 简历配置
resume:
@@ -15,7 +15,7 @@ public enum SmsTemplateEnum {
/**
* 通用验证码
*/
UNIVERSAL("油梨科技", "SMS_501981064", 5, UniversalSmsVariable.class);
UNIVERSAL("油梨科技", "SMS_501981064", 5,1, UniversalSmsVariable.class);
/**
* 模板签名
@@ -34,6 +34,13 @@ public enum SmsTemplateEnum {
*/
private final Integer effectiveTime;
/**
* 重发限制时间,单位分钟。发送后需间隔该时间才允许重发。
* NULL 或 <= 0:不限制重复发送
* > 0:发送后需等待指定分钟后才可重新发送
*/
private final Integer retryTime;
/**
* 短信参数对象类
*/
@@ -19,5 +19,5 @@ public interface JobMapper extends CommonMapper<Job> {
/**
* 分页查询岗位列表
*/
Page<JobListItemVo> selectJobPage(Page<JobListItemVo> page, @Param("jobIds") List<Long> jobIds, @Param("statusFilter") List<Integer> statusFilter, @Param("keyword") String keyword, @Param("regionCodes") List<String> regionCodes, @Param("categoryIds") List<Long> categoryIds, @Param("industryIds") List<Long> industryIds, @Param("employmentType") Integer employmentType, @Param("excludeJobIds") List<Long> excludeJobIds, @Param("excludeCompanyIds") List<Long> excludeCompanyIds, @Param("excludeRegionCodes") List<String> excludeRegionCodes, @Param("excludeIndustryIds") List<Long> excludeIndustryIds);
Page<JobListItemVo> selectJobPage(Page<JobListItemVo> page, @Param("jobIds") List<Long> jobIds, @Param("statusFilter") List<Integer> statusFilter, @Param("keyword") String keyword, @Param("regionCodes") List<String> regionCodes, @Param("categoryIds") List<Long> categoryIds, @Param("industryIds") List<Long> industryIds, @Param("employmentType") Integer employmentType, @Param("excludeJobIds") List<Long> excludeJobIds, @Param("excludeCompanyIds") List<Long> excludeCompanyIds, @Param("excludeRegionCodes") List<String> excludeRegionCodes, @Param("excludeIndustryIds") List<Long> excludeIndustryIds, @Param("recruitCategory") Integer recruitCategory);
}
@@ -77,12 +77,18 @@ public class Job {
/** 爬虫原始数据ID,用于去重 */
private String sourceId;
/** 招聘分类 0=校招 1=实习 2=社招 3=其他 */
/** 内容哈希,用于去重续命 */
private String contentHash;
/** 招聘分类 0=社招 1=校招 2=实习 3=其他 */
private Integer recruitCategory;
/** 发布日期 */
private Instant expireAt;
/** 脚本名字 */
private String pyname;
/** 状态 0=上架 1=下架 2=已失效 */
private Integer status;
@@ -41,6 +41,9 @@ public class UserJobIntention {
/** 工作类型 0=全职 1=实习 */
private Integer employmentType;
/** 招聘分类: 0=社招, 1=校招, 2=实习, 3=其他 */
private Integer recruitCategory;
/** 创建时间 */
private Instant createTime;
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import org.jiayunet.config.LongListTypeHandler;
import java.time.Instant;
import java.util.List;
/**
@@ -73,4 +74,13 @@ public class JobListItemVo {
/** 岗位状态(0=有效 1=已下架 2=已过期) */
private Integer status;
/** 发布日期 */
private Instant expireAt;
/** 脚本名字 */
private String pyname;
/** 招聘分类 0=社招 1=校招 2=实习 3=其他 */
private Integer recruitCategory;
}
@@ -5,6 +5,8 @@ import lombok.extern.slf4j.Slf4j;
import org.jiayunet.constant.SmsTemplateEnum;
import org.jiayunet.constant.sms.SmsVariableAllows;
import org.jiayunet.constant.sms.UniversalSmsVariable;
import org.jiayunet.exception.BusinessException;
import org.jiayunet.exception.BusinessExpCodeEnum;
import org.jiayunet.sms.AliYunSmsAbility;
import org.jiayunet.tool.server.RedisServerTool;
import org.springframework.beans.factory.annotation.Autowired;
@@ -41,33 +43,26 @@ public class SmsService {
* @param phone 目标手机号,不能为空
* @param template 短信模板枚举,必须与 {@code variable} 的类型匹配
* @param variable 短信参数对象,可能是 {@link UniversalSmsVariable}(验证码)或其他实现
* @return {@code true} 表示短信发送成功,{@code false} 表示发送失败或前置参数校验未通过
* @throws BusinessException 发送失败时抛出业务异常
*/
public boolean send(String phone, SmsTemplateEnum template, SmsVariableAllows variable) {
try {
Assert.hasText(phone, "手机号不能为空");
Assert.notNull(template, "短信模板不能为空");
Assert.notNull(variable, "短信参数不能为空");
public void send(String phone, SmsTemplateEnum template, SmsVariableAllows variable) {
Assert.hasText(phone, "手机号不能为空");
Assert.notNull(template, "短信模板不能为空");
Assert.notNull(variable, "短信参数不能为空");
// 校验参数类型是否匹配
if (!template.getVariableClass().isInstance(variable)) {
log.error("短信参数类型不匹配: template={}, expectedClass={}, actualClass={}",
template.name(), template.getVariableClass().getName(), variable.getClass().getName());
return false;
}
// 校验参数类型是否匹配
if (!template.getVariableClass().isInstance(variable)) {
log.error("短信参数类型不匹配: template={}, expectedClass={}, actualClass={}", template.name(), template.getVariableClass().getName(), variable.getClass().getName());
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "短信发送失败");
}
// 判断是否激活验证码模式
boolean isVerifyMode = isVerifyCodeMode(template, variable);
// 判断是否激活验证码模式
boolean isVerifyMode = isVerifyCodeMode(template, variable);
if (isVerifyMode) {
return sendVerifyCode(phone, template, (UniversalSmsVariable) variable);
} else {
return sendNormalSms(phone, template, variable);
}
} catch (Exception e) {
log.error("发送短信异常: phone={}, template={}", phone, template.name(), e);
return false;
if (isVerifyMode) {
sendVerifyCode(phone, template, (UniversalSmsVariable) variable);
} else {
sendNormalSms(phone, template, variable);
}
}
@@ -164,7 +159,7 @@ public class SmsService {
return false;
}
/**
/**
* 发送验证码短信。
*
* 在发送前先检查 Redis 中是否已有未过期的验证码,防止重复发送。
@@ -173,15 +168,24 @@ public class SmsService {
* @param phone 目标手机号,不能为空
* @param template 短信模板枚举,必须包含验证码相关配置(如有效时间)
* @param variable {@link UniversalSmsVariable},其中必须包含非空的 {@code code}
* @return {@code true} 表示短信发送及缓存成功,{@code false} 表示发送被拦截或失败
* @throws BusinessException 发送被拦截或失败时抛出业务异常
*/
private boolean sendVerifyCode(String phone, SmsTemplateEnum template, UniversalSmsVariable variable) {
private void sendVerifyCode(String phone, SmsTemplateEnum template, UniversalSmsVariable variable) {
String key = buildVerifyCodeKey(template, phone);
// 检查是否重复发送
// 检查是否允许重发
if (redisServerTool.hasKey(key)) {
log.warn("验证码尚未过期,请勿重复发送: phone={}, template={}", phone, template.name());
return false;
if (template.getRetryTime() != null && template.getRetryTime() > 0) {
// 配置了重发间隔且大于0:判断已过时间是否超过 retryTime
Long remainSeconds = redisServerTool.getExpire(key);
long elapsedSeconds = template.getEffectiveTime() * 60L - (remainSeconds != null ? remainSeconds : 0L);
if (elapsedSeconds < template.getRetryTime() * 60L) {
log.warn("重发间隔未到,请{}分钟后再试: phone={}, template={}", template.getRetryTime(), phone, template.name());
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "请勿重复发送");
}
// 已超过重发间隔,允许重新发送,后续会覆盖旧验证码
}
// retryTime 为 null 或 <= 0 时不限制重复发送
}
// 转换参数对象为 Map<String, String>
@@ -195,30 +199,34 @@ public class SmsService {
variableMap
);
// 发送成功后存储验证码
if (success) {
redisServerTool.set(key, variable.getCode(), template.getEffectiveTime(), TimeUnit.MINUTES);
log.info("验证码已存储: phone={}, template={}, effectiveTime={}分钟",
phone, template.name(), template.getEffectiveTime());
if (!success) {
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "短信发送失败,请稍后重试");
}
return success;
// 发送成功后存储验证码
redisServerTool.set(key, variable.getCode(), template.getEffectiveTime(), TimeUnit.MINUTES);
log.info("验证码已存储: phone={}, template={}, effectiveTime={}分钟",
phone, template.name(), template.getEffectiveTime());
}
/**
* 发送普通短信
*/
private boolean sendNormalSms(String phone, SmsTemplateEnum template, SmsVariableAllows variable) {
private void sendNormalSms(String phone, SmsTemplateEnum template, SmsVariableAllows variable) {
// 转换参数对象为 Map<String, String>
Map<String, String> variableMap = objectMapper.convertValue(variable, new com.fasterxml.jackson.core.type.TypeReference<Map<String, String>>() {});
// 直接发送短信
return aliYunSmsAbility.sendSms(
boolean success = aliYunSmsAbility.sendSms(
phone,
template.getSignName(),
template.getTemplateCode(),
variableMap
);
if (!success) {
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "短信发送失败,请稍后重试");
}
}
/**
@@ -13,6 +13,9 @@
<result column="required_major_ids" property="requiredMajorIds" typeHandler="org.jiayunet.config.LongListTypeHandler"/>
<result column="major_sensitivity" property="majorSensitivity"/>
<result column="status" property="status"/>
<result column="expire_at" property="expireAt"/>
<result column="pyname" property="pyname"/>
<result column="recruit_category" property="recruitCategory"/>
<result column="company_id" property="companyId"/>
<result column="company_name" property="companyName"/>
<result column="company_short_name" property="companyShortName"/>
@@ -36,6 +39,9 @@
j.required_major_ids,
j.major_sensitivity,
j.status,
j.expire_at,
j.pyname,
j.recruit_category,
c.id AS company_id,
c.name AS company_name,
c.short_name AS company_short_name,
@@ -126,6 +132,10 @@
<if test="employmentType != null">
AND j.employment_type = #{employmentType}
</if>
<!-- 筛选条件:招聘分类 -->
<if test="recruitCategory != null">
AND j.recruit_category = #{recruitCategory}
</if>
ORDER BY j.id DESC
</select>