Compare commits
74
Commits
1c248f8e48
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b2d791159 | ||
|
|
e286997bcc | ||
|
|
ea3cb4dead | ||
|
|
0a4b97e40e | ||
|
|
251552ea08 | ||
|
|
18f6a19a11 | ||
|
|
ff04496117 | ||
|
|
d9239a2b68 | ||
|
|
5074297775 | ||
|
|
243b1ffc5a | ||
|
|
9ce99ab3d7 | ||
|
|
04b6f94935 | ||
|
|
5556690788 | ||
|
|
6cfd8c57e1 | ||
|
|
09cf4cd5ed | ||
|
|
45dd6efe79 | ||
|
|
14cfeabb0b | ||
|
|
93ecc2364e | ||
|
|
a6a1081f74 | ||
|
|
ce801c1cf7 | ||
|
|
f6927c3327 | ||
|
|
1e5fc1506f | ||
|
|
313cc3593b | ||
|
|
ccec793079 | ||
|
|
d685dec660 | ||
|
|
f79a73a0c2 | ||
|
|
f4c60efdf2 | ||
|
|
c115f8b655 | ||
|
|
ab009d728a | ||
|
|
89e33e3713 | ||
|
|
7c21c82dba | ||
|
|
7601f9c5a5 | ||
|
|
c5de219122 | ||
|
|
a550b892af | ||
|
|
00940e610f | ||
|
|
07186297f8 | ||
|
|
dfb79160df | ||
|
|
0a413be803 | ||
|
|
9e01a5aeac | ||
|
|
283fb16816 | ||
|
|
9e7897332b | ||
|
|
30b9a62d2a | ||
|
|
bcdd1419e5 | ||
|
|
20ab909329 | ||
|
|
45b2c3073f | ||
|
|
4585c824b7 | ||
|
|
45f4c3d96f | ||
|
|
b1b29fc6b1 | ||
|
|
9859724448 | ||
|
|
7db7bbbeda | ||
|
|
41b1e546db | ||
|
|
0a0cc6f5a5 | ||
|
|
7f7a314d6e | ||
|
|
ea1f71e010 | ||
|
|
a5853f8342 | ||
|
|
a4c5beaf05 | ||
|
|
86ecb7723e | ||
|
|
f626d3def0 | ||
|
|
38cd49d292 | ||
|
|
a915d709f4 | ||
|
|
80af170d18 | ||
|
|
cb5ea2c7e9 | ||
|
|
b86514d723 | ||
|
|
aa2f2a50c9 | ||
|
|
2243197f6f | ||
|
|
523efca0ac | ||
|
|
85f78c8bd2 | ||
|
|
05bed82c51 | ||
|
|
0dad7a291c | ||
|
|
77906a1ff7 | ||
|
|
5ff481711d | ||
|
|
77186cfde7 | ||
|
|
b0dd5c98d7 | ||
|
|
a544148e5d |
+204
-93
@@ -10,11 +10,11 @@ offerpie/back-end
|
|||||||
│
|
│
|
||||||
├─ pom.xml # 父 Maven 项目,统一管理依赖、插件、属性
|
├─ pom.xml # 父 Maven 项目,统一管理依赖、插件、属性
|
||||||
│
|
│
|
||||||
├─ client‑api/ # **C 端**(面向用户)API 服务
|
├─ client‑api/ # **C 端**(面向用户)API 服务,启动类 ClientApplication
|
||||||
│ ├─ pom.xml
|
│ ├─ pom.xml
|
||||||
│ └─ src/main/java
|
│ └─ src/main/java
|
||||||
│ └─ org.jiayunet
|
│ └─ org.jiayunet
|
||||||
│ ├─ ClientApplication.java # Spring Boot 主入口
|
│ ├─ ClientApplication.java # Spring Boot 主入口(C 端)
|
||||||
│ ├─ annotation/
|
│ ├─ annotation/
|
||||||
│ │ └─ FuncPermission.java # 功能权限校验注解(标记在Controller方法上)
|
│ │ └─ FuncPermission.java # 功能权限校验注解(标记在Controller方法上)
|
||||||
│ ├─ aop/
|
│ ├─ aop/
|
||||||
@@ -29,6 +29,9 @@ offerpie/back-end
|
|||||||
│ │ ├─ JobIntentionController.java # 求职意向接口(查询与保存)
|
│ │ ├─ JobIntentionController.java # 求职意向接口(查询与保存)
|
||||||
│ │ ├─ JobController.java # 岗位接口(岗位列表查询、收藏、投递、不感兴趣、求职助手任务列表、AI岗位推荐)
|
│ │ ├─ JobController.java # 岗位接口(岗位列表查询、收藏、投递、不感兴趣、求职助手任务列表、AI岗位推荐)
|
||||||
│ │ ├─ JobAgentConfigController.java # 求职助手配置接口(配置查询与保存)
|
│ │ ├─ JobAgentConfigController.java # 求职助手配置接口(配置查询与保存)
|
||||||
|
│ │ ├─ CompanyController.java # 公司接口(查询公司类型列表,/company)
|
||||||
|
│ │ ├─ RecruitAnnouncementController.java # 招聘公告接口(公开免鉴权,/public/recruitAnnouncement)
|
||||||
|
│ │ ├─ MentorController.java # 导师接口(对接简历星球开放平台,公开免鉴权,/public/mentor)
|
||||||
│ │ ├─ MemberProductController.java # 会员商品接口(商品列表、创建订单、查询订单详情)【暂未启用,注释@RestController】
|
│ │ ├─ MemberProductController.java # 会员商品接口(商品列表、创建订单、查询订单详情)【暂未启用,注释@RestController】
|
||||||
│ │ ├─ MemberStatusController.java # 会员状态接口(查询会员状态、查询功能权限库存)
|
│ │ ├─ MemberStatusController.java # 会员状态接口(查询会员状态、查询功能权限库存)
|
||||||
│ │ └─ MessageController.java # 站内信消息接口(消息列表、未读数、按类型未读数、标记已读)
|
│ │ └─ MessageController.java # 站内信消息接口(消息列表、未读数、按类型未读数、标记已读)
|
||||||
@@ -43,6 +46,8 @@ offerpie/back-end
|
|||||||
│ │ ├─ JobIntentionService.java # 求职意向服务(查询与保存/更新)
|
│ │ ├─ JobIntentionService.java # 求职意向服务(查询与保存/更新)
|
||||||
│ │ ├─ JobService.java # 岗位服务(岗位列表查询、匹配度计算编排、求职助手任务列表、AI岗位推荐)
|
│ │ ├─ JobService.java # 岗位服务(岗位列表查询、匹配度计算编排、求职助手任务列表、AI岗位推荐)
|
||||||
│ │ ├─ JobAgentConfigService.java # 求职助手配置服务(配置查询与保存)
|
│ │ ├─ JobAgentConfigService.java # 求职助手配置服务(配置查询与保存)
|
||||||
|
│ │ ├─ CompanyService.java # 公司服务(公司类型去重列表,兜底过滤NULL与空白值)
|
||||||
|
│ │ ├─ RecruitAnnouncementService.java # 招聘公告服务(列表/详情/推荐/统计/筛选项/投递入口)
|
||||||
│ │ ├─ UserResumeService.java # 用户简历服务(简历列表、主表及5张子表的CRUD、子表单条添加/编辑/删除、设置默认简历)
|
│ │ ├─ UserResumeService.java # 用户简历服务(简历列表、主表及5张子表的CRUD、子表单条添加/编辑/删除、设置默认简历)
|
||||||
│ │ ├─ MemberProductService.java # 会员商品服务(商品列表、创建订单、支付回调处理)【暂未启用,注释@Service】
|
│ │ ├─ MemberProductService.java # 会员商品服务(商品列表、创建订单、支付回调处理)【暂未启用,注释@Service】
|
||||||
│ │ ├─ MemberGrantService.java # 会员权益发放服务(续费会员+发放功能/菜单权限)
|
│ │ ├─ MemberGrantService.java # 会员权益发放服务(续费会员+发放功能/菜单权限)
|
||||||
@@ -53,9 +58,10 @@ offerpie/back-end
|
|||||||
│ └─ pojo/
|
│ └─ pojo/
|
||||||
│ ├─ param/
|
│ ├─ param/
|
||||||
│ │ ├─ userProfile/ # 个人资料入参(UserProfileParam、各子表Param)
|
│ │ ├─ userProfile/ # 个人资料入参(UserProfileParam、各子表Param)
|
||||||
│ │ ├─ resume/ # 简历入参(ResumeParam、各子表Param、各子表UpdateParam、ResumeSubTableParam)
|
│ │ ├─ resume/ # 简历入参(ResumeParam、各子表Param、各子表UpdateParam、ResumeSubTableParam、ResumeInfoUpdateParam)
|
||||||
│ │ ├─ job/ # 岗位相关入参(JobIntentionParam、JobQueryParam、JobAgentTaskQueryParam、JobAgentRecommendParam、JobApplyParam、JobApplyQueryParam、JobFavoriteQueryParam、JobDislikeParam)
|
│ │ ├─ job/ # 岗位相关入参(JobIntentionParam、JobQueryParam、JobAgentTaskQueryParam、JobAgentRecommendParam、JobApplyParam、JobApplyQueryParam、JobFavoriteQueryParam、JobDislikeParam)
|
||||||
│ │ ├─ jobAgent/ # 求职助手入参(JobAgentConfigParam、ChatMessageParam)
|
│ │ ├─ jobAgent/ # 求职助手入参(JobAgentConfigParam、ChatMessageParam)
|
||||||
|
│ │ ├─ recruitAnnouncement/ # 招聘公告入参(RecruitAnnouncementQueryParam、RecruitAnnouncementRecommendParam)
|
||||||
│ │ ├─ memberProduct/ # 会员商品入参(CreateOrderParam)
|
│ │ ├─ memberProduct/ # 会员商品入参(CreateOrderParam)
|
||||||
│ │ ├─ message/ # 站内信入参(MessageQueryParam)
|
│ │ ├─ message/ # 站内信入参(MessageQueryParam)
|
||||||
│ │ ├─ feedback/ # 用户反馈入参(UserFeedbackParam)
|
│ │ ├─ feedback/ # 用户反馈入参(UserFeedbackParam)
|
||||||
@@ -66,6 +72,7 @@ offerpie/back-end
|
|||||||
│ │ ├─ resume/ # 简历出参(ResumeDto、ResumeListItemDto、各子表Dto)
|
│ │ ├─ resume/ # 简历出参(ResumeDto、ResumeListItemDto、各子表Dto)
|
||||||
│ │ ├─ job/ # 岗位相关出参(JobIntentionDto、JobDto、JobDetailDto、JobMatchScoreDto、JobAgentRecommendDto、JobApplicationDto)
|
│ │ ├─ job/ # 岗位相关出参(JobIntentionDto、JobDto、JobDetailDto、JobMatchScoreDto、JobAgentRecommendDto、JobApplicationDto)
|
||||||
│ │ ├─ jobAgent/ # 求职助手出参(JobAgentConfigDto、ChatMessageDto)
|
│ │ ├─ jobAgent/ # 求职助手出参(JobAgentConfigDto、ChatMessageDto)
|
||||||
|
│ │ ├─ recruitAnnouncement/ # 招聘公告出参(RecruitAnnouncementDto、RecruitAnnouncementDetailDto、RecruitAnnouncementRecommendDto、RecruitAnnouncementStatisticsDto)
|
||||||
│ │ ├─ memberProduct/ # 会员商品出参(CreateOrderDto、OrderDetailDto)
|
│ │ ├─ memberProduct/ # 会员商品出参(CreateOrderDto、OrderDetailDto)
|
||||||
│ │ ├─ memberStatus/ # 会员状态出参(MemberStatusDto、FuncStockDto)
|
│ │ ├─ memberStatus/ # 会员状态出参(MemberStatusDto、FuncStockDto)
|
||||||
│ │ ├─ message/ # 站内信出参(MessageDto、MessageUnreadCountDto)
|
│ │ ├─ message/ # 站内信出参(MessageDto、MessageUnreadCountDto)
|
||||||
@@ -76,6 +83,31 @@ offerpie/back-end
|
|||||||
│ ├─ JobApplyCountVo.java # 岗位投递数量统计VO
|
│ ├─ JobApplyCountVo.java # 岗位投递数量统计VO
|
||||||
│ └─ JobFavoriteCountVo.java # 岗位收藏数量统计VO
|
│ └─ JobFavoriteCountVo.java # 岗位收藏数量统计VO
|
||||||
│
|
│
|
||||||
|
├─ admin/ # **B 端**管理后台服务,启动类 AdminApplication(Thymeleaf 页面 + REST 接口)
|
||||||
|
│ ├─ pom.xml
|
||||||
|
│ ├─ Dockerfile / Jenkinsfile / nginx.conf
|
||||||
|
│ └─ src/main
|
||||||
|
│ ├─ java/org.jiayunet
|
||||||
|
│ │ ├─ AdminApplication.java # Spring Boot 主入口(B 端)
|
||||||
|
│ │ └─ admin/
|
||||||
|
│ │ ├─ constant/
|
||||||
|
│ │ │ └─ AdminRedisKeyName.java # B 端 Redis Key 常量(login:token、login:fail、login:lock)
|
||||||
|
│ │ ├─ controller/
|
||||||
|
│ │ │ ├─ AdminLoginController.java # 管理员登录(/public)
|
||||||
|
│ │ │ ├─ AdminUserController.java # 用户管理(列表/统计/禁用启用,/user)
|
||||||
|
│ │ │ ├─ AdminOrderController.java # 订单管理(列表/统计,/order)
|
||||||
|
│ │ │ ├─ AdminProductController.java # 商品管理(CRUD,/product)
|
||||||
|
│ │ │ └─ PageController.java # 页面路由(返回Thymeleaf视图名,不走统一响应包装)
|
||||||
|
│ │ ├─ service/
|
||||||
|
│ │ │ ├─ AdminLoginService.java # 管理员登录(密码校验、失败累计3次锁4小时、JWT、多设备踢出、Cookie)
|
||||||
|
│ │ │ ├─ AdminUserService.java # 用户管理(分页+简历数量统计、新增数统计、禁用/启用)
|
||||||
|
│ │ │ ├─ AdminOrderService.java # 订单管理(分页+关联用户/商品、支付数与收入统计)
|
||||||
|
│ │ │ └─ AdminProductService.java # 商品管理(会员商品CRUD)
|
||||||
|
│ │ └─ pojo/
|
||||||
|
│ │ ├─ param/ # login/AdminLoginParam、user/UserPageParam、order/OrderPageParam、product/ProductPageParam、product/ProductSaveParam
|
||||||
|
│ │ └─ dto/ # user/UserListDto、user/UserStatsDto、order/OrderListDto、order/OrderStatsDto
|
||||||
|
│ └─ resources/ # application.yml + dev/test/prod 多环境配置、Thymeleaf 模板与静态资源
|
||||||
|
│
|
||||||
├─ common/ # **共享层**:被 C 端和 B 端共同使用的代码库
|
├─ common/ # **共享层**:被 C 端和 B 端共同使用的代码库
|
||||||
│ ├─ pom.xml
|
│ ├─ pom.xml
|
||||||
│ └─ src/main/java
|
│ └─ src/main/java
|
||||||
@@ -83,7 +115,7 @@ offerpie/back-end
|
|||||||
│ ├─ config/ # 统一配置
|
│ ├─ config/ # 统一配置
|
||||||
│ │ ├─ OssConfig.java # 阿里云OSS凭证配置
|
│ │ ├─ OssConfig.java # 阿里云OSS凭证配置
|
||||||
│ │ ├─ RedissonConf.java # Redisson客户端配置
|
│ │ ├─ RedissonConf.java # Redisson客户端配置
|
||||||
│ │ ├─ SecurityConfig.java # Spring Security配置(JWT过滤器、白名单路径)
|
│ │ ├─ SecurityConfig.java # Spring Security配置(JWT过滤器、app.ignore.urls 白名单)
|
||||||
│ │ ├─ WxPayConfig.java # 微信支付配置
|
│ │ ├─ WxPayConfig.java # 微信支付配置
|
||||||
│ │ ├─ SmsConfig.java # 阿里云短信客户端配置
|
│ │ ├─ SmsConfig.java # 阿里云短信客户端配置
|
||||||
│ │ ├─ AsyncConfig.java # 异步线程池配置
|
│ │ ├─ AsyncConfig.java # 异步线程池配置
|
||||||
@@ -114,8 +146,7 @@ offerpie/back-end
|
|||||||
│ │ └─ SqlLoggerInterceptor.java # SQL日志拦截器
|
│ │ └─ SqlLoggerInterceptor.java # SQL日志拦截器
|
||||||
│ ├─ aop/
|
│ ├─ aop/
|
||||||
│ │ └─ ControllerLogAspect.java # Controller日志切面
|
│ │ └─ ControllerLogAspect.java # Controller日志切面
|
||||||
│ ├─ exception/ # 业务异常统一处理
|
│ ├─ exception/ # 业务异常定义
|
||||||
│ │ ├─ GlobalExceptionAdvice.java # → 移至web包,此处为异常定义
|
|
||||||
│ │ ├─ BusinessException.java # 业务异常
|
│ │ ├─ BusinessException.java # 业务异常
|
||||||
│ │ ├─ BusinessExpCodeEnum.java # 业务异常码枚举
|
│ │ ├─ BusinessExpCodeEnum.java # 业务异常码枚举
|
||||||
│ │ └─ BusinessExpCodeOperations.java # 异常码操作接口
|
│ │ └─ BusinessExpCodeOperations.java # 异常码操作接口
|
||||||
@@ -124,7 +155,7 @@ offerpie/back-end
|
|||||||
│ │ └─ UnifiedResponseBodyAdvice.java # 统一响应体包装
|
│ │ └─ UnifiedResponseBodyAdvice.java # 统一响应体包装
|
||||||
│ ├─ pojo/ # 公共POJO
|
│ ├─ pojo/ # 公共POJO
|
||||||
│ │ ├─ UnifiedResponse.java # 统一响应结构
|
│ │ ├─ UnifiedResponse.java # 统一响应结构
|
||||||
│ │ ├─ PageParam.java # 分页请求参数基类
|
│ │ ├─ PageParam.java # 分页请求参数基类(pageNum/pageSize/keyword,pageSize上限200)
|
||||||
│ │ ├─ PageResult.java # 分页结果包装
|
│ │ ├─ PageResult.java # 分页结果包装
|
||||||
│ │ ├─ login/
|
│ │ ├─ login/
|
||||||
│ │ │ └─ RedisLoginTokenInfo.java # Redis中登录Token信息结构
|
│ │ │ └─ RedisLoginTokenInfo.java # Redis中登录Token信息结构
|
||||||
@@ -152,7 +183,8 @@ offerpie/back-end
|
|||||||
│ ├─ WxPayNotifyController.java # 微信支付回调入口
|
│ ├─ WxPayNotifyController.java # 微信支付回调入口
|
||||||
│ ├─ WxPayNotifyMessageAbstract.java # 微信回调处理抽象接口
|
│ ├─ WxPayNotifyMessageAbstract.java # 微信回调处理抽象接口
|
||||||
│ └─ server/
|
│ └─ server/
|
||||||
│ └─ TransferServer.java # 微信转账服务端封装
|
│ ├─ TransferServer.java # 微信转账服务端封装
|
||||||
|
│ └─ model/ # 转账相关请求/响应模型(TransferBillsRequest/Response、CancelRequest/Response、BillNoQueryRequest、TransferNotification、TransferResultResponse、JsRequestPay)
|
||||||
│
|
│
|
||||||
└─ manager/ # **B 端 + C 端共享** 的业务实现
|
└─ manager/ # **B 端 + C 端共享** 的业务实现
|
||||||
├─ pom.xml
|
├─ pom.xml
|
||||||
@@ -171,9 +203,18 @@ offerpie/back-end
|
|||||||
│ ├─ AgreementController.java # 协议接口(根据协议码查询最新启用版本)
|
│ ├─ AgreementController.java # 协议接口(根据协议码查询最新启用版本)
|
||||||
│ ├─ RegionController.java # 地区接口(树形/层级/搜索/批量查询)
|
│ ├─ RegionController.java # 地区接口(树形/层级/搜索/批量查询)
|
||||||
│ ├─ JobCategoryController.java # 岗位分类接口(树形/层级/搜索/批量查询)
|
│ ├─ JobCategoryController.java # 岗位分类接口(树形/层级/搜索/批量查询)
|
||||||
│ └─ IndustryController.java # 行业接口(树形/层级/搜索/批量查询)
|
│ ├─ IndustryController.java # 行业接口(树形/层级/搜索/批量查询)
|
||||||
|
│ └─ MentorMockChatController.java # 导师模拟对话接口(/public/mentor/mockChat)
|
||||||
|
├─ remote/jsxq/ # 简历星球(jianshixingqiu)开放平台远程调用
|
||||||
|
│ ├─ JsxqClient.java # 平台客户端(统一请求头、拆 {code,msg,data,ok} 包装、按类型反序列化)
|
||||||
|
│ ├─ JsxqProperties.java # 域名/令牌/租户配置
|
||||||
|
│ ├─ pojo/JsxqResponse.java # 平台统一响应结构
|
||||||
|
│ ├─ pojo/JsxqPage.java # 平台分页结构
|
||||||
|
│ ├─ pojo/param/ # MentorPageParam、QrCodeParam
|
||||||
|
│ └─ pojo/dto/ # MentorItemDto、MentorFullInfoDto、TeacherTalkDto、ChatEvaluateDto
|
||||||
├─ mapper/ # MyBatis Mapper
|
├─ mapper/ # MyBatis Mapper
|
||||||
│ ├─ UserMapper.java
|
│ ├─ UserMapper.java
|
||||||
|
│ ├─ AdminUserMapper.java # 管理员用户Mapper
|
||||||
│ ├─ OssFileMapper.java
|
│ ├─ OssFileMapper.java
|
||||||
│ ├─ UserInviteMapper.java # 用户邀请记录Mapper
|
│ ├─ UserInviteMapper.java # 用户邀请记录Mapper
|
||||||
│ ├─ RouteMenuMapper.java # 路由菜单Mapper
|
│ ├─ RouteMenuMapper.java # 路由菜单Mapper
|
||||||
@@ -184,12 +225,21 @@ offerpie/back-end
|
|||||||
│ ├─ ChinaRegionsCodeMapper.java # 地区Mapper
|
│ ├─ ChinaRegionsCodeMapper.java # 地区Mapper
|
||||||
│ ├─ JobCategoryMapper.java # 岗位类型Mapper
|
│ ├─ JobCategoryMapper.java # 岗位类型Mapper
|
||||||
│ ├─ CompanyMapper.java # 公司Mapper
|
│ ├─ CompanyMapper.java # 公司Mapper
|
||||||
│ ├─ JobMapper.java # 岗位Mapper
|
│ ├─ JobMapper.java # 岗位Mapper(含selectJobPage自定义SQL)
|
||||||
│ ├─ JobRegionRelationMapper.java # 岗位-地区关联Mapper
|
│ ├─ JobRegionRelationMapper.java # 岗位-地区关联Mapper
|
||||||
│ ├─ JobSkillTagRelationMapper.java # 岗位-技能标签关联Mapper
|
│ ├─ JobSkillTagRelationMapper.java # 岗位-技能标签关联Mapper
|
||||||
│ ├─ IndustryMapper.java # 行业Mapper
|
│ ├─ IndustryMapper.java # 行业Mapper
|
||||||
│ ├─ SkillTagMapper.java # 技能标签Mapper(含insertIgnore自定义SQL)
|
│ ├─ SkillTagMapper.java # 技能标签Mapper(含insertIgnore自定义SQL)
|
||||||
│ ├─ MajorCategoryMapper.java # 专业分类Mapper
|
│ ├─ MajorCategoryMapper.java # 专业分类Mapper
|
||||||
|
│ ├─ RecruitAnnouncementMapper.java # 招聘公告主表Mapper(XML:最后更新时间、统计、列表分页、推荐分页)
|
||||||
|
│ ├─ RecruitAnnouncementYearMapper.java # 公告-招聘届数Mapper(@Select分组去重)
|
||||||
|
│ ├─ RecruitAnnouncementBatchMapper.java # 公告-批次Mapper(@Select分组去重)
|
||||||
|
│ ├─ RecruitAnnouncementTagMapper.java # 公告-标签Mapper(@Select分组去重)
|
||||||
|
│ ├─ RecruitAnnouncementCityMapper.java # 公告-热招城市Mapper(@Select分组去重)
|
||||||
|
│ ├─ RecruitAnnouncementCategoryMapper.java # 公告-岗位大类Mapper(@Select分组去重)
|
||||||
|
│ ├─ RecruitAnnouncementIndustryMapper.java # 公告-行业Mapper(@Select分组去重)
|
||||||
|
│ ├─ RecruitAnnouncementEducationMapper.java # 公告-学历要求Mapper(@Select分组去重)
|
||||||
|
│ ├─ MentorMockChatMapper.java # 导师模拟对话缓存Mapper
|
||||||
│ ├─ UserJobFavoriteMapper.java # 用户收藏岗位Mapper
|
│ ├─ UserJobFavoriteMapper.java # 用户收藏岗位Mapper
|
||||||
│ ├─ UserJobApplicationMapper.java # 用户投递记录Mapper
|
│ ├─ UserJobApplicationMapper.java # 用户投递记录Mapper
|
||||||
│ ├─ UserJobDislikeMapper.java # 用户不感兴趣记录Mapper
|
│ ├─ UserJobDislikeMapper.java # 用户不感兴趣记录Mapper
|
||||||
@@ -222,11 +272,11 @@ offerpie/back-end
|
|||||||
│ ├─ MemberRouteItemMapper.java # 会员菜单配置项Mapper
|
│ ├─ MemberRouteItemMapper.java # 会员菜单配置项Mapper
|
||||||
│ ├─ PayWechatFlowMapper.java # 微信支付流水Mapper
|
│ ├─ PayWechatFlowMapper.java # 微信支付流水Mapper
|
||||||
│ ├─ PayAlipayFlowMapper.java # 支付宝支付流水Mapper
|
│ ├─ PayAlipayFlowMapper.java # 支付宝支付流水Mapper
|
||||||
│ ├─ UserFeedbackMapper.java # 用户反馈Mapper
|
│ └─ UserFeedbackMapper.java # 用户反馈Mapper
|
||||||
│ └─ AppJobDataMapper.java # 爬虫岗位原始数据Mapper
|
|
||||||
├─ pojo/
|
├─ pojo/
|
||||||
│ ├─ po/ # 持久化实体
|
│ ├─ po/ # 持久化实体
|
||||||
│ │ ├─ User.java
|
│ │ ├─ User.java
|
||||||
|
│ │ ├─ AdminUser.java # 管理员用户表(bg_admin_user)
|
||||||
│ │ ├─ OssFile.java
|
│ │ ├─ OssFile.java
|
||||||
│ │ ├─ UserInvite.java # 用户邀请记录表(bg_user_invite)
|
│ │ ├─ UserInvite.java # 用户邀请记录表(bg_user_invite)
|
||||||
│ │ ├─ RouteMenu.java # 路由菜单表(bg_route_menu)
|
│ │ ├─ RouteMenu.java # 路由菜单表(bg_route_menu)
|
||||||
@@ -243,6 +293,15 @@ offerpie/back-end
|
|||||||
│ │ ├─ Industry.java # 行业字典表(bg_industry)
|
│ │ ├─ Industry.java # 行业字典表(bg_industry)
|
||||||
│ │ ├─ SkillTag.java # 技能标签表(bg_skill_tag)
|
│ │ ├─ SkillTag.java # 技能标签表(bg_skill_tag)
|
||||||
│ │ ├─ MajorCategory.java # 专业分类表(bg_major_category)
|
│ │ ├─ MajorCategory.java # 专业分类表(bg_major_category)
|
||||||
|
│ │ ├─ RecruitAnnouncement.java # 招聘公告主表(bg_recruit_announcement)
|
||||||
|
│ │ ├─ RecruitAnnouncementYear.java # 公告-招聘届数关联表(bg_recruit_announcement_year)
|
||||||
|
│ │ ├─ RecruitAnnouncementBatch.java # 公告-批次关联表(bg_recruit_announcement_batch)
|
||||||
|
│ │ ├─ RecruitAnnouncementTag.java # 公告-标签关联表(bg_recruit_announcement_tag)
|
||||||
|
│ │ ├─ RecruitAnnouncementCity.java # 公告-热招城市关联表(bg_recruit_announcement_city)
|
||||||
|
│ │ ├─ RecruitAnnouncementCategory.java # 公告-岗位大类关联表(bg_recruit_announcement_category)
|
||||||
|
│ │ ├─ RecruitAnnouncementIndustry.java # 公告-行业关联表(bg_recruit_announcement_industry)
|
||||||
|
│ │ ├─ RecruitAnnouncementEducation.java # 公告-学历要求关联表(bg_recruit_announcement_education)
|
||||||
|
│ │ ├─ MentorMockChat.java # 导师模拟对话表(bg_mentor_mock_chat,按导师ID唯一)
|
||||||
│ │ ├─ UserJobFavorite.java # 用户收藏岗位表(bg_user_job_favorite)
|
│ │ ├─ UserJobFavorite.java # 用户收藏岗位表(bg_user_job_favorite)
|
||||||
│ │ ├─ UserJobApplication.java # 用户投递记录表(bg_user_job_application)
|
│ │ ├─ UserJobApplication.java # 用户投递记录表(bg_user_job_application)
|
||||||
│ │ ├─ UserJobDislike.java # 用户不感兴趣记录表(bg_user_job_dislike)
|
│ │ ├─ UserJobDislike.java # 用户不感兴趣记录表(bg_user_job_dislike)
|
||||||
@@ -270,18 +329,22 @@ offerpie/back-end
|
|||||||
│ │ ├─ Agreement.java # 协议表(bg_agreement,版本递增)
|
│ │ ├─ Agreement.java # 协议表(bg_agreement,版本递增)
|
||||||
│ │ ├─ MemberProduct.java # 会员商品表(bg_member_product)
|
│ │ ├─ MemberProduct.java # 会员商品表(bg_member_product)
|
||||||
│ │ ├─ MemberOrder.java # 会员订单表(bg_member_order)
|
│ │ ├─ MemberOrder.java # 会员订单表(bg_member_order)
|
||||||
│ │ ├─ MemberUser.java # 会员用户表(bg_member_user,记录会员到期时间)
|
│ │ ├─ MemberUser.java # 会员用户表(bg_member_user)
|
||||||
│ │ ├─ MemberFuncItem.java # 会员功能配置项表(bg_member_func_item,定义会员包含的功能权限)
|
│ │ ├─ MemberFuncItem.java # 会员功能配置项表(bg_member_func_item)
|
||||||
│ │ ├─ MemberRouteItem.java # 会员菜单配置项表(bg_member_route_item,定义会员包含的菜单权限)
|
│ │ ├─ MemberRouteItem.java # 会员菜单配置项表(bg_member_route_item)
|
||||||
│ │ ├─ PayWechatFlow.java # 微信支付流水表(bg_pay_wechat_flow)
|
│ │ ├─ PayWechatFlow.java # 微信支付流水表(bg_pay_wechat_flow)
|
||||||
│ │ ├─ PayAlipayFlow.java # 支付宝支付流水表(bg_pay_alipay_flow)
|
│ │ ├─ PayAlipayFlow.java # 支付宝支付流水表(bg_pay_alipay_flow)
|
||||||
│ │ ├─ UserFeedback.java # 用户反馈表(bg_user_feedback)
|
│ │ └─ UserFeedback.java # 用户反馈表(bg_user_feedback)
|
||||||
│ │ └─ AppJobData.java # 爬虫岗位原始数据表(app_job_data)
|
│ ├─ dto/ # Mapper 入参 / 跨模块传输对象
|
||||||
│ └─ vo/ # ViewObject
|
│ │ ├─ RecruitAnnouncementQueryDto.java # 招聘公告查询条件(供Mapper使用,避免manager反向依赖client-api)
|
||||||
|
│ │ └─ mentor/MentorMockChatDto.java # 导师模拟对话出参
|
||||||
|
│ └─ vo/ # ViewObject / SQL查询结果
|
||||||
│ ├─ OssUrlVo.java # OSS签名URL返回
|
│ ├─ OssUrlVo.java # OSS签名URL返回
|
||||||
│ ├─ DescriptionParagraph.java # 描述段落对象(JSON字段通用结构)
|
│ ├─ DescriptionParagraph.java # 描述段落对象(JSON字段通用结构)
|
||||||
│ ├─ LanguageAbility.java # 语言能力对象(求职助手配置JSON字段)
|
│ ├─ LanguageAbility.java # 语言能力对象(求职助手配置JSON字段)
|
||||||
│ ├─ JobListItemVo.java # 岗位列表项VO
|
│ ├─ JobListItemVo.java # 岗位列表项VO
|
||||||
|
│ ├─ RecruitAnnouncementListItemVo.java # 公告列表项VO(列表与推荐共用)
|
||||||
|
│ ├─ RecruitAnnouncementStatisticsVo.java # 公告统计VO(日/月/累计)
|
||||||
│ ├─ UserHonorsVo.java # 用户荣誉VO
|
│ ├─ UserHonorsVo.java # 用户荣誉VO
|
||||||
│ ├─ AgreementListVo.java # 协议列表VO
|
│ ├─ AgreementListVo.java # 协议列表VO
|
||||||
│ ├─ RegionTreeVo.java # 地区树形VO
|
│ ├─ RegionTreeVo.java # 地区树形VO
|
||||||
@@ -291,7 +354,8 @@ offerpie/back-end
|
|||||||
│ ├─ IndustryTreeVo.java # 行业树形VO
|
│ ├─ IndustryTreeVo.java # 行业树形VO
|
||||||
│ └─ IndustryVo.java # 行业VO
|
│ └─ IndustryVo.java # 行业VO
|
||||||
├─ resources/mapper/ # MyBatis XML映射文件
|
├─ resources/mapper/ # MyBatis XML映射文件
|
||||||
│ └─ JobMapper.xml # 岗位自定义SQL(selectJobPage)
|
│ ├─ JobMapper.xml # 岗位自定义SQL(selectJobPage)
|
||||||
|
│ └─ RecruitAnnouncementMapper.xml # 公告自定义SQL(selectLastCreateTime、selectStatistics、selectAnnouncementPage、selectRecommendPage)
|
||||||
└─ service/ # 业务Service
|
└─ service/ # 业务Service
|
||||||
├─ OssService.java # 文件上传/下载服务
|
├─ OssService.java # 文件上传/下载服务
|
||||||
├─ SmsService.java # 短信发送服务
|
├─ SmsService.java # 短信发送服务
|
||||||
@@ -300,83 +364,67 @@ offerpie/back-end
|
|||||||
├─ JobCategoryService.java # 岗位分类服务(树形/层级/搜索/批量查询)
|
├─ JobCategoryService.java # 岗位分类服务(树形/层级/搜索/批量查询)
|
||||||
├─ IndustryService.java # 行业服务(树形/层级/搜索/批量查询)
|
├─ IndustryService.java # 行业服务(树形/层级/搜索/批量查询)
|
||||||
├─ AgreementService.java # 协议服务(查询最新版本、列表、创建/编辑版本递增)
|
├─ AgreementService.java # 协议服务(查询最新版本、列表、创建/编辑版本递增)
|
||||||
├─ JobCleanService.java # 岗位清洗服务(爬虫数据→业务表)
|
├─ MentorMockChatService.java # 导师模拟对话生成服务(查缓存→取导师信息与话题→AI生成→入库)
|
||||||
├─ JobCleanTransactionService.java # 岗位清洗事务服务
|
|
||||||
├─ CompanyCleanService.java # 公司清洗服务
|
|
||||||
├─ CompanyCleanTransactionService.java # 公司清洗事务服务
|
|
||||||
├─ UserProfileAnalyzeService.java # 用户简历分析服务(AI三次调用:综合分析/专业归一化/技能提取)
|
├─ UserProfileAnalyzeService.java # 用户简历分析服务(AI三次调用:综合分析/专业归一化/技能提取)
|
||||||
├─ JobMatchService.java # 岗位匹配度计算服务(教育30%+经历30%+技能40%)
|
├─ JobMatchService.java # 岗位匹配度计算服务(教育30%+经历30%+技能40%)
|
||||||
└─ MessageService.java # 站内信服务(发送系统/运营/订单消息)
|
└─ MessageService.java # 站内信服务(发送系统/运营/订单消息)
|
||||||
```
|
```
|
||||||
> **设计理念** – 业务实体和 Mapper 位于 `manager`,B 端和 C 端共享;C 端特有的注解、切面、权限服务、路由菜单服务位于 `client-api`,避免 B 端误用;`common` 提供统一的技术支撑。
|
> **设计理念** – 业务实体和 Mapper 位于 `manager`,B 端(admin)和 C 端(client-api)共享;C 端特有的注解、切面、权限服务、路由菜单服务位于 `client-api`,B 端特有的登录与管理服务位于 `admin`;`common` 提供统一的技术支撑。
|
||||||
|
>
|
||||||
|
> **模块依赖方向**:`client-api` / `admin` → `manager` → `common`,反向依赖不成立。因此 Mapper 需要接收复杂查询条件时,要在 `manager` 侧定义 DTO(如 `RecruitAnnouncementQueryDto`),不能直接引用 `client-api` 的 Param;SQL 查询结果同理放在 `manager/pojo/vo`。
|
||||||
|
|
||||||
## 2️⃣ 各层模块职责
|
## 2️⃣ 各层模块职责
|
||||||
| 层级 | 主要职责 | 关键类/包 |
|
| 层级 | 主要职责 | 关键类/包 |
|
||||||
|------|----------|-----------|
|
|------|----------|-----------|
|
||||||
| **client-api** | - 面向终端用户的 REST API <br> - 启动 Spring Boot 应用 <br> - 短信验证码登录(含自动注册、邀请码绑定) <br> - **功能权限校验**:注解 + 切面 + 权限服务(校验、扣减、回退) <br> - **路由菜单**:获取用户有效菜单树 <br> - **用户管理**:查看/修改信息、注销账号、邀请统计 <br> - **用户反馈**:提交反馈 <br> - **会员体系**:商品下单、支付回调、会员状态查询、权益发放 <br> - **求职助手**:配置管理、AI岗位推荐、任务列表 | `ClientApplication`、`LoginController`、`RouteMenuController`、`UserManageController`、`UserFeedbackController`、`MemberProductController`、`MemberStatusController`、`JobController`、`JobAgentConfigController`、`UserResumeController`、`MessageController`、`FuncPermission`、`FuncPermissionAspect`、`FuncPermissionService`、`MemberGrantService`、`MemberProductService`、`MemberStatusService`、`UserManageService` |
|
| **client-api** | - 面向终端用户的 REST API <br> - 启动 Spring Boot 应用(`ClientApplication`) <br> - 短信验证码登录(含自动注册、邀请码绑定) <br> - **功能权限校验**:注解 + 切面 + 权限服务(校验、扣减、回退) <br> - **路由菜单**:获取用户有效菜单树 <br> - **用户管理**:查看/修改信息、注销账号、邀请统计 <br> - **用户反馈**:提交反馈 <br> - **会员体系**:商品下单、支付回调、会员状态查询、权益发放 <br> - **求职助手**:配置管理、AI岗位推荐、任务列表 <br> - **招聘公告**:列表/详情/推荐/统计/筛选项查询 <br> - **导师**:对接简历星球开放平台 | `ClientApplication`、`LoginController`、`RouteMenuController`、`UserManageController`、`UserFeedbackController`、`MemberProductController`、`MemberStatusController`、`JobController`、`JobAgentConfigController`、`UserResumeController`、`MessageController`、`RecruitAnnouncementController`、`CompanyController`、`MentorController`、`FuncPermission`、`FuncPermissionAspect`、`FuncPermissionService`、`MemberGrantService`、`RecruitAnnouncementService` |
|
||||||
| **common** | - **统一配置**:OSS、Redis、Security、WxPay、Alipay、Sms、Async、Jackson、MyBatis、Web 等 <br> - **跨层工具**:HTTP、IP、认证、验证码、Redis Server、用户安全 <br> - **通用Mapper**:CommonMapper(批量插入/更新) <br> - **全局拦截/切面**:日志、TraceId、黑名单、防重放、SQL打印 <br> - **统一异常/响应**:`GlobalExceptionAdvice`、`UnifiedResponse`、`PageResult` <br> - **业务能力抽象**:邮件(EmailAbility)、短信(AliYunSmsAbility)、OSS(AliOssAbility)、AI对话(AiChatAbility)、微信支付(Native/JS/Transfer)、支付宝支付(PagePay) <br> - **公共 POJO**:登录令牌、防重放信息、分页参数/结果 | `config/`, `constant/`, `mapper/`, `tool/`, `interceptor/`, `aop/`, `exception/`, `web/`, `pojo/`, `email/`, `sms/`, `oss/`, `ai/`, `alipay/`, `wxPay/` |
|
| **admin** | - B 端管理后台(`AdminApplication`),Thymeleaf 页面 + REST 接口 <br> - **管理员登录**:账号密码 + JWT + 失败锁定 <br> - **用户管理**:分页列表、注册统计、禁用/启用 <br> - **订单管理**:分页列表、支付数与收入统计 <br> - **商品管理**:会员商品 CRUD | `AdminApplication`、`AdminLoginController`、`AdminUserController`、`AdminOrderController`、`AdminProductController`、`PageController`、`AdminLoginService`、`AdminUserService`、`AdminOrderService`、`AdminProductService`、`AdminRedisKeyName` |
|
||||||
| **manager** | - **业务实体**(User、OssFile、UserInvite、RouteMenu、FuncPermission、库存表、地区/岗位分类/行业/技能标签/专业分类字典、岗位及关联表、用户行为表、个人资料及5张子表、简历及5张子表、诊断报告/问题、求职助手配置/对话/定制简历、站内信、协议、会员商品/订单/用户/配置项、支付流水、用户反馈、爬虫原始数据) <br> - **MyBatis Mapper**(对应全部业务实体) <br> - **字典/公共 API**:文件上传/下载、健康检查、地区/岗位分类/行业字典查询、协议查询 <br> - **业务逻辑**:OssService、SmsService、DictCacheService、RegionService、JobCategoryService、IndustryService、AgreementService、JobCleanService、CompanyCleanService、UserProfileAnalyzeService、JobMatchService、MessageService | `controller/`, `mapper/`, `pojo/po/`, `pojo/vo/`, `service/`, `constant/` |
|
| **common** | - **统一配置**:OSS、Redis、Security、WxPay、Alipay、Sms、Async、Jackson、MyBatis、Web 等 <br> - **跨层工具**:HTTP、IP、认证、验证码、Redis Server、用户安全 <br> - **通用Mapper**:CommonMapper(批量插入/更新) <br> - **全局拦截/切面**:日志、TraceId、黑名单、防重放、SQL打印 <br> - **统一异常/响应**:`GlobalExceptionAdvice`、`UnifiedResponse`、`PageResult` <br> - **业务能力抽象**:邮件、短信、OSS、AI对话、微信支付(Native/JS/Transfer)、支付宝支付(PagePay) <br> - **公共 POJO**:登录令牌、防重放信息、分页参数/结果 | `config/`, `constant/`, `mapper/`, `tool/`, `interceptor/`, `aop/`, `exception/`, `web/`, `pojo/`, `email/`, `sms/`, `oss/`, `ai/`, `alipay/`, `wxPay/` |
|
||||||
|
| **manager** | - **业务实体**(用户/管理员、OSS文件、邀请、权限与库存、地区/岗位分类/行业/技能标签/专业分类字典、岗位及关联表、招聘公告及7张关联表、用户行为表、个人资料及5张子表、简历及5张子表、诊断报告/问题、求职助手配置/对话/定制简历、导师模拟对话、站内信、协议、会员商品/订单/用户/配置项、支付流水、用户反馈) <br> - **MyBatis Mapper**(对应全部业务实体) <br> - **字典/公共 API**:文件上传/下载、健康检查、地区/岗位分类/行业字典查询、协议查询、导师模拟对话 <br> - **远程调用**:简历星球开放平台客户端(`remote/jsxq`) <br> - **业务逻辑**:OssService、SmsService、DictCacheService、RegionService、JobCategoryService、IndustryService、AgreementService、MentorMockChatService、UserProfileAnalyzeService、JobMatchService、MessageService | `controller/`, `mapper/`, `remote/`, `pojo/po/`, `pojo/dto/`, `pojo/vo/`, `service/`, `constant/` |
|
||||||
|
|
||||||
|
> **注**:岗位与公司的数据清洗已迁出后端,由独立服务 `offerpie_job_cleaner` 负责(读 PostgreSQL 爬虫原始数据 → AI 清洗 → 写入 MySQL 业务库);招聘公告采集由 `offerpie_spider` 负责。后端只消费清洗后的业务表。
|
||||||
|
|
||||||
## 3️⃣ 关键业务实体
|
## 3️⃣ 关键业务实体
|
||||||
| 实体 | 所属模块 | 作用概述 |
|
| 实体 | 所属模块 | 作用概述 |
|
||||||
|------|----------|----------|
|
|------|----------|----------|
|
||||||
| `User` | manager | 记录用户基础信息(手机号、邮箱、密码、昵称、头像、生日、性别、真实姓名、微信绑定、邀请码等),配合 `UserMapper` 完成持久化。 |
|
| `User` | manager | 记录用户基础信息(手机号、邮箱、密码、昵称、头像、生日、性别、真实姓名、微信绑定、邀请码等)。 |
|
||||||
| `OssFile` | manager | 描述 OSS(对象存储)中文件的元数据(路径、大小、标签等),通过 `OssFileMapper` 进行增删改查。 |
|
| `AdminUser` | manager | 管理员用户表(bg_admin_user),B 端后台登录账号。 |
|
||||||
|
| `OssFile` | manager | 描述 OSS 中文件的元数据(路径、大小、标签等)。 |
|
||||||
| `UserInvite` | manager | 用户邀请记录表,记录邀请人与被邀请人的关系、邀请时间、奖励天数。 |
|
| `UserInvite` | manager | 用户邀请记录表,记录邀请人与被邀请人的关系、邀请时间、奖励天数。 |
|
||||||
| `UserFeedback` | manager | 用户反馈表(bg_user_feedback),记录用户提交的反馈类型和内容。 |
|
| `UserFeedback` | manager | 用户反馈表(bg_user_feedback),记录用户提交的反馈类型和内容。 |
|
||||||
| `RouteMenu` | manager | 路由菜单表(bg_route_menu),支持多级树形结构,通过 rootId/parentId 表达层级关系,openAccess 标识是否公开免费。 |
|
| `RouteMenu` | manager | 路由菜单表(bg_route_menu),支持多级树形结构,openAccess 标识是否公开免费。 |
|
||||||
| `FuncPermission` | manager | 功能权限表(bg_func_permission),定义功能点编码(func_code,最长12字符,唯一约束),daily_free_count 配置每日免费次数。 |
|
| `FuncPermission` | manager | 功能权限表(bg_func_permission),func_code 唯一,daily_free_count 配置每日免费次数。 |
|
||||||
| `UserRouteMenuStock` | manager | 用户路由菜单库存表(bg_user_route_menu_stock),记录用户拥有的菜单权限,支持时间维度。 |
|
| `UserRouteMenuStock` | manager | 用户路由菜单库存表,记录用户拥有的菜单权限,支持时间维度。 |
|
||||||
| `UserFuncPermissionStock` | manager | 用户功能权限库存表(bg_user_func_permission_stock),记录用户拥有的功能权限,支持时间/次数/复合维度。 |
|
| `UserFuncPermissionStock` | manager | 用户功能权限库存表,支持时间/次数/复合维度。 |
|
||||||
| `UserFuncUsageLog` | manager | 用户功能使用记录表(bg_user_func_usage_log),记录每次功能使用,用于免费次数统计和异常回退。 |
|
| `UserFuncUsageLog` | manager | 用户功能使用记录表,用于免费次数统计和异常回退。 |
|
||||||
| `Agreement` | manager | 协议表(bg_agreement),按协议码(agreementCode)分组,版本递增管理,支持启用/禁用状态。 |
|
| `Agreement` | manager | 协议表(bg_agreement),按协议码分组,版本递增管理。 |
|
||||||
| `MemberProduct` | manager | 会员商品表(bg_member_product),定义商品名称、价格、时长天数、排序、上下架状态。 |
|
| `MemberProduct` / `MemberOrder` / `MemberUser` | manager | 会员商品、订单、会员到期状态。 |
|
||||||
| `MemberOrder` | manager | 会员订单表(bg_member_order),记录用户下单信息、支付渠道、订单状态(0待支付/1已支付)、支付时间。 |
|
| `MemberFuncItem` / `MemberRouteItem` | manager | 会员套餐包含的功能权限项与菜单权限项。 |
|
||||||
| `MemberUser` | manager | 会员用户表(bg_member_user),记录用户的会员到期时间,一个用户一条记录。 |
|
| `PayWechatFlow` / `PayAlipayFlow` | manager | 微信 / 支付宝支付流水。 |
|
||||||
| `MemberFuncItem` | manager | 会员功能配置项表(bg_member_func_item),定义会员套餐包含的功能权限项(funcCode、countLimit、addCount)。 |
|
| `ChinaRegionsCode` | manager | 地区表(bg_china_regions_code),省/市/区三级,code 为主键。 |
|
||||||
| `MemberRouteItem` | manager | 会员菜单配置项表(bg_member_route_item),定义会员套餐包含的菜单权限项(routeId)。 |
|
| `JobCategory` | manager | 岗位类型表(bg_job_category),树形结构。 |
|
||||||
| `PayWechatFlow` | manager | 微信支付流水表(bg_pay_wechat_flow),记录微信支付的订单号、金额、状态、回调数据。 |
|
| `Company` | manager | 公司表(bg_company),公司基本信息、融资、规模、新闻动态、company_type、logo_url。 |
|
||||||
| `PayAlipayFlow` | manager | 支付宝支付流水表(bg_pay_alipay_flow),记录支付宝支付的订单号、金额、状态、交易号、回调数据。 |
|
| `Job` | manager | 岗位表(bg_job),岗位信息、薪资、标签、技能标签、要求专业、专业敏感度。 |
|
||||||
| `ChinaRegionsCode` | manager | 地区表(bg_china_regions_code),行政区划编码,省/市/区三级,code为主键。 |
|
| `JobRegionRelation` / `JobSkillTagRelation` | manager | 岗位-地区、岗位-技能标签关联表。 |
|
||||||
| `JobCategory` | manager | 岗位类型表(bg_job_category),树形结构,一级/二级分类。 |
|
| `Industry` | manager | 行业字典表(bg_industry),树形结构。 |
|
||||||
| `Company` | manager | 公司表(bg_company),公司基本信息、融资、规模、新闻动态等。 |
|
| `SkillTag` | manager | 技能标签表(bg_skill_tag),AI 自动入库,name 唯一索引。 |
|
||||||
| `Job` | manager | 岗位表(bg_job),岗位信息、薪资、标签、技能标签、要求专业、专业敏感度,关联公司和岗位类型。 |
|
| `MajorCategory` | manager | 专业分类表(bg_major_category),三级树形结构,用于专业匹配。 |
|
||||||
| `JobRegionRelation` | manager | 岗位-地区关联表(bg_job_region_relation),一个岗位可关联多个地区。 |
|
| `RecruitAnnouncement` | manager | 招聘公告主表(bg_recruit_announcement),公告标题、公司简介、面向对象、专业要求、招聘岗位、笔试、投递时间与入口(announcement_url/apply_url/apply_email/invite_code)、来源、clean_status、status。 |
|
||||||
| `JobSkillTagRelation` | manager | 岗位-技能标签关联表(bg_job_skill_tag_relation),AI提取的技能标签与岗位的关联,用于匹配度计算。 |
|
| `RecruitAnnouncement{Year,Batch,Tag,City,Category,Industry,Education}` | manager | 公告的 7 张多值维度关联表,统一结构(id + announcement_id + 值字段 + create_time),直接存原始值不引字典表,无唯一约束。 |
|
||||||
| `Industry` | manager | 行业字典表(bg_industry),树形结构,一级/二级分类。 |
|
| `MentorMockChat` | manager | 导师模拟对话表(bg_mentor_mock_chat),缓存 AI 生成的对话,按导师 ID 唯一。 |
|
||||||
| `SkillTag` | manager | 技能标签表(bg_skill_tag),AI自动入库,name唯一索引,用于匹配度计算。 |
|
| `UserJobFavorite` / `UserJobApplication` / `UserJobDislike` / `UserJobIntention` | manager | 用户收藏、投递、不感兴趣、求职意向。 |
|
||||||
| `MajorCategory` | manager | 专业分类表(bg_major_category),基于教育部专业目录,三级树形结构,用于专业匹配。 |
|
| `UserProfile` + 5 张子表 | manager | 用户个人资料及教育/工作/实习/项目/竞赛经历,主表存维度评分字段。 |
|
||||||
| `UserJobFavorite` | manager | 用户收藏岗位表(bg_user_job_favorite),用户与岗位唯一约束。 |
|
| `UserProfileSkillTagRelation` | manager | 用户技能标签关联表,AI 自动识别生成。 |
|
||||||
| `UserJobApplication` | manager | 用户投递记录表(bg_user_job_application),记录投递状态流转。 |
|
| `UserResume` + 5 张子表 | manager | 用户简历及教育/工作/实习/项目/竞赛经历,一个用户可有多份简历。 |
|
||||||
| `UserJobDislike` | manager | 用户不感兴趣记录表(bg_user_job_dislike),冗余公司ID/地区编码/行业ID方便推荐过滤。 |
|
| `ResumeDiagnosisReport` / `ResumeDiagnosisIssue` | manager | 简历诊断报告与问题明细。 |
|
||||||
| `UserJobIntention` | manager | 用户求职意向表(bg_user_job_intention),存储意向岗位类型、城市、行业(JSON数组)和工作类型。 |
|
| `JobAgentConfig` / `JobAgentChatMessage` / `UserJobCustomizeResume` | manager | 求职助手配置、对话消息、岗位定制简历。 |
|
||||||
| `UserProfile` | manager | 用户个人资料表(bg_user_profile),存储维度评分字段(学校等级、公司背书、经历时长、职责深度、量化产出、荣誉、技能标签、证书标签等)。 |
|
| `Message` / `MessageRead` | manager | 站内信消息与已读状态。 |
|
||||||
| `UserProfileEducation` | manager | 用户教育经历表(bg_user_profile_education,profile子表)。 |
|
|
||||||
| `UserProfileWork` | manager | 用户工作经历表(bg_user_profile_work,profile子表)。 |
|
|
||||||
| `UserProfileInternship` | manager | 用户实习经历表(bg_user_profile_internship,profile子表)。 |
|
|
||||||
| `UserProfileProject` | manager | 用户项目经历表(bg_user_profile_project,profile子表)。 |
|
|
||||||
| `UserProfileCompetition` | manager | 用户竞赛经历表(bg_user_profile_competition,profile子表)。 |
|
|
||||||
| `UserProfileSkillTagRelation` | manager | 用户技能标签关联表(bg_user_profile_skill_tag_relation),AI自动识别生成。 |
|
|
||||||
| `UserResume` | manager | 用户简历表(bg_user_resume),简历维度信息+个人基本信息合并存储,一个用户可有多份简历。 |
|
|
||||||
| `UserResumeEducation` | manager | 简历-教育经历表(bg_user_resume_education)。 |
|
|
||||||
| `UserResumeWork` | manager | 简历-工作经历表(bg_user_resume_work)。 |
|
|
||||||
| `UserResumeInternship` | manager | 简历-实习经历表(bg_user_resume_internship)。 |
|
|
||||||
| `UserResumeProject` | manager | 简历-项目经历表(bg_user_resume_project)。 |
|
|
||||||
| `UserResumeCompetition` | manager | 简历-竞赛经历表(bg_user_resume_competition)。 |
|
|
||||||
| `ResumeDiagnosisReport` | manager | 简历诊断报告表(bg_resume_diagnosis_report),AI整体诊断评级(A/B/C/D)和各类问题统计。 |
|
|
||||||
| `ResumeDiagnosisIssue` | manager | 简历诊断问题表(bg_resume_diagnosis_issue),AI对简历各模块的诊断发现、建议、改写内容。 |
|
|
||||||
| `JobAgentConfig` | manager | 求职助手配置表(bg_job_agent_config),一个用户一条记录,存储Agent模式、投递目标、网申常见问题预设答案。 |
|
|
||||||
| `JobAgentChatMessage` | manager | 求职助手对话消息表(bg_job_agent_chat_message),完整对话流,含4种消息类型。 |
|
|
||||||
| `UserJobCustomizeResume` | manager | 用户岗位定制简历表(bg_user_job_customize_resume),唯一索引(user_id, job_id)。 |
|
|
||||||
| `Message` | manager | 站内信消息表(bg_message),支持系统(1)/运营(2)/订单(3)消息,支持指定用户和全员推送。 |
|
|
||||||
| `MessageRead` | manager | 消息已读状态表(bg_message_read),唯一索引(message_id, user_id)。 |
|
|
||||||
| `AppJobData` | manager | 爬虫岗位原始数据表(app_job_data),存储爬虫抓取的原始岗位数据,供清洗服务读取。 |
|
|
||||||
|
|
||||||
## 4️⃣ 权限体系设计
|
## 4️⃣ 权限体系设计
|
||||||
### 整体架构
|
### 整体架构
|
||||||
- **前端**:路由控制菜单/页面可见性,登录后拉取用户有效菜单树(含公开免费菜单 + 库存授权菜单)
|
- **前端**:路由控制菜单/页面可见性,登录后拉取用户有效菜单树(含公开免费菜单 + 库存授权菜单)
|
||||||
- **后端**:AOP 切面拦截 `@FuncPermission` 注解,校验权限 + 扣减库存,业务异常自动回退次数
|
- **后端**:AOP 切面拦截 `@FuncPermission` 注解,校验权限 + 扣减库存,业务异常自动回退次数
|
||||||
- **权限来源**:会员购买成功后通过 `MemberGrantService` 统一发放到库存表
|
- **权限来源**:会员购买成功后通过 `MemberGrantService` 统一发放到库存表
|
||||||
|
- **免鉴权路径**:由配置 `app.ignore.urls` 控制(当前为 `/public/**,/job/list,/job/detail,/route/menu`),`antMatchers` 匹配完整路径,因此 `/public` 必须写在类级 `@RequestMapping` 的最前面
|
||||||
|
|
||||||
### 数据库表(5张)
|
### 数据库表(5张)
|
||||||
| 表名 | 说明 |
|
| 表名 | 说明 |
|
||||||
@@ -406,7 +454,7 @@ offerpie/back-end
|
|||||||
|
|
||||||
## 5️⃣ 会员商品与支付体系
|
## 5️⃣ 会员商品与支付体系
|
||||||
### 整体架构
|
### 整体架构
|
||||||
- **商品管理**:`MemberProduct` 定义会员套餐(名称、价格、时长天数、排序、上下架)
|
- **商品管理**:`MemberProduct` 定义会员套餐(名称、价格、时长天数、排序、上下架),B 端由 `AdminProductService` 维护
|
||||||
- **下单流程**:用户选择商品 → 创建订单(`MemberOrder`) → 根据支付渠道调用微信/支付宝下单 → 返回支付凭据
|
- **下单流程**:用户选择商品 → 创建订单(`MemberOrder`) → 根据支付渠道调用微信/支付宝下单 → 返回支付凭据
|
||||||
- **支付回调**:微信/支付宝回调 → 验签 → 更新流水 → 调用 `handlePaySuccess` → 更新订单状态 → 发放权益
|
- **支付回调**:微信/支付宝回调 → 验签 → 更新流水 → 调用 `handlePaySuccess` → 更新订单状态 → 发放权益
|
||||||
- **权益发放**:`MemberGrantService.grant()` → 续费会员时长 → 按 `MemberFuncItem` 发放功能权限 → 按 `MemberRouteItem` 发放菜单权限
|
- **权益发放**:`MemberGrantService.grant()` → 续费会员时长 → 按 `MemberFuncItem` 发放功能权限 → 按 `MemberRouteItem` 发放菜单权限
|
||||||
@@ -438,9 +486,9 @@ offerpie/back-end
|
|||||||
| **短信** | `AliYunSmsAbility`(阿里云短信发送,ConditionalOnProperty) | `common/sms` |
|
| **短信** | `AliYunSmsAbility`(阿里云短信发送,ConditionalOnProperty) | `common/sms` |
|
||||||
| **OSS** | `AliOssAbility`(阿里云OSS上传/下载/签名URL,ConditionalOnProperty) | `common/oss` |
|
| **OSS** | `AliOssAbility`(阿里云OSS上传/下载/签名URL,ConditionalOnProperty) | `common/oss` |
|
||||||
| **AI** | `AiChatAbility`(OpenAI 兼容多供应商对话)、`AiChatConfig`(供应商配置)、`AiResponseCleanTool`(AI响应文本清洗) | `common/ai` |
|
| **AI** | `AiChatAbility`(OpenAI 兼容多供应商对话)、`AiChatConfig`(供应商配置)、`AiResponseCleanTool`(AI响应文本清洗) | `common/ai` |
|
||||||
| **支付宝** | `AlipayClientConfig`(客户端配置)、`AlipayPagePayAbility`(电脑网站支付)、`AlipayNotifyController`(回调验签)、`AlipayNotifyMessageAbstract`(回调抽象) | `common/alipay` |
|
| **支付宝** | `AlipayClientConfig`、`AlipayPagePayAbility`、`AlipayNotifyController`、`AlipayNotifyMessageAbstract` | `common/alipay` |
|
||||||
| **微信支付** | `WxJsPayAbility`, `WxNativePayAbility`, `WxTransferPayAbility`, `WxPayNotifyController`, `WxPayNotifyMessageAbstract` | `common/wxPay` |
|
| **微信支付** | `WxJsPayAbility`, `WxNativePayAbility`, `WxTransferPayAbility`, `WxPayNotifyController`, `WxPayNotifyMessageAbstract`, `server/TransferServer` 及 `server/model/*` | `common/wxPay` |
|
||||||
| **全局异常** | `GlobalExceptionAdvice`, `BusinessException`, `BusinessExpCodeEnum`, `BusinessExpCodeOperations` | `common/exception`, `common/web` |
|
| **全局异常** | `GlobalExceptionAdvice`(web包), `BusinessException`, `BusinessExpCodeEnum`, `BusinessExpCodeOperations`(exception包) | `common/exception`, `common/web` |
|
||||||
| **日志 & AOP** | `ControllerLogAspect`, `LoggingOriginalRequestFilter`, `TraceIdFilter`, `SqlLoggerInterceptor` | `common/aop`, `common/interceptor` |
|
| **日志 & AOP** | `ControllerLogAspect`, `LoggingOriginalRequestFilter`, `TraceIdFilter`, `SqlLoggerInterceptor` | `common/aop`, `common/interceptor` |
|
||||||
| **工具类** | `HttpTool`, `HttpIpTool`, `AuthenticTool`, `ObjectTool`, `UserSecurityTool`, `VerifyImageCodeUtils`, `RedisServerTool` | `common/tool` |
|
| **工具类** | `HttpTool`, `HttpIpTool`, `AuthenticTool`, `ObjectTool`, `UserSecurityTool`, `VerifyImageCodeUtils`, `RedisServerTool` | `common/tool` |
|
||||||
| **统一返回体** | `UnifiedResponse`, `UnifiedResponseBodyAdvice`, `PageParam`, `PageResult` | `common/pojo`, `common/web` |
|
| **统一返回体** | `UnifiedResponse`, `UnifiedResponseBodyAdvice`, `PageParam`, `PageResult` | `common/pojo`, `common/web` |
|
||||||
@@ -448,9 +496,10 @@ offerpie/back-end
|
|||||||
|
|
||||||
## 7️⃣ 岗位列表与匹配度计算设计
|
## 7️⃣ 岗位列表与匹配度计算设计
|
||||||
### 整体架构
|
### 整体架构
|
||||||
- **触发时机**:用户访问岗位列表接口(支持筛选条件:地区/岗位类型/行业/工作类型)
|
- **触发时机**:用户访问岗位列表接口(支持筛选条件:地区/岗位类型/行业/工作类型/公司类型/公司ID)
|
||||||
- **核心流程**:扩展层级筛选 → 查询不感兴趣记录 → 扩展排除列表 → SQL分页查询 → 查询收藏状态 → 批量计算匹配度 → 组装返回
|
- **核心流程**:扩展层级筛选 → 查询不感兴趣记录 → 扩展排除列表 → SQL分页查询 → 查询收藏状态 → 批量计算匹配度 → 组装返回
|
||||||
- **匹配度计算**:教育30% + 经历30% + 技能40%,在Java层批量计算
|
- **匹配度计算**:教育30% + 经历30% + 技能40%,在Java层批量计算
|
||||||
|
- **分页策略**:`pageParam.setSearchCount(false)`,不查总数(前端按「加载更多」处理)
|
||||||
|
|
||||||
### 匹配度计算规则
|
### 匹配度计算规则
|
||||||
|
|
||||||
@@ -480,7 +529,45 @@ offerpie/back-end
|
|||||||
- **JobService**(client-api):岗位列表查询主流程编排
|
- **JobService**(client-api):岗位列表查询主流程编排
|
||||||
- **JobMatchService**(manager):批量计算匹配度(教育/经历/技能三个维度)
|
- **JobMatchService**(manager):批量计算匹配度(教育/经历/技能三个维度)
|
||||||
|
|
||||||
## 8️⃣ 用户简历分析设计
|
## 8️⃣ 招聘公告模块设计
|
||||||
|
### 数据模型
|
||||||
|
- **一主七从**:主表 `bg_recruit_announcement` + 7 张多值维度关联表(届数/批次/标签/城市/岗位大类/行业/学历)
|
||||||
|
- 从表统一结构:`id` + `announcement_id` + 单个值字段 + `create_time`,雪花主键,只有普通索引,**无唯一约束**(重复写入需按 announcement_id 先删后插)
|
||||||
|
- 刻意不引字典表,从表直接存原始文本值,避免归一化限制 AI 解析结果;筛选按名称匹配
|
||||||
|
- 双状态:`clean_status`(0-清洗中 1-完成)、`status`(0-失效 1-有效),有效数据口径为两者皆为 1
|
||||||
|
|
||||||
|
### 接口一览(`/public/recruitAnnouncement`,全部免鉴权)
|
||||||
|
| 接口 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `POST /page` | 分页列表,支持关键字(公司名/招聘岗位 LIKE)+ 发布时间区间 + 投递截止区间 + 公司类型 + 7 个多值维度筛选(条件间为「且」) |
|
||||||
|
| `GET /detail` | 详情,返回主表字段(不含三个投递入口地址)+ 公司 logo 与类型 + 全部 7 个多值维度 |
|
||||||
|
| `POST /recommend` | 推荐,按传入公告的行业或岗位大类命中任一即推荐 |
|
||||||
|
| `GET /statistics` | 统计(今日新增/本月新增/累计),一次条件聚合出三个计数 |
|
||||||
|
| `GET /lastUpdateTime` | 最后更新时间(按 id 倒序取第一条的 create_time) |
|
||||||
|
| `GET /announcementUrl` `/applyUrl` `/applyEmail` | 按公告 id 分别查询公告地址、投递地址、投递邮箱(只查单列) |
|
||||||
|
| `GET /batch` `/category` `/city` `/education` `/industry` `/tag` `/year` | 各维度筛选项,`GROUP BY` 值字段去重 |
|
||||||
|
|
||||||
|
### 实现约定
|
||||||
|
- **列表与推荐**:主表 + `bg_company` INNER JOIN 只取单值字段,多值维度不进 SQL;拿到本页 id 后按 `IN` 批量查从表,在 Java 侧 `groupingBy` + `mapping` 组装,避免 N+1
|
||||||
|
- **详情**:`selectById` 取 PO + 单独查公司两列 + 7 张从表各查一次,共 9 次查询,全部走主键或 `idx_announcement_id`
|
||||||
|
- **筛选**:多值维度用 `ra.id IN (子查询)` 半连接;推荐场景两个维度是「或」关系,用 `UNION` 合并后再套单个 `IN`(避免 `OR` 连接多个 `IN` 无法半连接优化),`<trim prefixOverrides="UNION">` 处理动态拼接
|
||||||
|
- **统计时区**:固定 `ZoneId.of("Asia/Shanghai")` 计算今日零点与本月 1 号零点,不依赖 JVM 默认时区
|
||||||
|
- **分页**:`setSearchCount(false)`,`total` 恒为 0
|
||||||
|
|
||||||
|
## 9️⃣ 导师模块设计(对接简历星球开放平台)
|
||||||
|
### 整体架构
|
||||||
|
- **远程客户端**:`JsxqClient`(manager/remote/jsxq)统一拼公共请求头、拆平台 `{code,msg,data,ok}` 包装、按类型反序列化 data,业务接口为其 public 方法
|
||||||
|
- **配置**:`JsxqProperties` 管理域名、令牌、租户
|
||||||
|
- **对外接口**:`MentorController`(client-api,`/public/mentor`)透传导师分页列表、导师完整信息、老师话题、老师评价
|
||||||
|
- **模拟对话**:`MentorMockChatController` + `MentorMockChatService`(manager,`/public/mentor/mockChat`)
|
||||||
|
|
||||||
|
### 模拟对话流程
|
||||||
|
1. 按 mentorId 查 `bg_mentor_mock_chat`,有缓存直接返回
|
||||||
|
2. 无缓存则通过 `JsxqClient` 获取导师信息与话题
|
||||||
|
3. 调用 `AiChatAbility` 生成对话
|
||||||
|
4. 结果入库缓存,再返回
|
||||||
|
|
||||||
|
## 🔟 用户简历分析设计
|
||||||
### 整体架构
|
### 整体架构
|
||||||
- **触发时机**:用户保存个人资料(主表或任意子表)后异步触发
|
- **触发时机**:用户保存个人资料(主表或任意子表)后异步触发
|
||||||
- **分析流程**:三次 AI 调用 → 第一次综合分析(学校等级、公司背书、经历时长、职责深度、量化产出、荣誉),第二次专业归一化(匹配到 bg_major_category ID),第三次技能提取(自由提取,入 bg_skill_tag)
|
- **分析流程**:三次 AI 调用 → 第一次综合分析(学校等级、公司背书、经历时长、职责深度、量化产出、荣誉),第二次专业归一化(匹配到 bg_major_category ID),第三次技能提取(自由提取,入 bg_skill_tag)
|
||||||
@@ -498,7 +585,7 @@ offerpie/back-end
|
|||||||
- 每次 AI 调用独立容错,某一次失败不影响其他维度
|
- 每次 AI 调用独立容错,某一次失败不影响其他维度
|
||||||
- 核心服务:**UserProfileAnalyzeService**(manager)
|
- 核心服务:**UserProfileAnalyzeService**(manager)
|
||||||
|
|
||||||
## 9️⃣ 邀请模块设计
|
## 1️⃣1️⃣ 邀请模块设计
|
||||||
### 数据库表(1张)
|
### 数据库表(1张)
|
||||||
| 表名 | 说明 |
|
| 表名 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
@@ -510,17 +597,41 @@ offerpie/back-end
|
|||||||
- 注册逻辑由 `UserRegisterService` 独立承担,`LoginService` 委托调用
|
- 注册逻辑由 `UserRegisterService` 独立承担,`LoginService` 委托调用
|
||||||
- 邀请统计由 `UserManageService.getInviteStats()` 提供(累计邀请人数、累计获得会员天数)
|
- 邀请统计由 `UserManageService.getInviteStats()` 提供(累计邀请人数、累计获得会员天数)
|
||||||
|
|
||||||
## 🔟 构建与运行
|
## 1️⃣2️⃣ B 端管理后台设计
|
||||||
- **父 POM**(`back-end/pom.xml`)统一管理子模块的依赖与插件。
|
### 整体架构
|
||||||
- 子模块 (`client-api`, `common`, `manager`) 均可单独 `mvn clean install`,生成各自的 jar 包。
|
- 独立启动类 `AdminApplication`,独立 `application.yml`(含 dev/test/prod),Thymeleaf 渲染页面 + REST 接口
|
||||||
- **启动入口**:运行 `client-api` 中的 `ClientApplication`,Spring Boot 会自动扫描并加载 `common`(配置、拦截、工具)以及 `manager` 中声明的 Mapper 与 Service。
|
- `PageController` 返回视图名,不参与 `UnifiedResponseBodyAdvice` 统一包装
|
||||||
- **条件加载**:多个能力组件使用 `@ConditionalOnProperty` 按配置启用(OSS、SMS、Alipay),未配置时不加载。
|
- 复用 `manager` 的全部 Mapper 与 `common` 的技术设施,不重复定义实体
|
||||||
|
|
||||||
## 1️⃣1️⃣ 小结
|
### 登录与安全
|
||||||
- 项目采用 **三层结构**:
|
- 账号密码登录(`bg_admin_user`),JWT 令牌写入 Redis
|
||||||
1. **client-api** → C 端 REST 接口 + 功能权限校验 + 路由菜单 + 用户管理 + 会员体系 + 支付回调。
|
- 失败累计:连续 3 次失败锁定账号 4 小时
|
||||||
2. **manager** → 业务实体、Mapper 与业务 API/Service,B 端 + C 端共享层。
|
- Redis Key:`login:token:{adminId}`、`login:fail:{username}`、`login:lock:{username}`(常量见 `AdminRedisKeyName`)
|
||||||
3. **common** → 所有层共同依赖的底层设施(配置、工具、拦截、异常、支付、OSS、短信、AI、邮件等)。
|
- 登录成功写入 Redis 并设置 Cookie,支持多设备踢出
|
||||||
|
|
||||||
|
### 管理能力
|
||||||
|
| 模块 | 能力 |
|
||||||
|
|------|------|
|
||||||
|
| 用户管理 | 分页列表(含每人简历数量)、总数/今日/本周/本月新增统计、禁用与启用 |
|
||||||
|
| 订单管理 | 分页列表(关联用户与商品信息)、总数/已支付数/今日本周本月支付数与收入统计 |
|
||||||
|
| 商品管理 | 会员商品新增、编辑、分页查询 |
|
||||||
|
|
||||||
|
## 1️⃣3️⃣ 构建与运行
|
||||||
|
- **父 POM**(`back-end/pom.xml`)统一管理子模块的依赖与插件,Java 17。
|
||||||
|
- 子模块(`client-api`, `admin`, `common`, `manager`)均可单独 `mvn clean install`,生成各自的 jar 包。
|
||||||
|
- **启动入口**:C 端运行 `client-api` 的 `ClientApplication`;B 端运行 `admin` 的 `AdminApplication`。两者都会自动扫描并加载 `common`(配置、拦截、工具)与 `manager` 中声明的 Mapper 与 Service。
|
||||||
|
- **MyBatis XML**:`mapper-locations: classpath*:mapper/**/*.xml`,放在 `manager/src/main/resources/mapper/` 下。注意同一方法不能同时存在 `@Select` 注解和 XML 语句,否则启动报 `Mapped Statements collection already contains value`。
|
||||||
|
- **条件加载**:多个能力组件使用 `@ConditionalOnProperty` 按配置启用(OSS、SMS、Alipay),未配置时不加载。
|
||||||
|
- **部署**:`client-api` 与 `admin` 各有独立 `Dockerfile`、`Jenkinsfile`(含 `.test` 变体)、`nginx.conf` 与 `docker-compose.*.yml`。
|
||||||
|
|
||||||
|
## 1️⃣4️⃣ 小结
|
||||||
|
- 项目采用 **四模块结构**:
|
||||||
|
1. **client-api** → C 端 REST 接口 + 功能权限校验 + 路由菜单 + 用户管理 + 会员体系 + 支付回调 + 招聘公告 + 导师。
|
||||||
|
2. **admin** → B 端管理后台(管理员登录、用户/订单/商品管理,Thymeleaf 页面)。
|
||||||
|
3. **manager** → 业务实体、Mapper、远程调用与业务 API/Service,B 端 + C 端共享层。
|
||||||
|
4. **common** → 所有层共同依赖的底层设施(配置、工具、拦截、异常、支付、OSS、短信、AI、邮件等)。
|
||||||
|
- 依赖方向单向:`client-api` / `admin` → `manager` → `common`。跨模块传参需在 `manager` 侧定义 DTO/VO。
|
||||||
- 权限体系分两层:前端路由控制菜单可见性,后端切面控制功能点权限与库存扣减。
|
- 权限体系分两层:前端路由控制菜单可见性,后端切面控制功能点权限与库存扣减。
|
||||||
- 会员体系:商品下单 → 支付回调 → `MemberGrantService` 统一发放权益(会员时长 + 功能权限 + 菜单权限)。
|
- 会员体系:商品下单 → 支付回调 → `MemberGrantService` 统一发放权益(会员时长 + 功能权限 + 菜单权限)。
|
||||||
- 支付双通道:微信Native支付 + 支付宝电脑网站支付,各自独立流水表和回调处理。
|
- 支付双通道:微信Native支付 + 支付宝电脑网站支付,各自独立流水表和回调处理。
|
||||||
|
- 数据来源:招聘公告由 `offerpie_spider` 采集,岗位与公司由 `offerpie_job_cleaner` 清洗入库,后端只消费业务表。
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# ==================== 第一阶段:编译 ====================
|
||||||
|
FROM maven:3.8-openjdk-17 AS builder
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# 阿里云 Maven 镜像加速
|
||||||
|
COPY settings.xml /root/.m2/settings.xml
|
||||||
|
|
||||||
|
# 先拷贝 pom 利用 Docker 缓存层(依赖不变时跳过下载)
|
||||||
|
COPY pom.xml .
|
||||||
|
COPY common/pom.xml common/pom.xml
|
||||||
|
COPY manager/pom.xml manager/pom.xml
|
||||||
|
COPY admin/pom.xml admin/pom.xml
|
||||||
|
|
||||||
|
# 拷贝源码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 构建(从父 pom 编译,admin 依赖 common 和 manager)
|
||||||
|
RUN --mount=type=cache,target=/root/.m2/repository \
|
||||||
|
mvn clean package -pl admin -am -DskipTests
|
||||||
|
|
||||||
|
# ==================== 第二阶段:运行 ====================
|
||||||
|
FROM alibabadragonwell/dragonwell:17-anolis
|
||||||
|
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
ENV PROFILES_ACTIVE=prod
|
||||||
|
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
|
||||||
|
-XX:+UseG1GC \
|
||||||
|
-XX:+HeapDumpOnOutOfMemoryError \
|
||||||
|
-XX:HeapDumpPath=/app/logs/heapdump.hprof \
|
||||||
|
-Dfile.encoding=UTF-8 \
|
||||||
|
-Duser.timezone=Asia/Shanghai"
|
||||||
|
|
||||||
|
# 时区 + curl(健康检查用)
|
||||||
|
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \
|
||||||
|
&& yum install -y curl && yum clean all
|
||||||
|
|
||||||
|
RUN mkdir -p /app/logs
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
EXPOSE 8081
|
||||||
|
|
||||||
|
COPY --from=builder /build/admin/target/admin.jar ./admin.jar
|
||||||
|
|
||||||
|
CMD java $JAVA_OPTS -jar admin.jar
|
||||||
Vendored
+202
@@ -0,0 +1,202 @@
|
|||||||
|
/**
|
||||||
|
* OfferPie Backend Admin 蓝绿部署流水线
|
||||||
|
*
|
||||||
|
* 架构:Jenkins 本地编译 → scp 镜像到目标机 → SSH 远程蓝绿切换
|
||||||
|
* 目标机目录:/opt/offerpie/admin/
|
||||||
|
*/
|
||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
choice(name: 'BRANCH', choices: ['master', 'pre', 'dev', 'test'], description: '选择要部署的分支')
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
// 目标服务器配置
|
||||||
|
DEPLOY_HOST = '8.148.237.97'
|
||||||
|
DEPLOY_PORT = '22'
|
||||||
|
DEPLOY_USER = 'root'
|
||||||
|
DEPLOY_PASS = 'Mzpy520@126.com'
|
||||||
|
|
||||||
|
// 项目配置
|
||||||
|
IMAGE_NAME = 'offerpie-backend-admin'
|
||||||
|
IMAGE_TAG = 'latest'
|
||||||
|
CONTAINER_PREFIX = 'offerpie-backend-admin'
|
||||||
|
REMOTE_DIR = '/opt/offerpie/admin'
|
||||||
|
HEALTH_URL = 'http://localhost:8081/admin/public/actuator/health'
|
||||||
|
|
||||||
|
// SSH 命令前缀
|
||||||
|
SSH_CMD = "sshpass -p '${DEPLOY_PASS}' ssh -o StrictHostKeyChecking=no -p ${DEPLOY_PORT} ${DEPLOY_USER}@${DEPLOY_HOST}"
|
||||||
|
SCP_CMD = "sshpass -p '${DEPLOY_PASS}' scp -o StrictHostKeyChecking=no -P ${DEPLOY_PORT}"
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('环境检查') {
|
||||||
|
steps {
|
||||||
|
sh 'sshpass -V'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('拉取代码') {
|
||||||
|
steps {
|
||||||
|
echo "拉取 ${params.BRANCH} 分支代码"
|
||||||
|
git branch: "${params.BRANCH}",
|
||||||
|
credentialsId: 'gitea-fab089c1-b55d-4b58-9fad',
|
||||||
|
url: 'http://git.jianshixingqiu.com/offerpai/offerpai_backend.git'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('本地编译') {
|
||||||
|
steps {
|
||||||
|
echo "开始构建镜像"
|
||||||
|
sh "docker build -f admin/Dockerfile -t ${IMAGE_NAME}:${IMAGE_TAG} ."
|
||||||
|
echo "导出镜像"
|
||||||
|
sh "docker save -o ${IMAGE_NAME}.tar ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('文件传输') {
|
||||||
|
steps {
|
||||||
|
echo "传输文件到目标服务器"
|
||||||
|
sh "${SSH_CMD} 'mkdir -p ${REMOTE_DIR}'"
|
||||||
|
sh "${SCP_CMD} ${IMAGE_NAME}.tar ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/"
|
||||||
|
sh "${SCP_CMD} admin/nginx.conf ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/nginx.conf"
|
||||||
|
sh "${SCP_CMD} docker-compose.admin.yml ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/docker-compose.yml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('加载镜像') {
|
||||||
|
steps {
|
||||||
|
sh "${SSH_CMD} 'docker load < ${REMOTE_DIR}/${IMAGE_NAME}.tar'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('检测部署目标') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def blueRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-blue'", returnStdout: true).trim()
|
||||||
|
def greenRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-green'", returnStdout: true).trim()
|
||||||
|
|
||||||
|
env.DEPLOY_TARGET = ''
|
||||||
|
|
||||||
|
if (blueRunning && !greenRunning) {
|
||||||
|
env.DEPLOY_TARGET = 'green'
|
||||||
|
}
|
||||||
|
if (greenRunning && !blueRunning) {
|
||||||
|
env.DEPLOY_TARGET = 'blue'
|
||||||
|
}
|
||||||
|
if (!env.DEPLOY_TARGET) {
|
||||||
|
echo "当前环境未部署服务或状态异常,默认部署 blue"
|
||||||
|
env.DEPLOY_TARGET = 'blue'
|
||||||
|
}
|
||||||
|
|
||||||
|
env.OTHER_TARGET = (env.DEPLOY_TARGET == 'blue') ? 'green' : 'blue'
|
||||||
|
echo "当前激活: ${env.OTHER_TARGET},即将部署: ${env.DEPLOY_TARGET}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('启动新版本') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def existingContainer = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'", returnStdout: true).trim()
|
||||||
|
if (existingContainer) {
|
||||||
|
sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'"
|
||||||
|
}
|
||||||
|
sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d ${env.DEPLOY_TARGET}'"
|
||||||
|
sh 'sleep 35'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('检查Nginx') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def nginxExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim()
|
||||||
|
if (!nginxExists) {
|
||||||
|
echo "首次部署,初始化 Nginx 容器"
|
||||||
|
sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d nginx'"
|
||||||
|
sh 'sleep 3'
|
||||||
|
} else {
|
||||||
|
def nginxRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim()
|
||||||
|
if (!nginxRunning) {
|
||||||
|
echo "Nginx 容器已停止,重新启动"
|
||||||
|
sh "${SSH_CMD} 'docker start ${CONTAINER_PREFIX}-nginx'"
|
||||||
|
sh 'sleep 2'
|
||||||
|
}
|
||||||
|
echo "Nginx 容器已存在且运行中"
|
||||||
|
}
|
||||||
|
// docker cp nginx.conf 进容器(覆盖更新配置模板)
|
||||||
|
sh "${SSH_CMD} 'docker cp ${REMOTE_DIR}/nginx.conf ${CONTAINER_PREFIX}-nginx:/etc/nginx/nginx.conf'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('健康检查') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def maxRetries = 3
|
||||||
|
def retryCount = 0
|
||||||
|
def healthy = false
|
||||||
|
|
||||||
|
while (retryCount < maxRetries && !healthy) {
|
||||||
|
try {
|
||||||
|
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}'"
|
||||||
|
healthy = true
|
||||||
|
} catch (Exception e) {
|
||||||
|
retryCount++
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
echo "健康检查失败,5秒后重试 (${retryCount}/${maxRetries})"
|
||||||
|
sleep 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!healthy) {
|
||||||
|
error "健康检查失败,部署中止"
|
||||||
|
}
|
||||||
|
echo "✅ ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} 健康检查通过"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('切换流量') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx sed -i \"s/proxy_pass http:\\/\\/\\(blue\\|green\\):8081;/proxy_pass http:\\/\\/${env.DEPLOY_TARGET}:8081;/\" /etc/nginx/nginx.conf'"
|
||||||
|
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload'"
|
||||||
|
echo "✅ 流量已切换到 ${env.DEPLOY_TARGET}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('删除旧版本') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def otherExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}'", returnStdout: true).trim()
|
||||||
|
if (otherExists) {
|
||||||
|
sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}'"
|
||||||
|
echo "旧版本 ${env.OTHER_TARGET} 已删除"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('清理') {
|
||||||
|
steps {
|
||||||
|
sh "${SSH_CMD} 'rm -f ${REMOTE_DIR}/${IMAGE_NAME}.tar'"
|
||||||
|
sh "rm -f ${IMAGE_NAME}.tar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
echo "✅ 蓝绿部署成功!当前运行: ${env.DEPLOY_TARGET}"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo '❌ 部署失败,请检查日志'
|
||||||
|
sh "rm -f ${IMAGE_NAME}.tar || true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* OfferPie Backend Admin 蓝绿部署流水线(测试环境)
|
||||||
|
*
|
||||||
|
* 架构:Jenkins 本地编译 → scp 镜像到目标机 → SSH 远程蓝绿切换
|
||||||
|
* 目标机目录:/opt/offerpie/admin/
|
||||||
|
*
|
||||||
|
* 与生产差异:
|
||||||
|
* - 部署目标为测试服务器
|
||||||
|
* - 默认分支 test
|
||||||
|
* - 镜像 tag = test(与生产 latest 隔离,避免同一 Jenkins 并发构建串包)
|
||||||
|
* - tar 文件名带 tag(双保险)
|
||||||
|
* - 运行时 PROFILES_ACTIVE=test(在 docker-compose.test.admin.yml 中设置)
|
||||||
|
*/
|
||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
choice(name: 'BRANCH', choices: ['test', 'dev', 'pre', 'master'], description: '选择要部署的分支')
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
// 目标服务器配置(测试)
|
||||||
|
DEPLOY_HOST = '8.163.131.234'
|
||||||
|
DEPLOY_PORT = '22'
|
||||||
|
DEPLOY_USER = 'root'
|
||||||
|
DEPLOY_PASS = 'Mzpy520@126.com'
|
||||||
|
|
||||||
|
// 项目配置
|
||||||
|
IMAGE_NAME = 'offerpie-backend-admin'
|
||||||
|
IMAGE_TAG = 'test'
|
||||||
|
IMAGE_TAR = 'offerpie-backend-admin-test.tar'
|
||||||
|
CONTAINER_PREFIX = 'offerpie-backend-admin'
|
||||||
|
REMOTE_DIR = '/opt/offerpie/admin'
|
||||||
|
HEALTH_URL = 'http://localhost:8081/admin/public/actuator/health'
|
||||||
|
|
||||||
|
// SSH 命令前缀
|
||||||
|
SSH_CMD = "sshpass -p '${DEPLOY_PASS}' ssh -o StrictHostKeyChecking=no -p ${DEPLOY_PORT} ${DEPLOY_USER}@${DEPLOY_HOST}"
|
||||||
|
SCP_CMD = "sshpass -p '${DEPLOY_PASS}' scp -o StrictHostKeyChecking=no -P ${DEPLOY_PORT}"
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('环境检查') {
|
||||||
|
steps {
|
||||||
|
sh 'sshpass -V'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('拉取代码') {
|
||||||
|
steps {
|
||||||
|
echo "拉取 ${params.BRANCH} 分支代码"
|
||||||
|
git branch: "${params.BRANCH}",
|
||||||
|
credentialsId: 'gitea-fab089c1-b55d-4b58-9fad',
|
||||||
|
url: 'http://git.jianshixingqiu.com/offerpai/offerpai_backend.git'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('本地编译') {
|
||||||
|
steps {
|
||||||
|
echo "开始构建镜像"
|
||||||
|
sh "docker build -f admin/Dockerfile -t ${IMAGE_NAME}:${IMAGE_TAG} ."
|
||||||
|
echo "导出镜像"
|
||||||
|
sh "docker save -o ${IMAGE_TAR} ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('文件传输') {
|
||||||
|
steps {
|
||||||
|
echo "传输文件到目标服务器"
|
||||||
|
sh "${SSH_CMD} 'mkdir -p ${REMOTE_DIR}'"
|
||||||
|
sh "${SCP_CMD} ${IMAGE_TAR} ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/"
|
||||||
|
sh "${SCP_CMD} admin/nginx.conf ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/nginx.conf"
|
||||||
|
sh "${SCP_CMD} docker-compose.test.admin.yml ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/docker-compose.yml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('加载镜像') {
|
||||||
|
steps {
|
||||||
|
sh "${SSH_CMD} 'docker load < ${REMOTE_DIR}/${IMAGE_TAR}'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('检测部署目标') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def blueRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-blue'", returnStdout: true).trim()
|
||||||
|
def greenRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-green'", returnStdout: true).trim()
|
||||||
|
|
||||||
|
env.DEPLOY_TARGET = ''
|
||||||
|
|
||||||
|
if (blueRunning && !greenRunning) {
|
||||||
|
env.DEPLOY_TARGET = 'green'
|
||||||
|
}
|
||||||
|
if (greenRunning && !blueRunning) {
|
||||||
|
env.DEPLOY_TARGET = 'blue'
|
||||||
|
}
|
||||||
|
if (!env.DEPLOY_TARGET) {
|
||||||
|
echo "当前环境未部署服务或状态异常,默认部署 blue"
|
||||||
|
env.DEPLOY_TARGET = 'blue'
|
||||||
|
}
|
||||||
|
|
||||||
|
env.OTHER_TARGET = (env.DEPLOY_TARGET == 'blue') ? 'green' : 'blue'
|
||||||
|
echo "当前激活: ${env.OTHER_TARGET},即将部署: ${env.DEPLOY_TARGET}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('启动新版本') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def existingContainer = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'", returnStdout: true).trim()
|
||||||
|
if (existingContainer) {
|
||||||
|
sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'"
|
||||||
|
}
|
||||||
|
sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d ${env.DEPLOY_TARGET}'"
|
||||||
|
sh 'sleep 70'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('检查Nginx') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def nginxExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim()
|
||||||
|
if (!nginxExists) {
|
||||||
|
echo "首次部署,初始化 Nginx 容器"
|
||||||
|
sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d nginx'"
|
||||||
|
sh 'sleep 3'
|
||||||
|
} else {
|
||||||
|
def nginxRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim()
|
||||||
|
if (!nginxRunning) {
|
||||||
|
echo "Nginx 容器已停止,重新启动"
|
||||||
|
sh "${SSH_CMD} 'docker start ${CONTAINER_PREFIX}-nginx'"
|
||||||
|
sh 'sleep 2'
|
||||||
|
}
|
||||||
|
echo "Nginx 容器已存在且运行中"
|
||||||
|
}
|
||||||
|
// docker cp nginx.conf 进容器(覆盖更新配置模板)
|
||||||
|
sh "${SSH_CMD} 'docker cp ${REMOTE_DIR}/nginx.conf ${CONTAINER_PREFIX}-nginx:/etc/nginx/nginx.conf'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('健康检查') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def maxRetries = 3
|
||||||
|
def retryCount = 0
|
||||||
|
def healthy = false
|
||||||
|
|
||||||
|
while (retryCount < maxRetries && !healthy) {
|
||||||
|
try {
|
||||||
|
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}'"
|
||||||
|
healthy = true
|
||||||
|
} catch (Exception e) {
|
||||||
|
retryCount++
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
echo "健康检查失败,5秒后重试 (${retryCount}/${maxRetries})"
|
||||||
|
sleep 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!healthy) {
|
||||||
|
error "健康检查失败,部署中止"
|
||||||
|
}
|
||||||
|
echo "✅ ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} 健康检查通过"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('切换流量') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx sed -i \"s/proxy_pass http:\\/\\/\\(blue\\|green\\):8081;/proxy_pass http:\\/\\/${env.DEPLOY_TARGET}:8081;/\" /etc/nginx/nginx.conf'"
|
||||||
|
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload'"
|
||||||
|
echo "✅ 流量已切换到 ${env.DEPLOY_TARGET}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('删除旧版本') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def otherExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}'", returnStdout: true).trim()
|
||||||
|
if (otherExists) {
|
||||||
|
sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}'"
|
||||||
|
echo "旧版本 ${env.OTHER_TARGET} 已删除"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('清理') {
|
||||||
|
steps {
|
||||||
|
sh "${SSH_CMD} 'rm -f ${REMOTE_DIR}/${IMAGE_TAR}'"
|
||||||
|
sh "rm -f ${IMAGE_TAR}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
echo "✅ 蓝绿部署成功!当前运行: ${env.DEPLOY_TARGET}"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo '❌ 部署失败,请检查日志'
|
||||||
|
sh "rm -f ${IMAGE_TAR} || true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
use epoll;
|
||||||
|
multi_accept on;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
client_max_body_size 20m;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# Nginx 自身健康检查
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 'ok';
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 默认代理到 blue,部署时通过 sed 切换
|
||||||
|
location / {
|
||||||
|
proxy_pass http://blue:8081;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
|
||||||
|
proxy_buffer_size 128k;
|
||||||
|
proxy_buffers 4 256k;
|
||||||
|
proxy_busy_buffers_size 256k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.jiayunet</groupId>
|
||||||
|
<artifactId>back_end</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>admin</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jiayunet</groupId>
|
||||||
|
<artifactId>manager</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>admin</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>17</source>
|
||||||
|
<target>17</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<version>2.6.13</version>
|
||||||
|
<configuration>
|
||||||
|
<fork>true</fork>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>repackage</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.jiayunet;
|
||||||
|
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* B 端管理后台启动类
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@SpringBootApplication(scanBasePackages = "org.jiayunet")
|
||||||
|
@MapperScan("org.jiayunet.**.mapper")
|
||||||
|
@EnableScheduling
|
||||||
|
@EnableAspectJAutoProxy
|
||||||
|
public class AdminApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(AdminApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.jiayunet.admin.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* B 端管理后台 Redis Key 常量
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
public interface AdminRedisKeyName {
|
||||||
|
|
||||||
|
/** 管理员登录 token 前缀,完整 key: admin:login:token:{adminId} */
|
||||||
|
String LOGIN_TOKEN = "login:token:";
|
||||||
|
|
||||||
|
/** 登录失败次数计数,完整 key: admin:login:fail:{username} */
|
||||||
|
String LOGIN_FAIL_COUNT = "login:fail:";
|
||||||
|
|
||||||
|
/** 账号锁定标记,完整 key: admin:login:lock:{username} */
|
||||||
|
String LOGIN_LOCK = "login:lock:";
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.jiayunet.admin.controller;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jiayunet.admin.pojo.param.login.AdminLoginParam;
|
||||||
|
import org.jiayunet.admin.service.AdminLoginService;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员登录控制类
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/public")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Validated
|
||||||
|
public class AdminLoginController {
|
||||||
|
|
||||||
|
private AdminLoginService adminLoginService;
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
public Long login(@Validated @RequestBody AdminLoginParam param, HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
return adminLoginService.login(param.getUsername(), param.getPassword(), request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package org.jiayunet.admin.controller;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jiayunet.admin.pojo.dto.order.OrderListDto;
|
||||||
|
import org.jiayunet.admin.pojo.dto.order.OrderStatsDto;
|
||||||
|
import org.jiayunet.admin.pojo.param.order.OrderPageParam;
|
||||||
|
import org.jiayunet.admin.service.AdminOrderService;
|
||||||
|
import org.jiayunet.pojo.PageResult;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台-订单管理控制类
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/order")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Validated
|
||||||
|
public class AdminOrderController {
|
||||||
|
|
||||||
|
private AdminOrderService adminOrderService;
|
||||||
|
|
||||||
|
/** 订单分页列表 */
|
||||||
|
@PostMapping("/list")
|
||||||
|
public PageResult<OrderListDto> list(@Validated @RequestBody OrderPageParam param) {
|
||||||
|
return adminOrderService.pageList(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单统计 */
|
||||||
|
@GetMapping("/stats")
|
||||||
|
public OrderStatsDto stats() {
|
||||||
|
return adminOrderService.stats();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package org.jiayunet.admin.controller;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jiayunet.admin.pojo.param.product.ProductPageParam;
|
||||||
|
import org.jiayunet.admin.pojo.param.product.ProductSaveParam;
|
||||||
|
import org.jiayunet.admin.service.AdminProductService;
|
||||||
|
import org.jiayunet.pojo.PageResult;
|
||||||
|
import org.jiayunet.pojo.po.MemberProduct;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台-商品管理控制类
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/product")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Validated
|
||||||
|
public class AdminProductController {
|
||||||
|
|
||||||
|
private AdminProductService adminProductService;
|
||||||
|
|
||||||
|
/** 商品分页列表 */
|
||||||
|
@PostMapping("/list")
|
||||||
|
public PageResult<MemberProduct> list(@Validated @RequestBody ProductPageParam param) {
|
||||||
|
return adminProductService.pageList(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增/编辑商品 */
|
||||||
|
@PostMapping("/save")
|
||||||
|
public void save(@Validated @RequestBody ProductSaveParam param) {
|
||||||
|
adminProductService.save(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上架/下架 */
|
||||||
|
@PostMapping("/updateStatus")
|
||||||
|
public void updateStatus(@RequestParam Long id, @RequestParam Integer status) {
|
||||||
|
adminProductService.updateStatus(id, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除商品 */
|
||||||
|
@PostMapping("/delete")
|
||||||
|
public void delete(@RequestParam Long id) {
|
||||||
|
adminProductService.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.jiayunet.admin.controller;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jiayunet.admin.pojo.dto.user.UserListDto;
|
||||||
|
import org.jiayunet.admin.pojo.dto.user.UserStatsDto;
|
||||||
|
import org.jiayunet.admin.pojo.param.user.UserPageParam;
|
||||||
|
import org.jiayunet.admin.service.AdminUserService;
|
||||||
|
import org.jiayunet.pojo.PageResult;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台-用户管理控制类
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/user")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Validated
|
||||||
|
public class AdminUserController {
|
||||||
|
|
||||||
|
private AdminUserService adminUserService;
|
||||||
|
|
||||||
|
/** 用户分页列表 */
|
||||||
|
@PostMapping("/list")
|
||||||
|
public PageResult<UserListDto> list(@Validated @RequestBody UserPageParam param) {
|
||||||
|
return adminUserService.pageList(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户统计(总数/今日/本周/本月) */
|
||||||
|
@GetMapping("/stats")
|
||||||
|
public UserStatsDto stats() {
|
||||||
|
return adminUserService.stats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 禁用/启用用户 */
|
||||||
|
@PostMapping("/updateStatus")
|
||||||
|
public void updateStatus(@RequestParam Long userId, @RequestParam Integer status) {
|
||||||
|
adminUserService.updateStatus(userId, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.jiayunet.admin.controller;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面路由控制器(返回 Thymeleaf 视图名,不走 UnifiedResponseBodyAdvice)
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/page")
|
||||||
|
public class PageController {
|
||||||
|
|
||||||
|
/** 登录页 */
|
||||||
|
@GetMapping("/login")
|
||||||
|
public String login() {
|
||||||
|
return "login";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 管理后台首页(需登录) */
|
||||||
|
@GetMapping("/index")
|
||||||
|
public String index() {
|
||||||
|
return "index";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.jiayunet.admin.pojo.dto.order;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单列表出参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class OrderListDto {
|
||||||
|
|
||||||
|
/** 订单ID */
|
||||||
|
private Long id;
|
||||||
|
/** 订单编号 */
|
||||||
|
private String orderNo;
|
||||||
|
/** 用户昵称 */
|
||||||
|
private String userNick;
|
||||||
|
/** 用户手机号 */
|
||||||
|
private String mobileNumber;
|
||||||
|
/** 商品名称 */
|
||||||
|
private String productName;
|
||||||
|
/** 实付金额(分) */
|
||||||
|
private Integer payAmount;
|
||||||
|
/** 支付渠道 1=微信 2=支付宝 */
|
||||||
|
private Integer payChannel;
|
||||||
|
/** 订单状态 0=待支付 1=已支付 2=已退款 3=已关闭 */
|
||||||
|
private Integer status;
|
||||||
|
/** 下单时间 */
|
||||||
|
private Instant createTime;
|
||||||
|
/** 支付时间 */
|
||||||
|
private Instant payTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.jiayunet.admin.pojo.dto.order;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单统计出参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class OrderStatsDto {
|
||||||
|
|
||||||
|
/** 总订单数 */
|
||||||
|
private Long totalCount;
|
||||||
|
/** 已支付订单数 */
|
||||||
|
private Long paidCount;
|
||||||
|
/** 今日已支付 */
|
||||||
|
private Long todayPaid;
|
||||||
|
/** 本周已支付 */
|
||||||
|
private Long weekPaid;
|
||||||
|
/** 本月已支付 */
|
||||||
|
private Long monthPaid;
|
||||||
|
/** 今日收入(分) */
|
||||||
|
private Long todayIncome;
|
||||||
|
/** 本周收入(分) */
|
||||||
|
private Long weekIncome;
|
||||||
|
/** 本月收入(分) */
|
||||||
|
private Long monthIncome;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.jiayunet.admin.pojo.dto.user;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户列表出参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UserListDto {
|
||||||
|
|
||||||
|
/** 用户ID */
|
||||||
|
private Long id;
|
||||||
|
/** 昵称 */
|
||||||
|
private String nick;
|
||||||
|
/** 真实姓名 */
|
||||||
|
private String realName;
|
||||||
|
/** 手机号 */
|
||||||
|
private String mobileNumber;
|
||||||
|
/** 状态 0=正常 1=禁用 */
|
||||||
|
private Integer status;
|
||||||
|
/** 简历数量 */
|
||||||
|
private Integer resumeCount;
|
||||||
|
/** 注册时间 */
|
||||||
|
private Instant createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package org.jiayunet.admin.pojo.dto.user;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户统计出参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UserStatsDto {
|
||||||
|
|
||||||
|
/** 用户总数 */
|
||||||
|
private Long total;
|
||||||
|
/** 今日新增 */
|
||||||
|
private Long today;
|
||||||
|
/** 本周新增 */
|
||||||
|
private Long thisWeek;
|
||||||
|
/** 本月新增 */
|
||||||
|
private Long thisMonth;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.jiayunet.admin.pojo.param.login;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员登录入参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AdminLoginParam {
|
||||||
|
|
||||||
|
@NotBlank(message = "账号不能为空")
|
||||||
|
@Size(max = 32, message = "账号长度不能超过32位")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@NotBlank(message = "密码不能为空")
|
||||||
|
@Size(min = 6, max = 32, message = "密码长度6-32位")
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package org.jiayunet.admin.pojo.param.order;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.jiayunet.pojo.PageParam;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单分页查询入参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class OrderPageParam extends PageParam {
|
||||||
|
|
||||||
|
/** 订单状态 0=待支付 1=已支付 2=已退款 3=已关闭 */
|
||||||
|
private Integer status;
|
||||||
|
/** 支付渠道 1=微信 2=支付宝 */
|
||||||
|
private Integer payChannel;
|
||||||
|
/** 用户手机号(模糊) */
|
||||||
|
private String mobileNumber;
|
||||||
|
/** 下单时间-起 */
|
||||||
|
private Instant startTime;
|
||||||
|
/** 下单时间-止 */
|
||||||
|
private Instant endTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.jiayunet.admin.pojo.param.product;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.jiayunet.pojo.PageParam;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品分页查询入参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ProductPageParam extends PageParam {
|
||||||
|
|
||||||
|
/** 状态筛选 0=下架 1=上架 */
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.jiayunet.admin.pojo.param.product;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品新增/编辑入参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ProductSaveParam {
|
||||||
|
|
||||||
|
/** 商品ID(编辑时必传,新增时不传) */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank(message = "商品名称不能为空")
|
||||||
|
private String productName;
|
||||||
|
|
||||||
|
/** 标签,如"限时优惠" */
|
||||||
|
private String tag;
|
||||||
|
|
||||||
|
/** 主推标识 0=否 1=是 */
|
||||||
|
private Integer isFeatured;
|
||||||
|
|
||||||
|
/** 购买按钮文字 */
|
||||||
|
private String buyButtonText;
|
||||||
|
|
||||||
|
@NotNull(message = "价格不能为空")
|
||||||
|
@Min(value = 1, message = "价格必须大于0")
|
||||||
|
private Integer price;
|
||||||
|
|
||||||
|
/** 划线价(分) */
|
||||||
|
private Integer originalPrice;
|
||||||
|
|
||||||
|
/** 折算月价(分) */
|
||||||
|
private Integer monthlyPrice;
|
||||||
|
|
||||||
|
@NotNull(message = "有效天数不能为空")
|
||||||
|
@Min(value = 1, message = "有效天数必须大于0")
|
||||||
|
private Integer durationDays;
|
||||||
|
|
||||||
|
/** 排序 */
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/** 状态 0=下架 1=上架 */
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package org.jiayunet.admin.pojo.param.user;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.jiayunet.pojo.PageParam;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户分页查询入参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class UserPageParam extends PageParam {
|
||||||
|
|
||||||
|
/** 手机号(模糊搜索) */
|
||||||
|
private String mobileNumber;
|
||||||
|
|
||||||
|
/** 状态筛选 0=正常 1=禁用 */
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package org.jiayunet.admin.service;
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT;
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jiayunet.admin.constant.AdminRedisKeyName;
|
||||||
|
import org.jiayunet.mapper.AdminUserMapper;
|
||||||
|
import org.jiayunet.pojo.login.RedisLoginTokenInfo;
|
||||||
|
import org.jiayunet.pojo.po.AdminUser;
|
||||||
|
import org.jiayunet.tool.HttpIpTool;
|
||||||
|
import org.jiayunet.tool.server.RedisServerTool;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import javax.servlet.http.Cookie;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员登录服务
|
||||||
|
* <p>依赖:AdminUserMapper(管理员查询)、RedisServerTool(token管理与登录失败计数)</p>
|
||||||
|
* <p>使用表:bg_admin_user(查询管理员账号)</p>
|
||||||
|
* <p>使用Redis:login:token:{adminId}(登录令牌)、login:fail:{username}(失败计数)、login:lock:{username}(账号锁定)</p>
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class AdminLoginService {
|
||||||
|
|
||||||
|
/** 密码错误允许最大次数 */
|
||||||
|
private static final int MAX_FAIL_COUNT = 3;
|
||||||
|
/** 账号锁定时长(小时) */
|
||||||
|
private static final int LOCK_HOURS = 24;
|
||||||
|
|
||||||
|
@Value("${app.secret.token:youweiqingnian123}")
|
||||||
|
private String secret;
|
||||||
|
@Value("${app.login.token.exceed_time:43200}")
|
||||||
|
private int tokenExceedTime;
|
||||||
|
@Value("${app.login.device_online_quantity:5}")
|
||||||
|
private int deviceOnlineQuantity;
|
||||||
|
@Autowired
|
||||||
|
private AdminUserMapper adminUserMapper;
|
||||||
|
@Autowired
|
||||||
|
private RedisServerTool redisServerTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账号密码登录
|
||||||
|
* <p>1. 校验账号锁定状态 2. 查询管理员 3. 校验密码(失败累计,3次锁定24小时) 4. 生成JWT 5. 写入Redis管理多设备 6. 设置Cookie</p>
|
||||||
|
*/
|
||||||
|
public Long login(String username, String password, HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
Assert.hasText(username, "账号不能为空");
|
||||||
|
Assert.hasText(password, "密码不能为空");
|
||||||
|
|
||||||
|
// 检查账号是否被锁定
|
||||||
|
Assert.isTrue(!redisServerTool.hasKey(AdminRedisKeyName.LOGIN_LOCK + username), "账号已被锁定,请24小时后再试");
|
||||||
|
|
||||||
|
// 查询管理员
|
||||||
|
AdminUser admin = adminUserMapper.selectOne(new LambdaQueryWrapper<AdminUser>().eq(AdminUser::getUsername, username).eq(AdminUser::getStatus, 0));
|
||||||
|
Assert.notNull(admin, "账号或密码错误");
|
||||||
|
|
||||||
|
// 校验密码:MD5(明文 + salt)
|
||||||
|
if (!md5(password + secret).equals(admin.getPassword())) {
|
||||||
|
handleLoginFail(username);
|
||||||
|
throw new IllegalArgumentException("账号或密码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录成功,清除失败计数
|
||||||
|
clearLoginFail(username);
|
||||||
|
|
||||||
|
// 生成JWT
|
||||||
|
String uuId = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
String token = JWT.create().withClaim("userId", admin.getId()).withClaim("uuId", uuId).sign(Algorithm.HMAC256(secret));
|
||||||
|
|
||||||
|
// 构建Redis登录信息
|
||||||
|
String redisKey = AdminRedisKeyName.LOGIN_TOKEN + admin.getId();
|
||||||
|
RedisLoginTokenInfo info = redisServerTool.get(redisKey, RedisLoginTokenInfo.class);
|
||||||
|
if (info == null) {
|
||||||
|
info = new RedisLoginTokenInfo();
|
||||||
|
info.setUserId(admin.getId());
|
||||||
|
info.setAuthority(new ArrayList<>());
|
||||||
|
info.setRole(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤过期设备
|
||||||
|
List<RedisLoginTokenInfo.LoginDevice> devices = info.getLoginDevices() == null ? new ArrayList<>() : info.getLoginDevices();
|
||||||
|
long expireMillis = System.currentTimeMillis() - tokenExceedTime * 1000L;
|
||||||
|
devices = devices.stream().filter(d -> d.getLastLoginTime().isAfter(Instant.ofEpochMilli(expireMillis))).collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 超过设备上限,移除最早的
|
||||||
|
while (devices.size() >= deviceOnlineQuantity) {
|
||||||
|
devices.stream().min(Comparator.comparing(RedisLoginTokenInfo.LoginDevice::getLastLoginTime)).ifPresent(devices::remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加当前设备
|
||||||
|
RedisLoginTokenInfo.LoginDevice device = new RedisLoginTokenInfo.LoginDevice();
|
||||||
|
device.setUuId(uuId);
|
||||||
|
device.setLastLoginTime(Instant.now());
|
||||||
|
device.setLoginIp(HttpIpTool.gteRealIP(request));
|
||||||
|
devices.add(device);
|
||||||
|
|
||||||
|
info.setLoginDevices(devices);
|
||||||
|
redisServerTool.set(redisKey, info, tokenExceedTime, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
// 设置Cookie
|
||||||
|
Cookie cookie = new Cookie("Token", token);
|
||||||
|
cookie.setHttpOnly(true);
|
||||||
|
cookie.setPath("/");
|
||||||
|
cookie.setMaxAge(tokenExceedTime);
|
||||||
|
response.addCookie(cookie);
|
||||||
|
|
||||||
|
return admin.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理登录失败:累计失败次数,达到上限则锁定账号24小时 */
|
||||||
|
private void handleLoginFail(String username) {
|
||||||
|
String failKey = AdminRedisKeyName.LOGIN_FAIL_COUNT + username;
|
||||||
|
Integer failCount = redisServerTool.get(failKey, Integer.class);
|
||||||
|
int currentCount = (failCount == null ? 0 : failCount) + 1;
|
||||||
|
if (currentCount >= MAX_FAIL_COUNT) {
|
||||||
|
redisServerTool.set(AdminRedisKeyName.LOGIN_LOCK + username, true, LOCK_HOURS, TimeUnit.HOURS);
|
||||||
|
redisServerTool.delete(failKey);
|
||||||
|
log.warn("管理员账号 [{}] 密码错误{}次,已锁定{}小时", username, MAX_FAIL_COUNT, LOCK_HOURS);
|
||||||
|
} else {
|
||||||
|
redisServerTool.set(failKey, currentCount, LOCK_HOURS, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录成功后清除失败计数 */
|
||||||
|
private void clearLoginFail(String username) {
|
||||||
|
redisServerTool.delete(AdminRedisKeyName.LOGIN_FAIL_COUNT + username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MD5 加密 */
|
||||||
|
private String md5(String input) {
|
||||||
|
try {
|
||||||
|
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
|
||||||
|
byte[] digest = md.digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : digest) { sb.append(String.format("%02x", b)); }
|
||||||
|
return sb.toString();
|
||||||
|
} catch (java.security.NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package org.jiayunet.admin.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jiayunet.admin.pojo.dto.order.OrderListDto;
|
||||||
|
import org.jiayunet.admin.pojo.dto.order.OrderStatsDto;
|
||||||
|
import org.jiayunet.admin.pojo.param.order.OrderPageParam;
|
||||||
|
import org.jiayunet.mapper.MemberOrderMapper;
|
||||||
|
import org.jiayunet.mapper.MemberProductMapper;
|
||||||
|
import org.jiayunet.mapper.UserMapper;
|
||||||
|
import org.jiayunet.pojo.PageResult;
|
||||||
|
import org.jiayunet.pojo.po.MemberOrder;
|
||||||
|
import org.jiayunet.pojo.po.MemberProduct;
|
||||||
|
import org.jiayunet.pojo.po.User;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.*;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台-订单管理服务
|
||||||
|
* <p>依赖:MemberOrderMapper、UserMapper、MemberProductMapper</p>
|
||||||
|
* <p>使用表:bg_member_order(订单列表/统计)、bg_user(关联用户信息)、bg_member_product(关联商品名)</p>
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class AdminOrderService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MemberOrderMapper memberOrderMapper;
|
||||||
|
@Autowired
|
||||||
|
private UserMapper userMapper;
|
||||||
|
@Autowired
|
||||||
|
private MemberProductMapper memberProductMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单分页列表
|
||||||
|
* <p>1. 如果按手机号筛选,先查用户ID集合 2. 分页查订单 3. 批量关联用户和商品信息 4. 组装DTO</p>
|
||||||
|
*/
|
||||||
|
public PageResult<OrderListDto> pageList(OrderPageParam param) {
|
||||||
|
// 如果传了手机号,先查对应用户ID
|
||||||
|
Set<Long> userIdFilter = null;
|
||||||
|
if (StringUtils.hasText(param.getMobileNumber())) {
|
||||||
|
List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>().like(User::getMobileNumber, param.getMobileNumber()).select(User::getId));
|
||||||
|
userIdFilter = users.stream().map(User::getId).collect(Collectors.toSet());
|
||||||
|
if (userIdFilter.isEmpty()) return new PageResult<>(param.getPageNum().longValue(), param.getPageSize().longValue(), 0L, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<MemberOrder> wrapper = new LambdaQueryWrapper<MemberOrder>()
|
||||||
|
.eq(param.getStatus() != null, MemberOrder::getStatus, param.getStatus())
|
||||||
|
.eq(param.getPayChannel() != null, MemberOrder::getPayChannel, param.getPayChannel())
|
||||||
|
.ge(param.getStartTime() != null, MemberOrder::getCreateTime, param.getStartTime())
|
||||||
|
.le(param.getEndTime() != null, MemberOrder::getCreateTime, param.getEndTime())
|
||||||
|
.in(userIdFilter != null, MemberOrder::getUserId, userIdFilter)
|
||||||
|
.orderByDesc(MemberOrder::getCreateTime);
|
||||||
|
|
||||||
|
Page<MemberOrder> page = memberOrderMapper.selectPage(param.toPage(), wrapper);
|
||||||
|
if (page.getRecords().isEmpty()) return PageResult.from((Page<OrderListDto>) page.convert(o -> new OrderListDto()));
|
||||||
|
|
||||||
|
// 批量查用户和商品
|
||||||
|
Set<Long> userIds = page.getRecords().stream().map(MemberOrder::getUserId).collect(Collectors.toSet());
|
||||||
|
Set<Long> productIds = page.getRecords().stream().map(MemberOrder::getProductId).collect(Collectors.toSet());
|
||||||
|
Map<Long, User> userMap = userMapper.selectBatchIds(userIds).stream().collect(Collectors.toMap(User::getId, u -> u));
|
||||||
|
Map<Long, MemberProduct> productMap = memberProductMapper.selectBatchIds(productIds).stream().collect(Collectors.toMap(MemberProduct::getId, p -> p));
|
||||||
|
|
||||||
|
Page<OrderListDto> dtoPage = (Page<OrderListDto>) page.convert(order -> {
|
||||||
|
OrderListDto dto = new OrderListDto();
|
||||||
|
dto.setId(order.getId());
|
||||||
|
dto.setOrderNo(order.getOrderNo());
|
||||||
|
dto.setPayAmount(order.getPayAmount());
|
||||||
|
dto.setPayChannel(order.getPayChannel());
|
||||||
|
dto.setStatus(order.getStatus());
|
||||||
|
dto.setCreateTime(order.getCreateTime());
|
||||||
|
dto.setPayTime(order.getPayTime());
|
||||||
|
User user = userMap.get(order.getUserId());
|
||||||
|
if (user != null) { dto.setUserNick(user.getNick()); dto.setMobileNumber(user.getMobileNumber()); }
|
||||||
|
MemberProduct product = productMap.get(order.getProductId());
|
||||||
|
if (product != null) { dto.setProductName(product.getProductName()); }
|
||||||
|
return dto;
|
||||||
|
});
|
||||||
|
return PageResult.from(dtoPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单统计:总数/已支付数/今日本周本月已支付数和收入
|
||||||
|
*/
|
||||||
|
public OrderStatsDto stats() {
|
||||||
|
OrderStatsDto dto = new OrderStatsDto();
|
||||||
|
ZoneId zone = ZoneId.of("Asia/Shanghai");
|
||||||
|
LocalDate now = LocalDate.now(zone);
|
||||||
|
Instant todayStart = now.atStartOfDay(zone).toInstant();
|
||||||
|
Instant weekStart = now.with(DayOfWeek.MONDAY).atStartOfDay(zone).toInstant();
|
||||||
|
Instant monthStart = now.withDayOfMonth(1).atStartOfDay(zone).toInstant();
|
||||||
|
|
||||||
|
dto.setTotalCount(memberOrderMapper.selectCount(new LambdaQueryWrapper<MemberOrder>()));
|
||||||
|
dto.setPaidCount(memberOrderMapper.selectCount(new LambdaQueryWrapper<MemberOrder>().eq(MemberOrder::getStatus, 1)));
|
||||||
|
|
||||||
|
// 今日/本周/本月已支付订单
|
||||||
|
List<MemberOrder> todayOrders = memberOrderMapper.selectList(new LambdaQueryWrapper<MemberOrder>().eq(MemberOrder::getStatus, 1).ge(MemberOrder::getPayTime, todayStart));
|
||||||
|
List<MemberOrder> weekOrders = memberOrderMapper.selectList(new LambdaQueryWrapper<MemberOrder>().eq(MemberOrder::getStatus, 1).ge(MemberOrder::getPayTime, weekStart));
|
||||||
|
List<MemberOrder> monthOrders = memberOrderMapper.selectList(new LambdaQueryWrapper<MemberOrder>().eq(MemberOrder::getStatus, 1).ge(MemberOrder::getPayTime, monthStart));
|
||||||
|
|
||||||
|
dto.setTodayPaid((long) todayOrders.size());
|
||||||
|
dto.setWeekPaid((long) weekOrders.size());
|
||||||
|
dto.setMonthPaid((long) monthOrders.size());
|
||||||
|
dto.setTodayIncome(todayOrders.stream().mapToLong(o -> o.getPayAmount() == null ? 0 : o.getPayAmount()).sum());
|
||||||
|
dto.setWeekIncome(weekOrders.stream().mapToLong(o -> o.getPayAmount() == null ? 0 : o.getPayAmount()).sum());
|
||||||
|
dto.setMonthIncome(monthOrders.stream().mapToLong(o -> o.getPayAmount() == null ? 0 : o.getPayAmount()).sum());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package org.jiayunet.admin.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jiayunet.admin.pojo.param.product.ProductPageParam;
|
||||||
|
import org.jiayunet.admin.pojo.param.product.ProductSaveParam;
|
||||||
|
import org.jiayunet.mapper.MemberProductMapper;
|
||||||
|
import org.jiayunet.pojo.PageResult;
|
||||||
|
import org.jiayunet.pojo.po.MemberProduct;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台-商品管理服务
|
||||||
|
* <p>依赖:MemberProductMapper</p>
|
||||||
|
* <p>使用表:bg_member_product(CRUD)</p>
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class AdminProductService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MemberProductMapper memberProductMapper;
|
||||||
|
|
||||||
|
/** 商品分页列表 */
|
||||||
|
public PageResult<MemberProduct> pageList(ProductPageParam param) {
|
||||||
|
LambdaQueryWrapper<MemberProduct> wrapper = new LambdaQueryWrapper<MemberProduct>()
|
||||||
|
.eq(param.getStatus() != null, MemberProduct::getStatus, param.getStatus())
|
||||||
|
.orderByAsc(MemberProduct::getSortOrder);
|
||||||
|
Page<MemberProduct> page = memberProductMapper.selectPage(param.toPage(), wrapper);
|
||||||
|
return PageResult.from(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增/编辑商品 */
|
||||||
|
public void save(ProductSaveParam param) {
|
||||||
|
MemberProduct product = new MemberProduct();
|
||||||
|
BeanUtils.copyProperties(param, product);
|
||||||
|
if (param.getId() == null) {
|
||||||
|
if (product.getStatus() == null) product.setStatus(0);
|
||||||
|
if (product.getSortOrder() == null) product.setSortOrder(0);
|
||||||
|
if (product.getIsFeatured() == null) product.setIsFeatured(0);
|
||||||
|
memberProductMapper.insert(product);
|
||||||
|
} else {
|
||||||
|
product.setId(param.getId());
|
||||||
|
memberProductMapper.updateById(product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上架/下架商品 */
|
||||||
|
public void updateStatus(Long id, Integer status) {
|
||||||
|
Assert.notNull(id, "商品ID不能为空");
|
||||||
|
Assert.isTrue(status == 0 || status == 1, "状态值非法");
|
||||||
|
memberProductMapper.update(null, new LambdaUpdateWrapper<MemberProduct>().eq(MemberProduct::getId, id).set(MemberProduct::getStatus, status));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除商品(逻辑删除) */
|
||||||
|
public void delete(Long id) {
|
||||||
|
Assert.notNull(id, "商品ID不能为空");
|
||||||
|
memberProductMapper.deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package org.jiayunet.admin.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jiayunet.admin.pojo.dto.user.UserListDto;
|
||||||
|
import org.jiayunet.admin.pojo.dto.user.UserStatsDto;
|
||||||
|
import org.jiayunet.admin.pojo.param.user.UserPageParam;
|
||||||
|
import org.jiayunet.mapper.UserMapper;
|
||||||
|
import org.jiayunet.mapper.UserResumeMapper;
|
||||||
|
import org.jiayunet.pojo.PageResult;
|
||||||
|
import org.jiayunet.pojo.po.User;
|
||||||
|
import org.jiayunet.pojo.po.UserResume;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.*;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台-用户管理服务
|
||||||
|
* <p>依赖:UserMapper(用户查询/更新)、UserResumeMapper(简历数量统计)</p>
|
||||||
|
* <p>使用表:bg_user(用户列表/统计/禁用)、bg_user_resume(简历数量)</p>
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class AdminUserService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserMapper userMapper;
|
||||||
|
@Autowired
|
||||||
|
private UserResumeMapper userResumeMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户分页列表
|
||||||
|
* <p>1. 分页查询用户 2. 批量查询每个用户的简历数量 3. 组装返回</p>
|
||||||
|
*/
|
||||||
|
public PageResult<UserListDto> pageList(UserPageParam param) {
|
||||||
|
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
|
||||||
|
.like(StringUtils.hasText(param.getMobileNumber()), User::getMobileNumber, param.getMobileNumber())
|
||||||
|
.like(StringUtils.hasText(param.getKeyword()), User::getNick, param.getKeyword())
|
||||||
|
.eq(param.getStatus() != null, User::getStatus, param.getStatus())
|
||||||
|
.orderByDesc(User::getCreateTime);
|
||||||
|
|
||||||
|
Page<User> page = userMapper.selectPage(param.toPage(), wrapper);
|
||||||
|
if (page.getRecords().isEmpty()) return PageResult.from((Page<UserListDto>) page.convert(u -> new UserListDto()));
|
||||||
|
|
||||||
|
// 批量查询简历数量
|
||||||
|
List<Long> userIds = page.getRecords().stream().map(User::getId).collect(Collectors.toList());
|
||||||
|
List<UserResume> resumes = userResumeMapper.selectList(new LambdaQueryWrapper<UserResume>().in(UserResume::getUserId, userIds).select(UserResume::getUserId));
|
||||||
|
Map<Long, Long> resumeCountMap = resumes.stream().collect(Collectors.groupingBy(UserResume::getUserId, Collectors.counting()));
|
||||||
|
|
||||||
|
// 组装DTO
|
||||||
|
Page<UserListDto> dtoPage = (Page<UserListDto>) page.convert(user -> {
|
||||||
|
UserListDto dto = new UserListDto();
|
||||||
|
dto.setId(user.getId());
|
||||||
|
dto.setNick(user.getNick());
|
||||||
|
dto.setRealName(user.getRealName());
|
||||||
|
dto.setMobileNumber(user.getMobileNumber());
|
||||||
|
dto.setStatus(user.getStatus());
|
||||||
|
dto.setResumeCount(resumeCountMap.getOrDefault(user.getId(), 0L).intValue());
|
||||||
|
dto.setCreateTime(user.getCreateTime());
|
||||||
|
return dto;
|
||||||
|
});
|
||||||
|
return PageResult.from(dtoPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户统计:总数 / 今日 / 本周 / 本月 新增
|
||||||
|
*/
|
||||||
|
public UserStatsDto stats() {
|
||||||
|
UserStatsDto dto = new UserStatsDto();
|
||||||
|
ZoneId zone = ZoneId.of("Asia/Shanghai");
|
||||||
|
LocalDate now = LocalDate.now(zone);
|
||||||
|
|
||||||
|
Instant todayStart = now.atStartOfDay(zone).toInstant();
|
||||||
|
Instant weekStart = now.with(DayOfWeek.MONDAY).atStartOfDay(zone).toInstant();
|
||||||
|
Instant monthStart = now.withDayOfMonth(1).atStartOfDay(zone).toInstant();
|
||||||
|
|
||||||
|
dto.setTotal(userMapper.selectCount(new LambdaQueryWrapper<User>()));
|
||||||
|
dto.setToday(userMapper.selectCount(new LambdaQueryWrapper<User>().ge(User::getCreateTime, todayStart)));
|
||||||
|
dto.setThisWeek(userMapper.selectCount(new LambdaQueryWrapper<User>().ge(User::getCreateTime, weekStart)));
|
||||||
|
dto.setThisMonth(userMapper.selectCount(new LambdaQueryWrapper<User>().ge(User::getCreateTime, monthStart)));
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用/启用用户
|
||||||
|
*/
|
||||||
|
public void updateStatus(Long userId, Integer status) {
|
||||||
|
Assert.notNull(userId, "用户ID不能为空");
|
||||||
|
Assert.isTrue(status == 0 || status == 1, "状态值非法");
|
||||||
|
userMapper.update(null, new LambdaUpdateWrapper<User>().eq(User::getId, userId).set(User::getStatus, status));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# tomcat 端口配置
|
||||||
|
server:
|
||||||
|
port: 8081
|
||||||
|
# 数据源配置
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://${MYSQL_HOST:192.168.31.105}:${MYSQL_PORT:3306}/${DB_NAME:offerpie}?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
|
||||||
|
username: ${MYSQL_USERNAME:root}
|
||||||
|
password: ${MYSQL_PASSWORD:123456}
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
redis:
|
||||||
|
host: ${REDIS_HOST:192.168.31.105}
|
||||||
|
password: ${REDIS_PASSWORD:123456}
|
||||||
|
port: ${REDIS_PORT:6379}
|
||||||
|
timeout: 8s
|
||||||
|
database: 0
|
||||||
|
# 开发环境关闭模板缓存,方便调试
|
||||||
|
thymeleaf:
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
# 电子邮箱
|
||||||
|
email:
|
||||||
|
status: close
|
||||||
|
account: ${EMAIL_ACCOUNT:xxx@163.com}
|
||||||
|
authorization: ${EMAIL_AUTHORIZATION:123456}
|
||||||
|
|
||||||
|
# 微信支付
|
||||||
|
wx_pay:
|
||||||
|
status: close
|
||||||
|
|
||||||
|
# 支付宝支付
|
||||||
|
alipay:
|
||||||
|
status: close
|
||||||
|
|
||||||
|
app:
|
||||||
|
# 加密秘钥配置
|
||||||
|
secret:
|
||||||
|
token: ${SECRET_TOKEN:Aa123123}
|
||||||
|
|
||||||
|
# 登陆配置
|
||||||
|
login:
|
||||||
|
token:
|
||||||
|
exceed_time: 5184000
|
||||||
|
device_online_quantity: 3
|
||||||
|
|
||||||
|
# 短信
|
||||||
|
sms:
|
||||||
|
service_provider: aliyun
|
||||||
|
aliyun:
|
||||||
|
access_key_id: LTAI5tJBVUUJhB7yp14UDzVf
|
||||||
|
access_key_secret: Opf0iO5FKNrdwI63DPhXazW7utAGTj
|
||||||
|
|
||||||
|
oss:
|
||||||
|
service_provider: aliyun
|
||||||
|
aliyun:
|
||||||
|
access_key_id: LTAI5tEdLKKQUKhTyUpfH5Mk
|
||||||
|
access_key_secret: RjUdTrq0V5qA4b3BUElNhXqs3ZLp5k
|
||||||
|
|
||||||
|
# 防刷配置
|
||||||
|
prevent_replay:
|
||||||
|
if_open: false
|
||||||
|
interval_time: 20
|
||||||
|
limit_number: 20
|
||||||
|
|
||||||
|
# 开放接口(登录页面 + 静态资源 + 登录接口)
|
||||||
|
ignore:
|
||||||
|
urls: "/public/**,/page/**,/static/**,/css/**,/js/**,/img/**,/favicon.ico"
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# tomcat 端口配置
|
||||||
|
server:
|
||||||
|
port: 8081
|
||||||
|
# 数据源配置
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://${MYSQL_HOST:8.163.14.142}:${MYSQL_PORT:30006}/${DB_NAME:offerpie}?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
|
||||||
|
username: ${MYSQL_USERNAME:root}
|
||||||
|
password: ${MYSQL_PASSWORD:^CgDatabase2020}
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
redis:
|
||||||
|
host: ${REDIS_HOST:8.163.14.142}
|
||||||
|
password: ${REDIS_PASSWORD:#8kPCdAsser}
|
||||||
|
port: ${REDIS_PORT:30089}
|
||||||
|
timeout: 10s
|
||||||
|
database: 0
|
||||||
|
|
||||||
|
# 电子邮箱
|
||||||
|
email:
|
||||||
|
status: close
|
||||||
|
account: ${EMAIL_ACCOUNT:xxx@163.com}
|
||||||
|
authorization: ${EMAIL_AUTHORIZATION:123456}
|
||||||
|
|
||||||
|
# 微信支付
|
||||||
|
wx_pay:
|
||||||
|
status: close
|
||||||
|
|
||||||
|
# 支付宝支付
|
||||||
|
alipay:
|
||||||
|
status: close
|
||||||
|
|
||||||
|
app:
|
||||||
|
# 加密秘钥配置
|
||||||
|
secret:
|
||||||
|
token: ${SECRET_TOKEN:Aa123123}
|
||||||
|
|
||||||
|
# 登陆配置
|
||||||
|
login:
|
||||||
|
token:
|
||||||
|
exceed_time: 5184000
|
||||||
|
device_online_quantity: 3
|
||||||
|
|
||||||
|
# 短信
|
||||||
|
sms:
|
||||||
|
service_provider: aliyun
|
||||||
|
aliyun:
|
||||||
|
access_key_id: LTAI5tJBVUUJhB7yp14UDzVf
|
||||||
|
access_key_secret: Opf0iO5FKNrdwI63DPhXazW7utAGTj
|
||||||
|
|
||||||
|
oss:
|
||||||
|
service_provider: aliyun
|
||||||
|
aliyun:
|
||||||
|
access_key_id: LTAI5tEdLKKQUKhTyUpfH5Mk
|
||||||
|
access_key_secret: RjUdTrq0V5qA4b3BUElNhXqs3ZLp5k
|
||||||
|
|
||||||
|
# 防刷配置
|
||||||
|
prevent_replay:
|
||||||
|
if_open: false
|
||||||
|
interval_time: 20
|
||||||
|
limit_number: 20
|
||||||
|
|
||||||
|
# 开放接口
|
||||||
|
ignore:
|
||||||
|
urls: "/public/**,/page/**,/static/**,/css/**,/js/**,/img/**,/favicon.ico"
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# tomcat 端口配置
|
||||||
|
server:
|
||||||
|
port: 8081
|
||||||
|
# 数据源配置
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://${MYSQL_HOST:8.163.14.142}:${MYSQL_PORT:30006}/${DB_NAME:offerpie_test}?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
|
||||||
|
username: ${MYSQL_USERNAME:root}
|
||||||
|
password: ${MYSQL_PASSWORD:^CgDatabase2020}
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
redis:
|
||||||
|
host: ${REDIS_HOST:8.163.14.142}
|
||||||
|
password: ${REDIS_PASSWORD:#8kPCdAsser}
|
||||||
|
port: ${REDIS_PORT:30089}
|
||||||
|
timeout: 10s
|
||||||
|
database: 1
|
||||||
|
|
||||||
|
# 电子邮箱
|
||||||
|
email:
|
||||||
|
status: close
|
||||||
|
account: ${EMAIL_ACCOUNT:xxx@163.com}
|
||||||
|
authorization: ${EMAIL_AUTHORIZATION:123456}
|
||||||
|
|
||||||
|
# 微信支付
|
||||||
|
wx_pay:
|
||||||
|
status: close
|
||||||
|
|
||||||
|
# 支付宝支付
|
||||||
|
alipay:
|
||||||
|
status: close
|
||||||
|
|
||||||
|
app:
|
||||||
|
# 加密秘钥配置
|
||||||
|
secret:
|
||||||
|
token: ${SECRET_TOKEN:Aa123123}
|
||||||
|
|
||||||
|
# 登陆配置
|
||||||
|
login:
|
||||||
|
token:
|
||||||
|
exceed_time: 5184000
|
||||||
|
device_online_quantity: 3
|
||||||
|
|
||||||
|
# 短信
|
||||||
|
sms:
|
||||||
|
service_provider: aliyun
|
||||||
|
aliyun:
|
||||||
|
access_key_id: LTAI5tJBVUUJhB7yp14UDzVf
|
||||||
|
access_key_secret: Opf0iO5FKNrdwI63DPhXazW7utAGTj
|
||||||
|
|
||||||
|
oss:
|
||||||
|
service_provider: aliyun
|
||||||
|
aliyun:
|
||||||
|
access_key_id: LTAI5tEdLKKQUKhTyUpfH5Mk
|
||||||
|
access_key_secret: RjUdTrq0V5qA4b3BUElNhXqs3ZLp5k
|
||||||
|
|
||||||
|
# 防刷配置
|
||||||
|
prevent_replay:
|
||||||
|
if_open: false
|
||||||
|
interval_time: 20
|
||||||
|
limit_number: 20
|
||||||
|
|
||||||
|
# 开放接口
|
||||||
|
ignore:
|
||||||
|
urls: "/public/**,/page/**,/static/**,/css/**,/js/**,/img/**,/favicon.ico"
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# tomcat config
|
||||||
|
server:
|
||||||
|
tomcat:
|
||||||
|
accept-count: 100
|
||||||
|
threads:
|
||||||
|
max: 200
|
||||||
|
min-spare: 10
|
||||||
|
servlet:
|
||||||
|
context-path: /admin
|
||||||
|
# spring config
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: admin
|
||||||
|
profiles:
|
||||||
|
active: ${PROFILES_ACTIVE:dev}
|
||||||
|
datasource:
|
||||||
|
hikari:
|
||||||
|
maximum-pool-size: 50
|
||||||
|
minimum-idle: 20
|
||||||
|
connection-timeout: 5000
|
||||||
|
idle-timeout: 300000
|
||||||
|
max-lifetime: 900000
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 4MB
|
||||||
|
max-request-size: 20MB
|
||||||
|
# Thymeleaf 配置
|
||||||
|
thymeleaf:
|
||||||
|
prefix: classpath:/templates/
|
||||||
|
suffix: .html
|
||||||
|
mode: HTML
|
||||||
|
encoding: UTF-8
|
||||||
|
# 开启静态资源映射
|
||||||
|
web:
|
||||||
|
resources:
|
||||||
|
add-mappings: true
|
||||||
|
static-locations: classpath:/static/
|
||||||
|
cache:
|
||||||
|
redis:
|
||||||
|
cache-null-values: true
|
||||||
|
key-prefix: "${spring.application.name}:"
|
||||||
|
time-to-live: 24h
|
||||||
|
jackson:
|
||||||
|
default-property-inclusion: non_null
|
||||||
|
property-naming-strategy: LOWER_CAMEL_CASE
|
||||||
|
serialization:
|
||||||
|
fail-on-empty-beans: false
|
||||||
|
write-date-keys-as-timestamps: true
|
||||||
|
write-date-timestamps-as-nanoseconds: false
|
||||||
|
write-dates-as-timestamps: true
|
||||||
|
deserialization:
|
||||||
|
fail-on-unknown-properties: false
|
||||||
|
fail-on-numbers-for-enums: true
|
||||||
|
read-date-timestamps-as-nanoseconds: false
|
||||||
|
# mybatis plus config
|
||||||
|
mybatis-plus:
|
||||||
|
mapper-locations: classpath*:mapper/**/*.xml
|
||||||
|
global-config:
|
||||||
|
db-config:
|
||||||
|
id-type: assign_id
|
||||||
|
insert-strategy: not_null
|
||||||
|
update-strategy: not_null
|
||||||
|
logic-delete-field: isDelete
|
||||||
|
logic-delete-value: '1'
|
||||||
|
logic-not-delete-value: '0'
|
||||||
|
configuration:
|
||||||
|
map-underscore-to-camel-case: true
|
||||||
|
auto-mapping-unknown-column-behavior: warning
|
||||||
|
cache-enabled: false
|
||||||
|
call-setters-on-nulls: true
|
||||||
|
jdbc-type-for-null: 'null'
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
org:
|
||||||
|
mybatis: info
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* 认证相关 API
|
||||||
|
*/
|
||||||
|
const authApi = {
|
||||||
|
/** 登录 */
|
||||||
|
login(username, password) {
|
||||||
|
return request.post('/public/login', { username, password });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 订单管理 API
|
||||||
|
*/
|
||||||
|
const orderApi = {
|
||||||
|
/** 订单分页列表 */
|
||||||
|
list(params) {
|
||||||
|
return request.post('/order/list', params);
|
||||||
|
},
|
||||||
|
/** 订单统计 */
|
||||||
|
stats() {
|
||||||
|
return request.get('/order/stats');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 商品管理 API
|
||||||
|
*/
|
||||||
|
const productApi = {
|
||||||
|
/** 商品分页列表 */
|
||||||
|
list(params) {
|
||||||
|
return request.post('/product/list', params);
|
||||||
|
},
|
||||||
|
/** 新增/编辑商品 */
|
||||||
|
save(data) {
|
||||||
|
return request.post('/product/save', data);
|
||||||
|
},
|
||||||
|
/** 上架/下架 */
|
||||||
|
updateStatus(id, status) {
|
||||||
|
return request.post('/product/updateStatus?id=' + id + '&status=' + status);
|
||||||
|
},
|
||||||
|
/** 删除商品 */
|
||||||
|
delete(id) {
|
||||||
|
return request.post('/product/delete?id=' + id);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* 用户管理 API
|
||||||
|
*/
|
||||||
|
const userApi = {
|
||||||
|
/** 用户分页列表 */
|
||||||
|
list(params) {
|
||||||
|
return request.post('/user/list', params);
|
||||||
|
},
|
||||||
|
/** 用户统计 */
|
||||||
|
stats() {
|
||||||
|
return request.get('/user/stats');
|
||||||
|
},
|
||||||
|
/** 禁用/启用用户 */
|
||||||
|
updateStatus(userId, status) {
|
||||||
|
return request.post('/user/updateStatus?userId=' + userId + '&status=' + status);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 统一请求封装
|
||||||
|
*/
|
||||||
|
const request = (() => {
|
||||||
|
const BASE_URL = '/admin';
|
||||||
|
|
||||||
|
/** 从 cookie 中获取 Token */
|
||||||
|
function getToken() {
|
||||||
|
const match = document.cookie.match(/(?:^|;\s*)Token=([^;]*)/);
|
||||||
|
return match ? match[1] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统一请求方法 */
|
||||||
|
async function http(url, options = {}) {
|
||||||
|
const config = {
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Token': getToken(), ...options.headers },
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
const response = await fetch(BASE_URL + url, config);
|
||||||
|
|
||||||
|
// 401 未登录,跳转登录页
|
||||||
|
if (response.status === 401) {
|
||||||
|
window.location.href = BASE_URL + '/page/login';
|
||||||
|
return Promise.reject('未登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应体
|
||||||
|
let result;
|
||||||
|
try { result = await response.json(); } catch (e) {
|
||||||
|
const msg = 'HTTP ' + response.status + ' 请求失败';
|
||||||
|
ElementPlus ? ElementPlus.ElMessage.error(msg) : alert(msg);
|
||||||
|
return Promise.reject(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务异常:code 不为 "0" 表示失败
|
||||||
|
if (result.code !== '0') {
|
||||||
|
const msg = result.msg || '请求失败';
|
||||||
|
ElementPlus ? ElementPlus.ElMessage.error(msg) : alert(msg);
|
||||||
|
return Promise.reject(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get(url, params) {
|
||||||
|
const query = params ? '?' + new URLSearchParams(params).toString() : '';
|
||||||
|
return http(url + query, { method: 'GET' });
|
||||||
|
},
|
||||||
|
post(url, data) {
|
||||||
|
return http(url, { method: 'POST', body: data != null ? JSON.stringify(data) : undefined });
|
||||||
|
},
|
||||||
|
put(url, data) {
|
||||||
|
return http(url, { method: 'PUT', body: JSON.stringify(data) });
|
||||||
|
},
|
||||||
|
del(url, data) {
|
||||||
|
return http(url, { method: 'DELETE', body: data ? JSON.stringify(data) : undefined });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
const DashboardView = {
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<h2 style="margin-bottom: 24px; font-size: 18px; font-weight: 600; color: #303133;">数据概览</h2>
|
||||||
|
<el-row :gutter="20" style="margin-bottom: 24px;">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" body-style="padding: 24px;">
|
||||||
|
<el-statistic title="用户总数" :value="userStats.total"><template #suffix><span style="font-size: 12px; color: #909399;"> 人</span></template></el-statistic>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" body-style="padding: 24px;">
|
||||||
|
<el-statistic title="今日新增" :value="userStats.today"><template #suffix><span style="font-size: 12px; color: #909399;"> 人</span></template></el-statistic>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" body-style="padding: 24px;">
|
||||||
|
<el-statistic title="本月订单" :value="orderStats.monthPaid"><template #suffix><span style="font-size: 12px; color: #909399;"> 笔</span></template></el-statistic>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" body-style="padding: 24px;">
|
||||||
|
<el-statistic title="本月收入" :value="monthIncome"><template #prefix><span style="font-size: 14px;">¥</span></template></el-statistic>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" body-style="padding: 24px;"><el-statistic title="本周新增用户" :value="userStats.thisWeek"/></el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" body-style="padding: 24px;"><el-statistic title="本月新增用户" :value="userStats.thisMonth"/></el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" body-style="padding: 24px;"><el-statistic title="本周订单" :value="orderStats.weekPaid"/></el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card shadow="hover" body-style="padding: 24px;">
|
||||||
|
<el-statistic title="本周收入" :value="weekIncome"><template #prefix><span style="font-size: 14px;">¥</span></template></el-statistic>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
setup() {
|
||||||
|
const { ref, onMounted, computed } = Vue;
|
||||||
|
const userStats = ref({ total: 0, today: 0, thisWeek: 0, thisMonth: 0 });
|
||||||
|
const orderStats = ref({ totalCount: 0, paidCount: 0, todayPaid: 0, weekPaid: 0, monthPaid: 0, monthIncome: 0, weekIncome: 0 });
|
||||||
|
const monthIncome = computed(() => (orderStats.value.monthIncome / 100).toFixed(2));
|
||||||
|
const weekIncome = computed(() => (orderStats.value.weekIncome / 100).toFixed(2));
|
||||||
|
const toNum = (obj) => { const r = {}; for (const k in obj) r[k] = Number(obj[k]); return r; };
|
||||||
|
onMounted(async () => {
|
||||||
|
try { userStats.value = toNum(await userApi.stats()); } catch(e) {}
|
||||||
|
try { orderStats.value = toNum(await orderApi.stats()); } catch(e) {}
|
||||||
|
});
|
||||||
|
return { userStats, orderStats, monthIncome, weekIncome };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
const OrderView = {
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<el-row :gutter="16" style="margin-bottom: 20px;">
|
||||||
|
<el-col :span="6"><el-card shadow="hover" body-style="padding: 20px;"><el-statistic title="总订单" :value="stats.totalCount"/></el-card></el-col>
|
||||||
|
<el-col :span="6"><el-card shadow="hover" body-style="padding: 20px;"><el-statistic title="已支付" :value="stats.paidCount"/></el-card></el-col>
|
||||||
|
<el-col :span="6"><el-card shadow="hover" body-style="padding: 20px;"><el-statistic title="今日支付" :value="stats.todayPaid"/></el-card></el-col>
|
||||||
|
<el-col :span="6"><el-card shadow="hover" body-style="padding: 20px;"><el-statistic title="今日收入(元)" :value="todayIncome"/></el-card></el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header><span style="font-size: 16px; font-weight: 600;">订单管理</span></template>
|
||||||
|
<el-form :inline="true" style="margin-bottom: 16px;">
|
||||||
|
<el-form-item label="手机号"><el-input v-model="query.mobileNumber" placeholder="用户手机号" clearable @clear="loadData" style="width: 150px;"/></el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="query.status" placeholder="全部" clearable @change="loadData" style="width: 110px;">
|
||||||
|
<el-option label="待支付" :value="0"/><el-option label="已支付" :value="1"/><el-option label="已退款" :value="2"/><el-option label="已关闭" :value="3"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="渠道">
|
||||||
|
<el-select v-model="query.payChannel" placeholder="全部" clearable @change="loadData" style="width: 100px;">
|
||||||
|
<el-option label="微信" :value="1"/><el-option label="支付宝" :value="2"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item><el-button type="primary" @click="loadData">查询</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-table :data="tableData" stripe v-loading="loading" style="width: 100%;">
|
||||||
|
<el-table-column prop="orderNo" label="订单编号" min-width="150"/>
|
||||||
|
<el-table-column prop="userNick" label="用户" min-width="100"/>
|
||||||
|
<el-table-column prop="mobileNumber" label="手机号" min-width="120"/>
|
||||||
|
<el-table-column prop="productName" label="商品" min-width="120"/>
|
||||||
|
<el-table-column label="金额(元)" width="100" align="center">
|
||||||
|
<template #default="{row}">{{(row.payAmount/100).toFixed(2)}}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="渠道" width="80" align="center">
|
||||||
|
<template #default="{row}"><el-tag size="small" :type="row.payChannel===1?'success':'primary'">{{row.payChannel===1?'微信':'支付宝'}}</el-tag></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="90" align="center">
|
||||||
|
<template #default="{row}"><el-tag size="small" :type="statusType(row.status)">{{statusText(row.status)}}</el-tag></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="下单时间" min-width="160">
|
||||||
|
<template #default="{row}">{{formatTime(row.createTime)}}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="支付时间" min-width="160">
|
||||||
|
<template #default="{row}">{{formatTime(row.payTime)}}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div style="margin-top: 16px; display: flex; justify-content: flex-end;">
|
||||||
|
<el-pagination background layout="total, prev, pager, next" :total="total" :page-size="query.pageSize" v-model:current-page="query.pageNum" @current-change="loadData"/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
setup() {
|
||||||
|
const { ref, reactive, onMounted, computed } = Vue;
|
||||||
|
const loading = ref(false);
|
||||||
|
const tableData = ref([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const stats = ref({ totalCount: 0, paidCount: 0, todayPaid: 0, todayIncome: 0 });
|
||||||
|
const todayIncome = computed(() => (stats.value.todayIncome / 100).toFixed(2));
|
||||||
|
const query = reactive({ pageNum: 1, pageSize: 10, mobileNumber: '', status: null, payChannel: null });
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try { const res = await orderApi.list(query); tableData.value = res.list; total.value = Number(res.total); } catch(e) {}
|
||||||
|
finally { loading.value = false; }
|
||||||
|
};
|
||||||
|
const loadStats = async () => { try { const s = await orderApi.stats(); const r = {}; for (const k in s) r[k] = Number(s[k]); stats.value = r; } catch(e) {} };
|
||||||
|
const statusText = (s) => ['待支付','已支付','已退款','已关闭'][s] || '-';
|
||||||
|
const statusType = (s) => ['warning','success','info','danger'][s] || 'info';
|
||||||
|
const formatTime = (ts) => {
|
||||||
|
if (!ts) return '-';
|
||||||
|
const d = new Date(ts);
|
||||||
|
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0') + ' ' + String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0');
|
||||||
|
};
|
||||||
|
onMounted(() => { loadData(); loadStats(); });
|
||||||
|
return { loading, tableData, total, stats, todayIncome, query, loadData, statusText, statusType, formatTime };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
const ProductView = {
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||||
|
<span style="font-size: 16px; font-weight: 600;">商品管理</span>
|
||||||
|
<el-button type="primary" @click="openDialog(null)"><el-icon><Plus/></el-icon> 新增商品</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :inline="true" style="margin-bottom: 16px;">
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="query.status" placeholder="全部" clearable @change="loadData" style="width: 120px;">
|
||||||
|
<el-option label="上架" :value="1"/><el-option label="下架" :value="0"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item><el-button type="primary" @click="loadData">查询</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-table :data="tableData" stripe v-loading="loading" style="width: 100%;">
|
||||||
|
<el-table-column prop="productName" label="商品名称" min-width="140"/>
|
||||||
|
<el-table-column prop="tag" label="标签" width="120"/>
|
||||||
|
<el-table-column label="价格(元)" width="100" align="center">
|
||||||
|
<template #default="{row}">{{(row.price/100).toFixed(2)}}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="划线价(元)" width="110" align="center">
|
||||||
|
<template #default="{row}">{{row.originalPrice?(row.originalPrice/100).toFixed(2):'-'}}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="durationDays" label="有效天数" width="90" align="center"/>
|
||||||
|
<el-table-column prop="sortOrder" label="排序" width="70" align="center"/>
|
||||||
|
<el-table-column label="状态" width="80" align="center">
|
||||||
|
<template #default="{row}"><el-tag :type="row.status===1?'success':'info'" size="small">{{row.status===1?'上架':'下架'}}</el-tag></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-button link type="primary" size="small" @click="openDialog(row)">编辑</el-button>
|
||||||
|
<el-button link :type="row.status===1?'warning':'success'" size="small" @click="toggleStatus(row)">{{row.status===1?'下架':'上架'}}</el-button>
|
||||||
|
<el-popconfirm title="确定删除该商品?" @confirm="handleDelete(row)"><template #reference><el-button link type="danger" size="small">删除</el-button></template></el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div style="margin-top: 16px; display: flex; justify-content: flex-end;">
|
||||||
|
<el-pagination background layout="total, prev, pager, next" :total="total" :page-size="query.pageSize" v-model:current-page="query.pageNum" @current-change="loadData"/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" :title="form.id?'编辑商品':'新增商品'" width="520px" destroy-on-close>
|
||||||
|
<el-form :model="form" label-width="90px">
|
||||||
|
<el-form-item label="商品名称"><el-input v-model="form.productName" maxlength="32"/></el-form-item>
|
||||||
|
<el-form-item label="标签"><el-input v-model="form.tag" maxlength="16" placeholder="如:限时优惠"/></el-form-item>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12"><el-form-item label="价格(分)"><el-input-number v-model="form.price" :min="1" style="width: 100%;"/></el-form-item></el-col>
|
||||||
|
<el-col :span="12"><el-form-item label="划线价(分)"><el-input-number v-model="form.originalPrice" :min="0" style="width: 100%;"/></el-form-item></el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12"><el-form-item label="月价(分)"><el-input-number v-model="form.monthlyPrice" :min="0" style="width: 100%;"/></el-form-item></el-col>
|
||||||
|
<el-col :span="12"><el-form-item label="有效天数"><el-input-number v-model="form.durationDays" :min="1" style="width: 100%;"/></el-form-item></el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12"><el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" style="width: 100%;"/></el-form-item></el-col>
|
||||||
|
<el-col :span="12"><el-form-item label="主推"><el-switch v-model="form.isFeatured" :active-value="1" :inactive-value="0"/></el-form-item></el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="按钮文字"><el-input v-model="form.buyButtonText" maxlength="16" placeholder="如:立即开通"/></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible=false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
setup() {
|
||||||
|
const { ref, reactive, onMounted } = Vue;
|
||||||
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const tableData = ref([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const query = reactive({ pageNum: 1, pageSize: 10, status: null });
|
||||||
|
const form = reactive({ id: null, productName: '', tag: '', price: null, originalPrice: null, monthlyPrice: null, durationDays: null, sortOrder: 0, isFeatured: 0, buyButtonText: '' });
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try { const res = await productApi.list(query); tableData.value = res.list; total.value = Number(res.total); } catch(e) {}
|
||||||
|
finally { loading.value = false; }
|
||||||
|
};
|
||||||
|
const openDialog = (row) => {
|
||||||
|
if (row) { Object.assign(form, { id: row.id, productName: row.productName, tag: row.tag, price: row.price, originalPrice: row.originalPrice, monthlyPrice: row.monthlyPrice, durationDays: row.durationDays, sortOrder: row.sortOrder, isFeatured: row.isFeatured, buyButtonText: row.buyButtonText }); }
|
||||||
|
else { Object.assign(form, { id: null, productName: '', tag: '', price: null, originalPrice: null, monthlyPrice: null, durationDays: null, sortOrder: 0, isFeatured: 0, buyButtonText: '' }); }
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!form.productName || !form.price || !form.durationDays) { ElementPlus.ElMessage.warning('请填写必填项'); return; }
|
||||||
|
saving.value = true;
|
||||||
|
try { await productApi.save(form); dialogVisible.value = false; ElementPlus.ElMessage.success('保存成功'); loadData(); } catch(e) {}
|
||||||
|
finally { saving.value = false; }
|
||||||
|
};
|
||||||
|
const toggleStatus = async (row) => {
|
||||||
|
const newStatus = row.status === 1 ? 0 : 1;
|
||||||
|
try { await productApi.updateStatus(row.id, newStatus); row.status = newStatus; ElementPlus.ElMessage.success('操作成功'); } catch(e) {}
|
||||||
|
};
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try { await productApi.delete(row.id); ElementPlus.ElMessage.success('删除成功'); loadData(); } catch(e) {}
|
||||||
|
};
|
||||||
|
onMounted(loadData);
|
||||||
|
return { loading, saving, dialogVisible, tableData, total, query, form, loadData, openDialog, handleSave, toggleStatus, handleDelete };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
const UserView = {
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header><span style="font-size: 16px; font-weight: 600;">用户管理</span></template>
|
||||||
|
<el-form :inline="true" style="margin-bottom: 16px;">
|
||||||
|
<el-form-item label="手机号"><el-input v-model="query.mobileNumber" placeholder="手机号" clearable @clear="loadData" style="width: 150px;"/></el-form-item>
|
||||||
|
<el-form-item label="昵称"><el-input v-model="query.keyword" placeholder="昵称" clearable @clear="loadData" style="width: 150px;"/></el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="query.status" placeholder="全部" clearable @change="loadData" style="width: 100px;">
|
||||||
|
<el-option label="正常" :value="0"/><el-option label="禁用" :value="1"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item><el-button type="primary" @click="loadData">查询</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-table :data="tableData" stripe v-loading="loading" style="width: 100%;">
|
||||||
|
<el-table-column prop="nick" label="昵称" min-width="100"/>
|
||||||
|
<el-table-column prop="realName" label="真实姓名" min-width="100"/>
|
||||||
|
<el-table-column prop="mobileNumber" label="手机号" min-width="130"/>
|
||||||
|
<el-table-column prop="resumeCount" label="简历数" width="80" align="center"/>
|
||||||
|
<el-table-column label="状态" width="80" align="center">
|
||||||
|
<template #default="{row}"><el-tag :type="row.status===0?'success':'danger'" size="small">{{row.status===0?'正常':'禁用'}}</el-tag></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="注册时间" min-width="160">
|
||||||
|
<template #default="{row}">{{formatTime(row.createTime)}}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-popconfirm :title="row.status===0?'确定禁用该用户?':'确定启用该用户?'" @confirm="toggleStatus(row)">
|
||||||
|
<template #reference><el-button link :type="row.status===0?'danger':'success'" size="small">{{row.status===0?'禁用':'启用'}}</el-button></template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div style="margin-top: 16px; display: flex; justify-content: flex-end;">
|
||||||
|
<el-pagination background layout="total, prev, pager, next" :total="total" :page-size="query.pageSize" v-model:current-page="query.pageNum" @current-change="loadData"/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
setup() {
|
||||||
|
const { ref, reactive, onMounted } = Vue;
|
||||||
|
const loading = ref(false);
|
||||||
|
const tableData = ref([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const query = reactive({ pageNum: 1, pageSize: 10, mobileNumber: '', keyword: '', status: null });
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try { const res = await userApi.list(query); tableData.value = res.list; total.value = Number(res.total); } catch(e) {}
|
||||||
|
finally { loading.value = false; }
|
||||||
|
};
|
||||||
|
const toggleStatus = async (row) => {
|
||||||
|
const newStatus = row.status === 0 ? 1 : 0;
|
||||||
|
try { await userApi.updateStatus(row.id, newStatus); row.status = newStatus; ElementPlus.ElMessage.success('操作成功'); } catch(e) {}
|
||||||
|
};
|
||||||
|
const formatTime = (ts) => {
|
||||||
|
if (!ts) return '-';
|
||||||
|
const d = new Date(ts);
|
||||||
|
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0') + ' ' + String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0');
|
||||||
|
};
|
||||||
|
onMounted(loadData);
|
||||||
|
return { loading, tableData, total, query, loadData, toggleStatus, formatTime };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>OfferPie 管理后台</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.7.3/dist/index.css"/>
|
||||||
|
<style>
|
||||||
|
html, body, #app { margin: 0; padding: 0; height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; }
|
||||||
|
.layout { height: 100%; }
|
||||||
|
.aside { background: #1d1e3c; overflow-y: auto; }
|
||||||
|
.aside .logo { height: 60px; display: flex; align-items: center; justify-content: center; padding: 0 16px; border-bottom: 1px solid rgba(255,255,255,0.06); }
|
||||||
|
.aside .logo img { height: 32px; max-width: 100%; object-fit: contain; }
|
||||||
|
.aside .el-menu { border-right: none; background: transparent; padding-top: 8px; }
|
||||||
|
.aside .el-menu-item { margin: 4px 10px; border-radius: 6px; }
|
||||||
|
.aside .el-menu-item.is-active { background: rgba(64,158,255,0.18) !important; }
|
||||||
|
.header { background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; height: 60px; box-shadow: 0 1px 4px rgba(0,21,41,0.08); z-index: 10; }
|
||||||
|
.header .title { font-size: 16px; font-weight: 600; color: #303133; }
|
||||||
|
.header .admin-info { display: flex; align-items: center; gap: 10px; font-size: 14px; color: #606266; }
|
||||||
|
.main-content { padding: 20px; background: #f0f2f5; overflow-y: auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app" v-cloak>
|
||||||
|
<el-container class="layout">
|
||||||
|
<el-aside width="210px" class="aside">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/admin/img/logo.png" alt="logo"/>
|
||||||
|
</div>
|
||||||
|
<el-menu :default-active="currentView" background-color="#1d1e3c" text-color="rgba(255,255,255,0.65)" active-text-color="#fff" @select="handleMenuSelect">
|
||||||
|
<el-menu-item index="dashboard"><el-icon><data-board/></el-icon><span>数据概览</span></el-menu-item>
|
||||||
|
<el-menu-item index="user"><el-icon><user/></el-icon><span>用户管理</span></el-menu-item>
|
||||||
|
<el-menu-item index="order"><el-icon><tickets/></el-icon><span>订单管理</span></el-menu-item>
|
||||||
|
<el-menu-item index="product"><el-icon><goods/></el-icon><span>商品管理</span></el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
<el-container>
|
||||||
|
<el-header class="header">
|
||||||
|
<span class="title">{{ title }}</span>
|
||||||
|
<div class="admin-info">
|
||||||
|
<el-button type="primary" plain @click="handleLogout">退出登录</el-button>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<el-main class="main-content">
|
||||||
|
<component :is="currentComponent"></component>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/vue@3.4.21/dist/vue.global.prod.js"></script>
|
||||||
|
<script src="https://unpkg.com/element-plus@2.7.3/dist/index.full.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/@element-plus/icons-vue@2.3.1/dist/index.iife.min.js"></script>
|
||||||
|
<script src="/admin/js/request.js"></script>
|
||||||
|
<script src="/admin/js/api/auth.js"></script>
|
||||||
|
<script src="/admin/js/api/user.js"></script>
|
||||||
|
<script src="/admin/js/api/order.js"></script>
|
||||||
|
<script src="/admin/js/api/product.js"></script>
|
||||||
|
<script src="/admin/js/views/dashboard.js"></script>
|
||||||
|
<script src="/admin/js/views/user.js"></script>
|
||||||
|
<script src="/admin/js/views/order.js"></script>
|
||||||
|
<script src="/admin/js/views/product.js"></script>
|
||||||
|
<style>[v-cloak] { display: none; }</style>
|
||||||
|
<script>
|
||||||
|
const { createApp, ref, computed } = Vue;
|
||||||
|
const app = createApp({
|
||||||
|
setup() {
|
||||||
|
const currentView = ref('dashboard');
|
||||||
|
const views = { dashboard: DashboardView, user: UserView, order: OrderView, product: ProductView };
|
||||||
|
const titles = { dashboard: '数据概览', user: '用户管理', order: '订单管理', product: '商品管理' };
|
||||||
|
const currentComponent = computed(() => views[currentView.value]);
|
||||||
|
const title = computed(() => titles[currentView.value]);
|
||||||
|
const handleMenuSelect = (index) => { currentView.value = index; };
|
||||||
|
const handleLogout = () => {
|
||||||
|
document.cookie = 'Token=; path=/; max-age=0';
|
||||||
|
window.location.href = '/admin/page/login';
|
||||||
|
};
|
||||||
|
return { currentView, currentComponent, title, handleMenuSelect, handleLogout };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 注册 Element Plus 图标(容错:CDN 未加载时不阻断应用)
|
||||||
|
if (window.ElementPlusIconsVue) {
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.use(ElementPlus);
|
||||||
|
app.mount('#app');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>OfferPie 管理后台</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.7.3/dist/index.css"/>
|
||||||
|
<style>[v-cloak] { display: none; }</style>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 0; }
|
||||||
|
#app {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
.left-panel {
|
||||||
|
flex: 1;
|
||||||
|
background: linear-gradient(160deg, #1d1e3c 0%, #2b2d5e 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 80px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.left-panel::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(64, 158, 255, 0.06);
|
||||||
|
top: 10%;
|
||||||
|
right: -100px;
|
||||||
|
}
|
||||||
|
.left-panel .logo { display: flex; align-items: center; gap: 14px; margin-bottom: 24px; }
|
||||||
|
.left-panel .logo img { height: 44px; }
|
||||||
|
.left-panel .logo span { font-size: 26px; font-weight: 600; color: #fff; }
|
||||||
|
.left-panel .desc { font-size: 15px; color: rgba(255,255,255,0.5); line-height: 1.8; }
|
||||||
|
.left-panel .footer { position: absolute; bottom: 30px; left: 80px; font-size: 12px; color: rgba(255,255,255,0.2); }
|
||||||
|
.right-panel {
|
||||||
|
width: 480px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
.login-box { width: 100%; max-width: 340px; }
|
||||||
|
.login-box h2 { font-size: 22px; color: #303133; margin-bottom: 6px; }
|
||||||
|
.login-box .sub { font-size: 13px; color: #909399; margin-bottom: 32px; }
|
||||||
|
.login-box .el-button { width: 100%; margin-top: 8px; }
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.left-panel { display: none; }
|
||||||
|
.right-panel { width: 100%; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app" v-cloak>
|
||||||
|
<div class="left-panel">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/admin/img/logo.png" alt="logo"/>
|
||||||
|
<span>OfferPie</span>
|
||||||
|
</div>
|
||||||
|
<div class="desc">运营管理后台</div>
|
||||||
|
<div class="footer">© 2025 OfferPie</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="login-box">
|
||||||
|
<h2>欢迎回来</h2>
|
||||||
|
<p class="sub">请登录管理员账号</p>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" @keyup.enter="handleLogin">
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input v-model="form.username" placeholder="账号" prefix-icon="User" size="large" maxlength="32"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input v-model="form.password" type="password" placeholder="密码" prefix-icon="Lock" size="large" maxlength="32" show-password/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" size="large" :loading="loading" @click="handleLogin">登录</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/vue@3.4.21/dist/vue.global.prod.js"></script>
|
||||||
|
<script src="https://unpkg.com/element-plus@2.7.3/dist/index.full.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/@element-plus/icons-vue@2.3.1/dist/index.iife.min.js"></script>
|
||||||
|
<script src="/admin/js/request.js"></script>
|
||||||
|
<script src="/admin/js/api/auth.js"></script>
|
||||||
|
<script>
|
||||||
|
const { createApp, ref, reactive } = Vue;
|
||||||
|
const app = createApp({
|
||||||
|
setup() {
|
||||||
|
const formRef = ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const form = reactive({ username: '', password: '' });
|
||||||
|
const rules = {
|
||||||
|
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }, { min: 6, max: 32, message: '密码长度6-32位', trigger: 'blur' }]
|
||||||
|
};
|
||||||
|
const handleLogin = async () => {
|
||||||
|
const valid = await formRef.value.validate().catch(() => false);
|
||||||
|
if (!valid) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await authApi.login(form.username, form.password);
|
||||||
|
window.location.href = '/admin/page/index';
|
||||||
|
} catch (e) { }
|
||||||
|
finally { loading.value = false; }
|
||||||
|
};
|
||||||
|
return { formRef, form, rules, loading, handleLogin };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 注册 Element Plus 图标(容错)
|
||||||
|
if (window.ElementPlusIconsVue) {
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.use(ElementPlus);
|
||||||
|
app.mount('#app');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* OfferPie Backend Client-API 蓝绿部署流水线(测试环境)
|
||||||
|
*
|
||||||
|
* 架构:Jenkins 本地编译 → scp 镜像到目标机 → SSH 远程蓝绿切换
|
||||||
|
* 目标机目录:/opt/offerpie/client-api/
|
||||||
|
*
|
||||||
|
* 与生产差异:
|
||||||
|
* - 部署目标为测试服务器
|
||||||
|
* - 默认分支 test
|
||||||
|
* - 镜像 tag = test(与生产 latest 隔离,避免同一 Jenkins 并发构建串包)
|
||||||
|
* - tar 文件名带 tag(双保险)
|
||||||
|
* - 运行时 PROFILES_ACTIVE=test(在 docker-compose.test.client-api.yml 中设置)
|
||||||
|
*/
|
||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
choice(name: 'BRANCH', choices: ['test', 'dev', 'pre', 'master'], description: '选择要部署的分支')
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
// 目标服务器配置(测试)
|
||||||
|
DEPLOY_HOST = '8.163.131.234'
|
||||||
|
DEPLOY_PORT = '22'
|
||||||
|
DEPLOY_USER = 'root'
|
||||||
|
DEPLOY_PASS = 'Mzpy520@126.com'
|
||||||
|
|
||||||
|
// 项目配置
|
||||||
|
IMAGE_NAME = 'offerpie-backend-client'
|
||||||
|
IMAGE_TAG = 'test'
|
||||||
|
IMAGE_TAR = 'offerpie-backend-client-test.tar'
|
||||||
|
CONTAINER_PREFIX = 'offerpie-backend-client'
|
||||||
|
REMOTE_DIR = '/opt/offerpie/client-api'
|
||||||
|
HEALTH_URL = 'http://localhost:8080/api/public/actuator/health'
|
||||||
|
|
||||||
|
// SSH 命令前缀
|
||||||
|
SSH_CMD = "sshpass -p '${DEPLOY_PASS}' ssh -o StrictHostKeyChecking=no -p ${DEPLOY_PORT} ${DEPLOY_USER}@${DEPLOY_HOST}"
|
||||||
|
SCP_CMD = "sshpass -p '${DEPLOY_PASS}' scp -o StrictHostKeyChecking=no -P ${DEPLOY_PORT}"
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('环境检查') {
|
||||||
|
steps {
|
||||||
|
sh 'sshpass -V'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('拉取代码') {
|
||||||
|
steps {
|
||||||
|
echo "拉取 ${params.BRANCH} 分支代码"
|
||||||
|
git branch: "${params.BRANCH}",
|
||||||
|
credentialsId: 'gitea-fab089c1-b55d-4b58-9fad',
|
||||||
|
url: 'http://git.jianshixingqiu.com/offerpai/offerpai_backend.git'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('本地编译') {
|
||||||
|
steps {
|
||||||
|
echo "开始构建镜像"
|
||||||
|
sh "docker build -f client-api/Dockerfile -t ${IMAGE_NAME}:${IMAGE_TAG} ."
|
||||||
|
echo "导出镜像"
|
||||||
|
sh "docker save -o ${IMAGE_TAR} ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('文件传输') {
|
||||||
|
steps {
|
||||||
|
echo "传输文件到目标服务器"
|
||||||
|
sh "${SSH_CMD} 'mkdir -p ${REMOTE_DIR}'"
|
||||||
|
sh "${SCP_CMD} ${IMAGE_TAR} ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/"
|
||||||
|
sh "${SCP_CMD} client-api/nginx.conf ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/nginx.conf"
|
||||||
|
sh "${SCP_CMD} docker-compose.test.client-api.yml ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/docker-compose.yml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('加载镜像') {
|
||||||
|
steps {
|
||||||
|
sh "${SSH_CMD} 'docker load < ${REMOTE_DIR}/${IMAGE_TAR}'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('检测部署目标') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def blueRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-blue'", returnStdout: true).trim()
|
||||||
|
def greenRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-green'", returnStdout: true).trim()
|
||||||
|
|
||||||
|
env.DEPLOY_TARGET = ''
|
||||||
|
|
||||||
|
if (blueRunning && !greenRunning) {
|
||||||
|
env.DEPLOY_TARGET = 'green'
|
||||||
|
}
|
||||||
|
if (greenRunning && !blueRunning) {
|
||||||
|
env.DEPLOY_TARGET = 'blue'
|
||||||
|
}
|
||||||
|
if (!env.DEPLOY_TARGET) {
|
||||||
|
echo "当前环境未部署服务或状态异常,默认部署 blue"
|
||||||
|
env.DEPLOY_TARGET = 'blue'
|
||||||
|
}
|
||||||
|
|
||||||
|
env.OTHER_TARGET = (env.DEPLOY_TARGET == 'blue') ? 'green' : 'blue'
|
||||||
|
echo "当前激活: ${env.OTHER_TARGET},即将部署: ${env.DEPLOY_TARGET}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('启动新版本') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def existingContainer = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'", returnStdout: true).trim()
|
||||||
|
if (existingContainer) {
|
||||||
|
sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'"
|
||||||
|
}
|
||||||
|
sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d ${env.DEPLOY_TARGET}'"
|
||||||
|
sh 'sleep 70'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('检查Nginx') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def nginxExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim()
|
||||||
|
if (!nginxExists) {
|
||||||
|
echo "首次部署,初始化 Nginx 容器"
|
||||||
|
sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d nginx'"
|
||||||
|
sh 'sleep 3'
|
||||||
|
} else {
|
||||||
|
def nginxRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim()
|
||||||
|
if (!nginxRunning) {
|
||||||
|
echo "Nginx 容器已停止,重新启动"
|
||||||
|
sh "${SSH_CMD} 'docker start ${CONTAINER_PREFIX}-nginx'"
|
||||||
|
sh 'sleep 2'
|
||||||
|
}
|
||||||
|
echo "Nginx 容器已存在且运行中"
|
||||||
|
}
|
||||||
|
// docker cp nginx.conf 进容器(覆盖更新配置模板)
|
||||||
|
sh "${SSH_CMD} 'docker cp ${REMOTE_DIR}/nginx.conf ${CONTAINER_PREFIX}-nginx:/etc/nginx/nginx.conf'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('健康检查') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def maxRetries = 3
|
||||||
|
def retryCount = 0
|
||||||
|
def healthy = false
|
||||||
|
|
||||||
|
while (retryCount < maxRetries && !healthy) {
|
||||||
|
try {
|
||||||
|
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}'"
|
||||||
|
healthy = true
|
||||||
|
} catch (Exception e) {
|
||||||
|
retryCount++
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
echo "健康检查失败,5秒后重试 (${retryCount}/${maxRetries})"
|
||||||
|
sleep 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!healthy) {
|
||||||
|
error "健康检查失败,部署中止"
|
||||||
|
}
|
||||||
|
echo "✅ ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} 健康检查通过"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('切换流量') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx sed -i \"s/proxy_pass http:\\/\\/\\(blue\\|green\\):8080;/proxy_pass http:\\/\\/${env.DEPLOY_TARGET}:8080;/\" /etc/nginx/nginx.conf'"
|
||||||
|
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload'"
|
||||||
|
echo "✅ 流量已切换到 ${env.DEPLOY_TARGET}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('删除旧版本') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def otherExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}'", returnStdout: true).trim()
|
||||||
|
if (otherExists) {
|
||||||
|
sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}'"
|
||||||
|
echo "旧版本 ${env.OTHER_TARGET} 已删除"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('清理') {
|
||||||
|
steps {
|
||||||
|
sh "${SSH_CMD} 'rm -f ${REMOTE_DIR}/${IMAGE_TAR}'"
|
||||||
|
sh "rm -f ${IMAGE_TAR}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
echo "✅ 蓝绿部署成功!当前运行: ${env.DEPLOY_TARGET}"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo '❌ 部署失败,请检查日志'
|
||||||
|
sh "rm -f ${IMAGE_TAR} || true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,4 +19,10 @@ public @interface FuncPermission {
|
|||||||
* 功能权限编码,对应 bg_func_permission.func_code
|
* 功能权限编码,对应 bg_func_permission.func_code
|
||||||
*/
|
*/
|
||||||
String value();
|
String value();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务资源标识表达式,SpEL,从方法参数中取值
|
||||||
|
* <p>如 #jobId、#dto.jobId,为空表示不做当日幂等,每次调用均扣减</p>
|
||||||
|
*/
|
||||||
|
String key() default "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,25 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
import org.aspectj.lang.annotation.Around;
|
import org.aspectj.lang.annotation.Around;
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
import org.jiayunet.annotation.FuncPermission;
|
import org.jiayunet.annotation.FuncPermission;
|
||||||
import org.jiayunet.service.FuncPermissionService;
|
import org.jiayunet.service.FuncPermissionService;
|
||||||
import org.jiayunet.tool.UserSecurityTool;
|
import org.jiayunet.tool.UserSecurityTool;
|
||||||
|
import org.springframework.aop.support.AopUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.expression.MethodBasedEvaluationContext;
|
||||||
|
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||||
|
import org.springframework.core.ParameterNameDiscoverer;
|
||||||
|
import org.springframework.expression.EvaluationContext;
|
||||||
|
import org.springframework.expression.ExpressionParser;
|
||||||
|
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 功能权限校验切面
|
* 功能权限校验切面
|
||||||
* <p>拦截 @FuncPermission 注解,校验权限并扣减库存后放行,业务异常时自动回退</p>
|
* <p>拦截 @FuncPermission 注解,当日已解锁的资源直接放行,否则校验权限并扣减库存后放行,业务异常时自动回退</p>
|
||||||
*
|
*
|
||||||
* @author zk
|
* @author zk
|
||||||
*/
|
*/
|
||||||
@@ -21,19 +31,36 @@ import org.springframework.stereotype.Component;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class FuncPermissionAspect {
|
public class FuncPermissionAspect {
|
||||||
|
|
||||||
|
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
|
||||||
|
|
||||||
|
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private FuncPermissionService funcPermissionService;
|
private FuncPermissionService funcPermissionService;
|
||||||
|
|
||||||
@Around("@annotation(funcPermission)")
|
@Around("@annotation(funcPermission)")
|
||||||
public Object check(ProceedingJoinPoint joinPoint, FuncPermission funcPermission) throws Throwable {
|
public Object check(ProceedingJoinPoint joinPoint, FuncPermission funcPermission) throws Throwable {
|
||||||
|
|
||||||
Long userId = UserSecurityTool.getUserId();
|
Long userId = 0L;
|
||||||
String funcCode = funcPermission.value();
|
try {
|
||||||
|
userId = UserSecurityTool.getUserId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 接口允许不登录,未登录用户ID为0
|
||||||
|
}
|
||||||
|
|
||||||
log.info("功能权限校验 userId:{} funcCode:{}", userId, funcCode);
|
String funcCode = funcPermission.value();
|
||||||
|
String bizKey = resolveBizKey(joinPoint, funcPermission.key());
|
||||||
|
|
||||||
|
log.info("功能权限校验 userId:{} funcCode:{} bizKey:{}", userId, funcCode, bizKey);
|
||||||
|
|
||||||
|
// 当日已解锁该资源,直接放行不再扣减
|
||||||
|
if (!bizKey.isEmpty() && funcPermissionService.existsTodayUnlock(userId, funcCode, bizKey)) {
|
||||||
|
log.info("当日已解锁,跳过扣减 userId:{} funcCode:{} bizKey:{}", userId, funcCode, bizKey);
|
||||||
|
return joinPoint.proceed();
|
||||||
|
}
|
||||||
|
|
||||||
// 校验权限 + 扣减库存,返回使用记录ID
|
// 校验权限 + 扣减库存,返回使用记录ID
|
||||||
Long logId = funcPermissionService.checkAndDeduct(userId, funcCode);
|
Long logId = funcPermissionService.checkAndDeduct(userId, funcCode, bizKey);
|
||||||
|
|
||||||
// 放行,业务异常时回退使用记录和库存
|
// 放行,业务异常时回退使用记录和库存
|
||||||
try {
|
try {
|
||||||
@@ -44,4 +71,27 @@ public class FuncPermissionAspect {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析业务资源标识
|
||||||
|
* <p>注解 key 为 SpEL 表达式,从方法参数取值,如 #jobId、#dto.jobId</p>
|
||||||
|
*
|
||||||
|
* @param joinPoint 连接点
|
||||||
|
* @param keyExpression SpEL 表达式
|
||||||
|
* @return 业务资源标识实际值,表达式为空或求值为 null 时返回空串
|
||||||
|
*/
|
||||||
|
private String resolveBizKey(ProceedingJoinPoint joinPoint, String keyExpression) {
|
||||||
|
if (keyExpression == null || keyExpression.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||||
|
Method method = AopUtils.getMostSpecificMethod(signature.getMethod(), joinPoint.getTarget().getClass());
|
||||||
|
|
||||||
|
EvaluationContext context = new MethodBasedEvaluationContext(
|
||||||
|
joinPoint.getTarget(), method, joinPoint.getArgs(), PARAMETER_NAME_DISCOVERER);
|
||||||
|
|
||||||
|
Object value = EXPRESSION_PARSER.parseExpression(keyExpression).getValue(context);
|
||||||
|
return value == null ? "" : value.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.jiayunet.controller;
|
||||||
|
|
||||||
|
import org.jiayunet.service.CompanyService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司接口
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/company")
|
||||||
|
public class CompanyController {
|
||||||
|
@Autowired
|
||||||
|
private CompanyService companyService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询公司类型列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/types")
|
||||||
|
public List<String> listCompanyTypes() {
|
||||||
|
return companyService.listCompanyTypes();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,28 @@
|
|||||||
package org.jiayunet.controller;
|
package org.jiayunet.controller;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jiayunet.annotation.FuncPermission;
|
||||||
import org.jiayunet.pojo.PageResult;
|
import org.jiayunet.pojo.PageResult;
|
||||||
import org.jiayunet.pojo.dto.job.JobApplicationDto;
|
import org.jiayunet.pojo.dto.job.JobApplicationDto;
|
||||||
import org.jiayunet.pojo.dto.job.JobDetailDto;
|
import org.jiayunet.pojo.dto.job.JobDetailDto;
|
||||||
import org.jiayunet.pojo.dto.job.JobDto;
|
import org.jiayunet.pojo.dto.job.JobDto;
|
||||||
import org.jiayunet.pojo.dto.job.JobAgentRecommendDto;
|
import org.jiayunet.pojo.dto.job.JobAgentRecommendDto;
|
||||||
import org.jiayunet.pojo.param.job.*;
|
import org.jiayunet.pojo.param.job.*;
|
||||||
|
import org.jiayunet.pojo.vo.HotCityVo;
|
||||||
|
import org.jiayunet.pojo.vo.HotIndustryVo;
|
||||||
|
import org.jiayunet.pojo.vo.HotJobCategoryVo;
|
||||||
import org.jiayunet.pojo.vo.JobApplyCountVo;
|
import org.jiayunet.pojo.vo.JobApplyCountVo;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.jiayunet.pojo.vo.JobFavoriteCountVo;
|
import org.jiayunet.pojo.vo.JobFavoriteCountVo;
|
||||||
import org.jiayunet.service.JobService;
|
import org.jiayunet.service.JobService;
|
||||||
import org.jiayunet.tool.UserSecurityTool;
|
import org.jiayunet.tool.UserSecurityTool;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 岗位接口
|
* 岗位接口
|
||||||
*
|
*
|
||||||
@@ -32,6 +40,7 @@ public class JobController {
|
|||||||
* <p>支持按地区/岗位类型/行业/工作类型筛选,自动排除不感兴趣的岗位/公司/地区/行业,返回匹配度和收藏状态</p>
|
* <p>支持按地区/岗位类型/行业/工作类型筛选,自动排除不感兴趣的岗位/公司/地区/行业,返回匹配度和收藏状态</p>
|
||||||
*/
|
*/
|
||||||
@PostMapping("/list")
|
@PostMapping("/list")
|
||||||
|
@FuncPermission(value = "job_list", key = "#param.pageNum")
|
||||||
public PageResult<JobDto> listJobs(@Validated @RequestBody JobQueryParam param) {
|
public PageResult<JobDto> listJobs(@Validated @RequestBody JobQueryParam param) {
|
||||||
|
|
||||||
Long userId = 0L;
|
Long userId = 0L;
|
||||||
@@ -45,11 +54,108 @@ public class JobController {
|
|||||||
return jobService.listJobs(param, userId);
|
return jobService.listJobs(param, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位列表查询(按匹配度排序)
|
||||||
|
* <p>入参与 /job/list 一致。匹配度是内存计算的,无法在 SQL 层排序,因此先取候选池全量算分排序
|
||||||
|
* 并整体缓存,再在本方法内对缓存结果切片分页</p>
|
||||||
|
* <p>排序仅在候选池范围内有效,候选池之外的岗位不参与排序</p>
|
||||||
|
* <p>未登录用户没有简历和求职意向,匹配度几乎全部相同,排序无意义,直接退回 /job/list 的最新排序</p>
|
||||||
|
*/
|
||||||
|
@PostMapping("/list/match")
|
||||||
|
@FuncPermission(value = "job_list", key = "#param.pageNum")
|
||||||
|
public PageResult<JobDto> listJobsByMatch(@Validated @RequestBody JobQueryParam param) {
|
||||||
|
|
||||||
|
Long userId = 0L;
|
||||||
|
try {
|
||||||
|
userId = UserSecurityTool.getUserId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 接口允许不登录,不处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未登录:匹配度无区分度,走原列表逻辑,省掉候选池全量算分
|
||||||
|
if (userId == 0L) {
|
||||||
|
return jobService.listJobs(param, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer pageNum = param.getPageNum();
|
||||||
|
Integer pageSize = param.getPageSize();
|
||||||
|
|
||||||
|
// 归一化候选池查询条件:pageNum/pageSize 参与缓存 key,必须固定,否则每页各自成为独立缓存条目
|
||||||
|
JobQueryParam poolParam = new JobQueryParam();
|
||||||
|
BeanUtils.copyProperties(param, poolParam);
|
||||||
|
poolParam.setPageNum(1);
|
||||||
|
poolParam.setPageSize(JobService.CANDIDATE_POOL_SIZE);
|
||||||
|
|
||||||
|
List<JobDto> sortedList = jobService.listJobsSortedByMatch(poolParam, userId);
|
||||||
|
|
||||||
|
// 手动分页:缓存中的列表为共享对象,切片后另建集合返回,避免污染缓存
|
||||||
|
long total = sortedList.size();
|
||||||
|
int fromIndex = (pageNum - 1) * pageSize;
|
||||||
|
if (fromIndex >= sortedList.size()) {
|
||||||
|
return new PageResult<>(pageNum.longValue(), pageSize.longValue(), total, Collections.emptyList());
|
||||||
|
}
|
||||||
|
int toIndex = Math.min(fromIndex + pageSize, sortedList.size());
|
||||||
|
List<JobDto> pageList = new ArrayList<>(sortedList.subList(fromIndex, toIndex));
|
||||||
|
|
||||||
|
return new PageResult<>(pageNum.longValue(), pageSize.longValue(), total, pageList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位列表总数查询
|
||||||
|
* <p>筛选条件与 /job/list 一致,仅返回符合条件的岗位总数,接口允许不登录访问</p>
|
||||||
|
*/
|
||||||
|
@PostMapping("/list/count")
|
||||||
|
public Long countJobs(@Validated @RequestBody JobQueryParam param) {
|
||||||
|
|
||||||
|
Long userId = 0L;
|
||||||
|
try {
|
||||||
|
userId = UserSecurityTool.getUserId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 接口允许不登录,不处理
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobService.countJobs(param, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位热门行业
|
||||||
|
* <p>按上架岗位数量倒序返回二级行业,接口允许不登录访问</p>
|
||||||
|
*
|
||||||
|
* @param limit 返回条数,默认10
|
||||||
|
*/
|
||||||
|
@GetMapping("/industry/hot")
|
||||||
|
public List<HotIndustryVo> listHotIndustries(@RequestParam(required = false, defaultValue = "10") Integer limit) {
|
||||||
|
return jobService.listHotIndustries(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热门岗位类型
|
||||||
|
* <p>按上架岗位数量倒序返回二级岗位类型,接口允许不登录访问</p>
|
||||||
|
*
|
||||||
|
* @param limit 返回条数,默认10
|
||||||
|
*/
|
||||||
|
@GetMapping("/category/hot")
|
||||||
|
public List<HotJobCategoryVo> listHotJobCategories(@RequestParam(required = false, defaultValue = "10") Integer limit) {
|
||||||
|
return jobService.listHotJobCategories(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热门城市
|
||||||
|
* <p>按上架岗位数量倒序返回城市,接口允许不登录访问</p>
|
||||||
|
*
|
||||||
|
* @param limit 返回条数,默认10
|
||||||
|
*/
|
||||||
|
@GetMapping("/city/hot")
|
||||||
|
public List<HotCityVo> listHotCities(@RequestParam(required = false, defaultValue = "10") Integer limit) {
|
||||||
|
return jobService.listHotCities(limit);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 岗位详情
|
* 岗位详情
|
||||||
* <p>返回岗位完整信息、公司信息、匹配度、收藏状态</p>
|
* <p>返回岗位完整信息、公司信息、匹配度、收藏状态</p>
|
||||||
*/
|
*/
|
||||||
@GetMapping("/detail")
|
@GetMapping("/detail")
|
||||||
|
@FuncPermission(value = "job_detail", key = "#jobId")
|
||||||
public JobDetailDto getJobDetail(@RequestParam Long jobId) {
|
public JobDetailDto getJobDetail(@RequestParam Long jobId) {
|
||||||
|
|
||||||
Long userId = 0L;
|
Long userId = 0L;
|
||||||
@@ -170,6 +276,15 @@ public class JobController {
|
|||||||
return jobService.recommendJobs(param, userId);
|
return jobService.recommendJobs(param, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有效岗位总数统计
|
||||||
|
* <p>统计当前上架(status=0)的岗位数量,接口允许不登录访问</p>
|
||||||
|
*/
|
||||||
|
@GetMapping("/count")
|
||||||
|
public Long countValidJobs() {
|
||||||
|
return jobService.countValidJobs();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据岗位来源地址查询岗位信息
|
* 根据岗位来源地址查询岗位信息
|
||||||
* <p>通过岗位的原始链接(如招聘网站URL)查询对应的岗位信息</p>
|
* <p>通过岗位的原始链接(如招聘网站URL)查询对应的岗位信息</p>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.jiayunet.controller;
|
|||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import org.jiayunet.pojo.dto.SmsLoginDto;
|
import org.jiayunet.pojo.dto.SmsLoginDto;
|
||||||
|
import org.jiayunet.pojo.dto.SuperAdminLoginDto;
|
||||||
import org.jiayunet.pojo.vo.LoginVo;
|
import org.jiayunet.pojo.vo.LoginVo;
|
||||||
import org.jiayunet.service.LoginService;
|
import org.jiayunet.service.LoginService;
|
||||||
import org.jiayunet.tool.UserSecurityTool;
|
import org.jiayunet.tool.UserSecurityTool;
|
||||||
@@ -37,6 +38,17 @@ public class LoginController {
|
|||||||
return loginService.smsLogin(dto.getMobileNumber(), dto.getCode(), dto.getInviteCode(), request, response);
|
return loginService.smsLogin(dto.getMobileNumber(), dto.getCode(), dto.getInviteCode(), request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超管密码登录
|
||||||
|
* <p>用手机号+超管密码直接登录,用户不存在则自动注册并赠送正式会员</p>
|
||||||
|
*/
|
||||||
|
@PostMapping("/login/superAdminLogin")
|
||||||
|
public LoginVo superAdminLogin(@Validated @RequestBody SuperAdminLoginDto dto,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
return loginService.superAdminLogin(dto.getMobileNumber(), dto.getPassword(), request, response);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/logout")
|
@PostMapping("/logout")
|
||||||
public boolean logout(HttpServletRequest request, HttpServletResponse response) {
|
public boolean logout(HttpServletRequest request, HttpServletResponse response) {
|
||||||
loginService.logout(request, response);
|
loginService.logout(request, response);
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package org.jiayunet.controller;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jiayunet.remote.jsxq.JsxqClient;
|
||||||
|
import org.jiayunet.remote.jsxq.pojo.JsxqPage;
|
||||||
|
import org.jiayunet.remote.jsxq.pojo.dto.ChatEvaluateDto;
|
||||||
|
import org.jiayunet.remote.jsxq.pojo.dto.MentorFullInfoDto;
|
||||||
|
import org.jiayunet.remote.jsxq.pojo.dto.MentorItemDto;
|
||||||
|
import org.jiayunet.remote.jsxq.pojo.dto.TeacherTalkDto;
|
||||||
|
import org.jiayunet.remote.jsxq.pojo.param.MentorPageParam;
|
||||||
|
import org.jiayunet.remote.jsxq.pojo.param.QrCodeParam;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导师接口(对接简史星球开放平台,公开免鉴权)
|
||||||
|
* <p>依赖:JsxqClient(远程调用简史星球平台)</p>
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/public/mentor")
|
||||||
|
@Validated
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MentorController {
|
||||||
|
|
||||||
|
private final JsxqClient jsxqClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询导师列表
|
||||||
|
*/
|
||||||
|
@PostMapping("/page")
|
||||||
|
public JsxqPage<MentorItemDto> pageSelectMentor(@RequestBody MentorPageParam param) {
|
||||||
|
return jsxqClient.pageSelectMentor(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询导师完整信息
|
||||||
|
*/
|
||||||
|
@GetMapping("/info")
|
||||||
|
public MentorFullInfoDto selectMentorFullInfo(@RequestParam Long mentorId) {
|
||||||
|
return jsxqClient.selectMentorFullInfo(mentorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询老师话题列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/talk")
|
||||||
|
public List<TeacherTalkDto> selectTalk(@RequestParam Long teacherId) {
|
||||||
|
return jsxqClient.selectTalk(teacherId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询老师评价列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/evaluate")
|
||||||
|
public List<ChatEvaluateDto> selectChatEvaluate(@RequestParam Long teacherId) {
|
||||||
|
return jsxqClient.selectChatEvaluate(teacherId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取二维码
|
||||||
|
*/
|
||||||
|
@PostMapping("/getQrCode")
|
||||||
|
public Map<String, Object> getQrCode(@Validated @RequestBody QrCodeParam param) {
|
||||||
|
return jsxqClient.getQrCode(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package org.jiayunet.controller;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jiayunet.annotation.FuncPermission;
|
||||||
|
import org.jiayunet.pojo.PageResult;
|
||||||
|
import org.jiayunet.pojo.dto.recruitAnnouncement.RecruitAnnouncementDetailDto;
|
||||||
|
import org.jiayunet.pojo.dto.recruitAnnouncement.RecruitAnnouncementDto;
|
||||||
|
import org.jiayunet.pojo.dto.recruitAnnouncement.RecruitAnnouncementRecommendDto;
|
||||||
|
import org.jiayunet.pojo.dto.recruitAnnouncement.RecruitAnnouncementStatisticsDto;
|
||||||
|
import org.jiayunet.pojo.param.recruitAnnouncement.RecruitAnnouncementQueryParam;
|
||||||
|
import org.jiayunet.pojo.param.recruitAnnouncement.RecruitAnnouncementRecommendParam;
|
||||||
|
import org.jiayunet.service.RecruitAnnouncementService;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 招聘公告接口
|
||||||
|
* <p>提供公告筛选项(批次/岗位大类/热招城市/学历要求/行业/标签/招聘届数)的去重查询</p>
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/public/recruitAnnouncement")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Validated
|
||||||
|
public class RecruitAnnouncementController {
|
||||||
|
|
||||||
|
private RecruitAnnouncementService recruitAnnouncementService;
|
||||||
|
|
||||||
|
/** 查询公告最后更新时间 */
|
||||||
|
@GetMapping("/lastUpdateTime")
|
||||||
|
public Instant getLastUpdateTime() {
|
||||||
|
return recruitAnnouncementService.getLastUpdateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统计公告数量(今日新增/本月新增/累计) */
|
||||||
|
@GetMapping("/statistics")
|
||||||
|
public RecruitAnnouncementStatisticsDto getStatistics() {
|
||||||
|
return recruitAnnouncementService.getStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页查询公告列表 */
|
||||||
|
@PostMapping("/page")
|
||||||
|
@FuncPermission(value = "ann_list", key = "#param.pageNum")
|
||||||
|
public PageResult<RecruitAnnouncementDto> pageAnnouncement(@Validated @RequestBody RecruitAnnouncementQueryParam param) {
|
||||||
|
return recruitAnnouncementService.pageAnnouncement(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页查询推荐公告(同行业或同岗位大类) */
|
||||||
|
@PostMapping("/recommend")
|
||||||
|
public PageResult<RecruitAnnouncementRecommendDto> pageRecommend(@Validated @RequestBody RecruitAnnouncementRecommendParam param) {
|
||||||
|
return recruitAnnouncementService.pageRecommend(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询公告详情 */
|
||||||
|
@GetMapping("/detail")
|
||||||
|
@FuncPermission(value = "ann_detail", key = "#id")
|
||||||
|
public RecruitAnnouncementDetailDto getDetail(@RequestParam Long id) {
|
||||||
|
return recruitAnnouncementService.getDetail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据公告id查询公告地址 */
|
||||||
|
@GetMapping("/announcementUrl")
|
||||||
|
@FuncPermission(value = "ann_url", key = "#id")
|
||||||
|
public String getAnnouncementUrl(@RequestParam Long id) {
|
||||||
|
return recruitAnnouncementService.getAnnouncementUrl(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据公告id查询岗位投递地址 */
|
||||||
|
@GetMapping("/applyUrl")
|
||||||
|
@FuncPermission(value = "ann_url", key = "#id")
|
||||||
|
public String getApplyUrl(@RequestParam Long id) {
|
||||||
|
return recruitAnnouncementService.getApplyUrl(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据公告id查询投递邮箱 */
|
||||||
|
@GetMapping("/applyEmail")
|
||||||
|
@FuncPermission(value = "ann_url", key = "#id")
|
||||||
|
public String getApplyEmail(@RequestParam Long id) {
|
||||||
|
return recruitAnnouncementService.getApplyEmail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询公告批次列表(去重) */
|
||||||
|
@GetMapping("/batch")
|
||||||
|
public List<String> listBatchName() {
|
||||||
|
return recruitAnnouncementService.listBatchName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询公告岗位大类列表(去重) */
|
||||||
|
@GetMapping("/category")
|
||||||
|
public List<String> listCategoryName() {
|
||||||
|
return recruitAnnouncementService.listCategoryName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询公告热招城市列表(去重) */
|
||||||
|
@GetMapping("/city")
|
||||||
|
public List<String> listCityName() {
|
||||||
|
return recruitAnnouncementService.listCityName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询公告学历要求列表(去重) */
|
||||||
|
@GetMapping("/education")
|
||||||
|
public List<String> listEducationName() {
|
||||||
|
return recruitAnnouncementService.listEducationName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询公告行业列表(去重) */
|
||||||
|
@GetMapping("/industry")
|
||||||
|
public List<String> listIndustryName() {
|
||||||
|
return recruitAnnouncementService.listIndustryName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询公告标签列表(去重) */
|
||||||
|
@GetMapping("/tag")
|
||||||
|
public List<String> listTagName() {
|
||||||
|
return recruitAnnouncementService.listTagName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询公告招聘届数列表(去重) */
|
||||||
|
@GetMapping("/year")
|
||||||
|
public List<Integer> listRecruitYear() {
|
||||||
|
return recruitAnnouncementService.listRecruitYear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户个人资料接口
|
* 用户个人资料接口
|
||||||
* 提供主表及5张子表(教育/工作/实习/项目/竞赛)的查询与保存功能
|
* 提供主表及6张子表(教育/工作/实习/项目/竞赛/社团组织)的查询与保存功能
|
||||||
*
|
*
|
||||||
* @author zk
|
* @author zk
|
||||||
*/
|
*/
|
||||||
@@ -54,13 +54,21 @@ public class UserProfileController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据简历ID同步更新个人资料(主表+5子表全量覆盖,触发一次异步AI分析)
|
* 根据简历ID同步更新个人资料(主表+6子表全量覆盖,触发一次异步AI分析)
|
||||||
*/
|
*/
|
||||||
@PostMapping("/syncFromResume")
|
@PostMapping("/syncFromResume")
|
||||||
public void syncFromResume(@RequestParam Long resumeId) {
|
public void syncFromResume(@RequestParam Long resumeId) {
|
||||||
userProfileService.syncProfileFromResume(resumeId);
|
userProfileService.syncProfileFromResume(resumeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据当前用户个人资料创建一份新简历(名"个人资料简历"),返回新简历ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/createResume")
|
||||||
|
public Long createResumeFromProfile() {
|
||||||
|
return userProfileService.createResumeFromProfile();
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 教育经历 ====================
|
// ==================== 教育经历 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -195,4 +203,31 @@ public class UserProfileController {
|
|||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
userProfileService.saveCompetitionList(list);
|
userProfileService.saveCompetitionList(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 社团组织经历 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户的社团组织经历列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/organization")
|
||||||
|
public List<UserProfileOrganizationDto> listOrganization() {
|
||||||
|
return userProfileService.listOrganization().stream().map(po -> {
|
||||||
|
UserProfileOrganizationDto dto = new UserProfileOrganizationDto();
|
||||||
|
BeanUtils.copyProperties(po, dto);
|
||||||
|
return dto;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存当前用户的社团组织经历列表(全量替换)
|
||||||
|
*/
|
||||||
|
@PostMapping("/organization")
|
||||||
|
public void saveOrganization(@Validated @RequestBody List<@Valid UserProfileOrganizationParam> params) {
|
||||||
|
List<UserProfileOrganization> list = params.stream().map(p -> {
|
||||||
|
UserProfileOrganization po = new UserProfileOrganization();
|
||||||
|
BeanUtils.copyProperties(p, po);
|
||||||
|
return po;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
userProfileService.saveOrganizationList(list);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户简历接口
|
* 用户简历接口
|
||||||
* <p>提供简历列表、主表及5张子表(教育/工作/实习/项目/竞赛)的查询与保存、简历删除功能</p>
|
* <p>提供简历列表、主表及6张子表(教育/工作/实习/项目/竞赛/社团组织)的查询与保存、简历删除功能</p>
|
||||||
* <p>所有保存接口支持自动创建:前端不传resumeId时自动创建新简历并返回resumeId</p>
|
* <p>所有保存接口支持自动创建:前端不传resumeId时自动创建新简历并返回resumeId</p>
|
||||||
*
|
*
|
||||||
* @author zk
|
* @author zk
|
||||||
@@ -64,6 +64,12 @@ public class UserResumeController {
|
|||||||
return userResumeService.saveResume(po, param.getResumeId());
|
return userResumeService.saveResume(po, param.getResumeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 修改简历名称和目标岗位 */
|
||||||
|
@PostMapping("/updateInfo")
|
||||||
|
public void updateResumeInfo(@Validated @RequestBody ResumeInfoUpdateParam param) {
|
||||||
|
userResumeService.updateResumeInfo(param.getResumeId(), param.getResumeName(), param.getTargetPosition());
|
||||||
|
}
|
||||||
|
|
||||||
/** 删除简历(主表 + 全部子表) */
|
/** 删除简历(主表 + 全部子表) */
|
||||||
@PostMapping("/delete")
|
@PostMapping("/delete")
|
||||||
public void deleteResume(@RequestParam Long resumeId) {
|
public void deleteResume(@RequestParam Long resumeId) {
|
||||||
@@ -300,4 +306,49 @@ public class UserResumeController {
|
|||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
return userResumeService.saveCompetitionList(list, param.getResumeId());
|
return userResumeService.saveCompetitionList(list, param.getResumeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 社团组织经历 ====================
|
||||||
|
|
||||||
|
/** 查询简历的社团组织经历列表 */
|
||||||
|
@GetMapping("/organization")
|
||||||
|
public List<ResumeOrganizationDto> listOrganization(@RequestParam Long resumeId) {
|
||||||
|
return userResumeService.listOrganization(resumeId).stream().map(po -> {
|
||||||
|
ResumeOrganizationDto dto = new ResumeOrganizationDto();
|
||||||
|
BeanUtils.copyProperties(po, dto);
|
||||||
|
return dto;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加单条社团组织经历,返回新记录id */
|
||||||
|
@PostMapping("/organization/add")
|
||||||
|
public Long addOrganization(@RequestParam Long resumeId, @Validated @RequestBody ResumeOrganizationParam param) {
|
||||||
|
UserResumeOrganization po = new UserResumeOrganization();
|
||||||
|
BeanUtils.copyProperties(param, po);
|
||||||
|
return userResumeService.addOrganization(po, resumeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据id编辑单条社团组织经历 */
|
||||||
|
@PostMapping("/organization/update")
|
||||||
|
public void updateOrganization(@Validated @RequestBody ResumeOrganizationUpdateParam param) {
|
||||||
|
UserResumeOrganization po = new UserResumeOrganization();
|
||||||
|
BeanUtils.copyProperties(param, po);
|
||||||
|
userResumeService.updateOrganization(po);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据id删除单条社团组织经历 */
|
||||||
|
@PostMapping("/organization/delete")
|
||||||
|
public void deleteOrganization(@RequestParam Long id) {
|
||||||
|
userResumeService.deleteOrganization(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存简历的社团组织经历列表(先删后插),返回resumeId */
|
||||||
|
@PostMapping("/organization")
|
||||||
|
public Long saveOrganization(@Validated @RequestBody ResumeSubTableParam<ResumeOrganizationParam> param) {
|
||||||
|
List<UserResumeOrganization> list = param.getItems().stream().map(p -> {
|
||||||
|
UserResumeOrganization po = new UserResumeOrganization();
|
||||||
|
BeanUtils.copyProperties(p, po);
|
||||||
|
return po;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
return userResumeService.saveOrganizationList(list, param.getResumeId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.jiayunet.pojo.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超管密码登录入参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SuperAdminLoginDto {
|
||||||
|
|
||||||
|
@NotBlank(message = "手机号不能为空")
|
||||||
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||||
|
private String mobileNumber;
|
||||||
|
|
||||||
|
@NotBlank(message = "超管密码不能为空")
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@@ -22,6 +22,9 @@ public class JobDto {
|
|||||||
/** 薪资描述 */
|
/** 薪资描述 */
|
||||||
private String salary;
|
private String salary;
|
||||||
|
|
||||||
|
/** 学历要求 0=不限 1=大专 2=本科 3=硕士 4=博士 */
|
||||||
|
private Integer education;
|
||||||
|
|
||||||
/** 公司名称 */
|
/** 公司名称 */
|
||||||
private String companyName;
|
private String companyName;
|
||||||
|
|
||||||
@@ -34,6 +37,9 @@ public class JobDto {
|
|||||||
/** 公司Logo */
|
/** 公司Logo */
|
||||||
private String companyLogoUrl;
|
private String companyLogoUrl;
|
||||||
|
|
||||||
|
/** 公司标签 */
|
||||||
|
private List<String> companyTags;
|
||||||
|
|
||||||
/** 地区名称 */
|
/** 地区名称 */
|
||||||
private String regionName;
|
private String regionName;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,16 @@ public class JobIntentionDto {
|
|||||||
*/
|
*/
|
||||||
private List<Long> industryIds;
|
private List<Long> industryIds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI推荐岗位类型ID列表
|
||||||
|
*/
|
||||||
|
private List<Long> aiCategoryIds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI推荐行业ID列表
|
||||||
|
*/
|
||||||
|
private List<Long> aiIndustryIds;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 就业类型:0=全职,1=实习
|
* 就业类型:0=全职,1=实习
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,4 +22,7 @@ public class JobMatchScoreDto {
|
|||||||
|
|
||||||
/** 经历得分(0-100,百分制) */
|
/** 经历得分(0-100,百分制) */
|
||||||
private Integer experienceScore;
|
private Integer experienceScore;
|
||||||
|
|
||||||
|
/** 方向适配得分(0-100,百分制,基于AI推荐岗位方向+行业方向与岗位的匹配) */
|
||||||
|
private Integer fitScore;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ public class MemberStatusDto {
|
|||||||
/** 是否是会员 */
|
/** 是否是会员 */
|
||||||
private Boolean isMember;
|
private Boolean isMember;
|
||||||
|
|
||||||
|
/** 会员类型 1=正式 2=试用 */
|
||||||
|
private Integer memberType;
|
||||||
|
|
||||||
/** 到期时间 */
|
/** 到期时间 */
|
||||||
private Instant expireTime;
|
private Instant expireTime;
|
||||||
|
|
||||||
|
|||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
package org.jiayunet.pojo.dto.recruitAnnouncement;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 招聘公告详情返回
|
||||||
|
* <p>公告主表字段(不含公告地址/投递地址/投递邮箱)+ 公司logo与类型 + 全部多值维度</p>
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RecruitAnnouncementDetailDto {
|
||||||
|
|
||||||
|
/** 公告ID */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 公司ID */
|
||||||
|
private Long companyId;
|
||||||
|
|
||||||
|
/** 公司名称 */
|
||||||
|
private String companyName;
|
||||||
|
|
||||||
|
/** 公司logo */
|
||||||
|
private String logoUrl;
|
||||||
|
|
||||||
|
/** 公司类型 */
|
||||||
|
private String companyType;
|
||||||
|
|
||||||
|
/** 公告标题 */
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/** 公司简介 */
|
||||||
|
private String companyIntro;
|
||||||
|
|
||||||
|
/** 面向对象,如2027届本硕博 */
|
||||||
|
private String targetAudience;
|
||||||
|
|
||||||
|
/** 专业要求 */
|
||||||
|
private String majorRequire;
|
||||||
|
|
||||||
|
/** 招聘岗位 */
|
||||||
|
private String recruitPosition;
|
||||||
|
|
||||||
|
/** 备注 */
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
/** 是否笔试:有 / 无 / 未明确 */
|
||||||
|
private String writtenExam;
|
||||||
|
|
||||||
|
/** 投递开始时间 */
|
||||||
|
private Instant applyStartTime;
|
||||||
|
|
||||||
|
/** 投递结束时间 */
|
||||||
|
private Instant applyEndTime;
|
||||||
|
|
||||||
|
/** 投递截止描述 */
|
||||||
|
private String applyEndDesc;
|
||||||
|
|
||||||
|
/** 专属内推码 */
|
||||||
|
private String inviteCode;
|
||||||
|
|
||||||
|
/** 信息来源说明 */
|
||||||
|
private String source;
|
||||||
|
|
||||||
|
/** 公告发布/更新时间 */
|
||||||
|
private Instant publishTime;
|
||||||
|
|
||||||
|
/** 清洗状态:0-清洗中 1-清洗完成 */
|
||||||
|
private Integer cleanStatus;
|
||||||
|
|
||||||
|
/** 状态:0-失效 1-有效 */
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
private Instant createTime;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
private Instant updateTime;
|
||||||
|
|
||||||
|
/** 行业列表 */
|
||||||
|
private List<String> industryNames;
|
||||||
|
|
||||||
|
/** 岗位大类列表 */
|
||||||
|
private List<String> categoryNames;
|
||||||
|
|
||||||
|
/** 热招城市列表 */
|
||||||
|
private List<String> cityNames;
|
||||||
|
|
||||||
|
/** 学历要求列表 */
|
||||||
|
private List<String> educationNames;
|
||||||
|
|
||||||
|
/** 招聘届数列表 */
|
||||||
|
private List<Integer> recruitYears;
|
||||||
|
|
||||||
|
/** 批次列表 */
|
||||||
|
private List<String> batchNames;
|
||||||
|
|
||||||
|
/** 标签列表 */
|
||||||
|
private List<String> tagNames;
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
package org.jiayunet.pojo.dto.recruitAnnouncement;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 招聘公告列表返回
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RecruitAnnouncementDto {
|
||||||
|
|
||||||
|
/** 公告ID */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 公司ID */
|
||||||
|
private Long companyId;
|
||||||
|
|
||||||
|
/** 公司名称 */
|
||||||
|
private String companyName;
|
||||||
|
|
||||||
|
/** 公司logo */
|
||||||
|
private String logoUrl;
|
||||||
|
|
||||||
|
/** 公告标题 */
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/** 专业要求 */
|
||||||
|
private String majorRequire;
|
||||||
|
|
||||||
|
/** 投递结束时间 */
|
||||||
|
private Instant applyEndTime;
|
||||||
|
|
||||||
|
/** 发布时间 */
|
||||||
|
private Instant publishTime;
|
||||||
|
|
||||||
|
/** 投递截止描述 */
|
||||||
|
private String applyEndDesc;
|
||||||
|
|
||||||
|
/** 行业列表 */
|
||||||
|
private List<String> industryNames;
|
||||||
|
|
||||||
|
/** 岗位大类列表 */
|
||||||
|
private List<String> categoryNames;
|
||||||
|
|
||||||
|
/** 热招城市列表 */
|
||||||
|
private List<String> cityNames;
|
||||||
|
|
||||||
|
/** 学历要求列表 */
|
||||||
|
private List<String> educationNames;
|
||||||
|
|
||||||
|
/** 招聘届数列表 */
|
||||||
|
private List<Integer> recruitYears;
|
||||||
|
|
||||||
|
/** 批次列表 */
|
||||||
|
private List<String> batchNames;
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package org.jiayunet.pojo.dto.recruitAnnouncement;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 招聘公告推荐返回
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RecruitAnnouncementRecommendDto {
|
||||||
|
|
||||||
|
/** 公告ID */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 公司名称 */
|
||||||
|
private String companyName;
|
||||||
|
|
||||||
|
/** 公司logo */
|
||||||
|
private String logoUrl;
|
||||||
|
|
||||||
|
/** 岗位大类列表 */
|
||||||
|
private List<String> categoryNames;
|
||||||
|
|
||||||
|
/** 热招城市列表 */
|
||||||
|
private List<String> cityNames;
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package org.jiayunet.pojo.dto.recruitAnnouncement;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 招聘公告统计返回
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RecruitAnnouncementStatisticsDto {
|
||||||
|
|
||||||
|
/** 今日新增公告数 */
|
||||||
|
private Long dayCount;
|
||||||
|
|
||||||
|
/** 本月新增公告数 */
|
||||||
|
private Long monthCount;
|
||||||
|
|
||||||
|
/** 累计公告数 */
|
||||||
|
private Long totalCount;
|
||||||
|
}
|
||||||
@@ -52,4 +52,10 @@ public class ResumeDto {
|
|||||||
|
|
||||||
/** 个人概述 */
|
/** 个人概述 */
|
||||||
private String summary;
|
private String summary;
|
||||||
|
|
||||||
|
/** 兴趣爱好 */
|
||||||
|
private String hobbies;
|
||||||
|
|
||||||
|
/** 语言能力 */
|
||||||
|
private String languageSkills;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package org.jiayunet.pojo.dto.resume;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简历-社团组织经历返回
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ResumeOrganizationDto {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 社团/组织名称 */
|
||||||
|
private String organizationName;
|
||||||
|
|
||||||
|
/** 担任角色 */
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
/** 开始时间 */
|
||||||
|
private String startDate;
|
||||||
|
|
||||||
|
/** 结束时间 */
|
||||||
|
private String endDate;
|
||||||
|
|
||||||
|
/** 描述段落 */
|
||||||
|
private List<DescriptionParagraph> description;
|
||||||
|
}
|
||||||
@@ -11,6 +11,12 @@ import lombok.Data;
|
|||||||
public class InviteStatsDto {
|
public class InviteStatsDto {
|
||||||
/** 累计邀请人数 */
|
/** 累计邀请人数 */
|
||||||
private Long inviteCount;
|
private Long inviteCount;
|
||||||
|
/** 当前周期已邀请人数 */
|
||||||
|
private Long milestoneProgress;
|
||||||
/** 累计获得会员天数 */
|
/** 累计获得会员天数 */
|
||||||
private Integer rewardDays;
|
private Integer rewardDays;
|
||||||
|
/** 里程碑目标人数 */
|
||||||
|
private Integer milestoneTarget;
|
||||||
|
/** 里程碑奖励天数 */
|
||||||
|
private Integer milestoneDays;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,4 +46,10 @@ public class UserProfileDto {
|
|||||||
|
|
||||||
/** 证书标签列表 */
|
/** 证书标签列表 */
|
||||||
private List<String> certificates;
|
private List<String> certificates;
|
||||||
|
|
||||||
|
/** 兴趣爱好 */
|
||||||
|
private String hobbies;
|
||||||
|
|
||||||
|
/** 语言能力 */
|
||||||
|
private String languageSkills;
|
||||||
}
|
}
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package org.jiayunet.pojo.dto.userProfile;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 社团组织经历返回
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UserProfileOrganizationDto {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 社团/组织名称 */
|
||||||
|
private String organizationName;
|
||||||
|
|
||||||
|
/** 担任角色 */
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
/** 开始时间 */
|
||||||
|
private String startDate;
|
||||||
|
|
||||||
|
/** 结束时间 */
|
||||||
|
private String endDate;
|
||||||
|
|
||||||
|
/** 描述段落 */
|
||||||
|
private List<DescriptionParagraph> description;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import lombok.Data;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import org.jiayunet.pojo.PageParam;
|
import org.jiayunet.pojo.PageParam;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,6 +25,9 @@ public class JobQueryParam extends PageParam {
|
|||||||
/** 行业ID列表 */
|
/** 行业ID列表 */
|
||||||
private List<Long> industryIds;
|
private List<Long> industryIds;
|
||||||
|
|
||||||
|
/** 公司类型列表(上市企业、独角兽、国企等,可多选) */
|
||||||
|
private List<String> companyTypes;
|
||||||
|
|
||||||
/** 工作类型 0=全职 1=兼职 */
|
/** 工作类型 0=全职 1=兼职 */
|
||||||
private Integer employmentType;
|
private Integer employmentType;
|
||||||
|
|
||||||
@@ -38,4 +42,16 @@ public class JobQueryParam extends PageParam {
|
|||||||
|
|
||||||
/** 岗位状态过滤(0=有效 1=已下架 2=已过期,可多选,null或空=查所有) */
|
/** 岗位状态过滤(0=有效 1=已下架 2=已过期,可多选,null或空=查所有) */
|
||||||
private List<Integer> statusFilter;
|
private List<Integer> statusFilter;
|
||||||
|
|
||||||
|
/** 公司ID */
|
||||||
|
private Long companyId;
|
||||||
|
|
||||||
|
/** 学历要求列表 0=不限 1=大专 2=本科 3=硕士 4=博士(可多选,null或空=不限制) */
|
||||||
|
private List<Integer> educations;
|
||||||
|
|
||||||
|
/** 发布开始时间(对应 bg_job.expire_at,闭区间) */
|
||||||
|
private Instant publishStartTime;
|
||||||
|
|
||||||
|
/** 发布截止时间(对应 bg_job.expire_at,闭区间) */
|
||||||
|
private Instant publishEndTime;
|
||||||
}
|
}
|
||||||
|
|||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
package org.jiayunet.pojo.param.recruitAnnouncement;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.jiayunet.pojo.PageParam;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 招聘公告查询入参
|
||||||
|
* <p>各筛选条件之间为“且”关系,为空则不参与筛选</p>
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class RecruitAnnouncementQueryParam extends PageParam {
|
||||||
|
|
||||||
|
/** 发布时间起(不含) */
|
||||||
|
private Instant publishTimeStart;
|
||||||
|
|
||||||
|
/** 发布时间止(含) */
|
||||||
|
private Instant publishTimeEnd;
|
||||||
|
|
||||||
|
/** 投递截止时间起(不含) */
|
||||||
|
private Instant applyEndTimeStart;
|
||||||
|
|
||||||
|
/** 投递截止时间止(含) */
|
||||||
|
private Instant applyEndTimeEnd;
|
||||||
|
|
||||||
|
/** 公司类型列表 */
|
||||||
|
private List<String> companyTypes;
|
||||||
|
|
||||||
|
/** 学历要求列表 */
|
||||||
|
private List<String> educationNames;
|
||||||
|
|
||||||
|
/** 招聘届数列表 */
|
||||||
|
private List<Integer> recruitYears;
|
||||||
|
|
||||||
|
/** 批次列表 */
|
||||||
|
private List<String> batchNames;
|
||||||
|
|
||||||
|
/** 岗位大类列表 */
|
||||||
|
private List<String> categoryNames;
|
||||||
|
|
||||||
|
/** 热招城市列表 */
|
||||||
|
private List<String> cityNames;
|
||||||
|
|
||||||
|
/** 行业列表 */
|
||||||
|
private List<String> industryNames;
|
||||||
|
|
||||||
|
/** 标签列表 */
|
||||||
|
private List<String> tagNames;
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package org.jiayunet.pojo.param.recruitAnnouncement;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.jiayunet.pojo.PageParam;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 招聘公告推荐入参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class RecruitAnnouncementRecommendParam extends PageParam {
|
||||||
|
|
||||||
|
/** 公告ID */
|
||||||
|
@NotNull(message = "公告ID不能为空")
|
||||||
|
private Long announcementId;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package org.jiayunet.pojo.param.resume;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.Size;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,13 +15,15 @@ import java.util.List;
|
|||||||
public class ResumeCompetitionParam {
|
public class ResumeCompetitionParam {
|
||||||
|
|
||||||
/** 竞赛名称 */
|
/** 竞赛名称 */
|
||||||
@NotBlank(message = "竞赛名称不能为空")
|
@Size(max = 200, message = "竞赛名称长度不能超过200")
|
||||||
private String competitionName;
|
private String competitionName;
|
||||||
|
|
||||||
/** 获奖情况,如全国二等奖 */
|
/** 获奖情况,如全国二等奖 */
|
||||||
|
@Size(max = 200, message = "获奖情况长度不能超过200")
|
||||||
private String award;
|
private String award;
|
||||||
|
|
||||||
/** 获奖时间,格式:2023.07 */
|
/** 获奖时间,格式:2023.07 */
|
||||||
|
@Size(max = 10, message = "获奖时间长度不能超过10")
|
||||||
private String awardDate;
|
private String awardDate;
|
||||||
|
|
||||||
/** 描述段落 */
|
/** 描述段落 */
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.jiayunet.pojo.param.resume;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.Size;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,22 +15,27 @@ import java.util.List;
|
|||||||
public class ResumeEducationParam {
|
public class ResumeEducationParam {
|
||||||
|
|
||||||
/** 学校名称 */
|
/** 学校名称 */
|
||||||
@NotBlank(message = "学校名称不能为空")
|
@Size(max = 100, message = "学校名称长度不能超过100")
|
||||||
private String school;
|
private String school;
|
||||||
|
|
||||||
/** 专业 */
|
/** 专业 */
|
||||||
|
@Size(max = 100, message = "专业长度不能超过100")
|
||||||
private String major;
|
private String major;
|
||||||
|
|
||||||
/** 学历:大专/本科/硕士/博士 */
|
/** 学历:大专/本科/硕士/博士 */
|
||||||
|
@Size(max = 20, message = "学历长度不能超过20")
|
||||||
private String degree;
|
private String degree;
|
||||||
|
|
||||||
/** 学习形式:全日制/非全日制 */
|
/** 学习形式:全日制/非全日制 */
|
||||||
|
@Size(max = 20, message = "学习形式长度不能超过20")
|
||||||
private String studyType;
|
private String studyType;
|
||||||
|
|
||||||
/** 开始时间,格式:2023.09 */
|
/** 开始时间,格式:2023.09 */
|
||||||
|
@Size(max = 10, message = "开始时间长度不能超过10")
|
||||||
private String startDate;
|
private String startDate;
|
||||||
|
|
||||||
/** 结束时间,格式:2024.06 */
|
/** 结束时间,格式:2024.06 */
|
||||||
|
@Size(max = 10, message = "结束时间长度不能超过10")
|
||||||
private String endDate;
|
private String endDate;
|
||||||
|
|
||||||
/** 描述段落 */
|
/** 描述段落 */
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.jiayunet.pojo.param.resume;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简历名称、目标岗位编辑入参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ResumeInfoUpdateParam {
|
||||||
|
|
||||||
|
/** 简历ID */
|
||||||
|
@NotNull(message = "resumeId不能为空")
|
||||||
|
private Long resumeId;
|
||||||
|
|
||||||
|
/** 简历名称 */
|
||||||
|
@Size(max = 100, message = "简历名称长度不能超过100")
|
||||||
|
private String resumeName;
|
||||||
|
|
||||||
|
/** 目标岗位 */
|
||||||
|
@Size(max = 100, message = "目标岗位长度不能超过100")
|
||||||
|
private String targetPosition;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package org.jiayunet.pojo.param.resume;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.Size;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,18 +15,19 @@ import java.util.List;
|
|||||||
public class ResumeInternshipParam {
|
public class ResumeInternshipParam {
|
||||||
|
|
||||||
/** 公司名称 */
|
/** 公司名称 */
|
||||||
@NotBlank(message = "公司名称不能为空")
|
@Size(max = 200, message = "公司名称长度不能超过200")
|
||||||
private String companyName;
|
private String companyName;
|
||||||
|
|
||||||
/** 职位 */
|
/** 职位 */
|
||||||
@NotBlank(message = "职位不能为空")
|
@Size(max = 100, message = "职位长度不能超过100")
|
||||||
private String position;
|
private String position;
|
||||||
|
|
||||||
/** 开始时间,格式:2023.06 */
|
/** 开始时间,格式:2023.06 */
|
||||||
@NotBlank(message = "开始时间不能为空")
|
@Size(max = 10, message = "开始时间长度不能超过10")
|
||||||
private String startDate;
|
private String startDate;
|
||||||
|
|
||||||
/** 结束时间,格式:2023.09,至今则为空 */
|
/** 结束时间,格式:2023.09,至今则为空 */
|
||||||
|
@Size(max = 10, message = "结束时间长度不能超过10")
|
||||||
private String endDate;
|
private String endDate;
|
||||||
|
|
||||||
/** 描述段落 */
|
/** 描述段落 */
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.jiayunet.pojo.param.resume;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简历-社团组织经历保存入参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ResumeOrganizationParam {
|
||||||
|
|
||||||
|
/** 社团/组织名称 */
|
||||||
|
@Size(max = 200, message = "社团/组织名称长度不能超过200")
|
||||||
|
private String organizationName;
|
||||||
|
|
||||||
|
/** 担任角色 */
|
||||||
|
@Size(max = 100, message = "担任角色长度不能超过100")
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
/** 开始时间,格式:2023.06 */
|
||||||
|
@Size(max = 10, message = "开始时间长度不能超过10")
|
||||||
|
private String startDate;
|
||||||
|
|
||||||
|
/** 结束时间,格式:2023.09,至今则为空 */
|
||||||
|
@Size(max = 10, message = "结束时间长度不能超过10")
|
||||||
|
private String endDate;
|
||||||
|
|
||||||
|
/** 描述段落 */
|
||||||
|
private List<DescriptionParagraph> description;
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package org.jiayunet.pojo.param.resume;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简历-社团组织经历编辑入参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ResumeOrganizationUpdateParam extends ResumeOrganizationParam {
|
||||||
|
|
||||||
|
/** 记录ID */
|
||||||
|
@NotNull(message = "id不能为空")
|
||||||
|
private Long id;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package org.jiayunet.pojo.param.resume;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,30 +17,39 @@ public class ResumeParam {
|
|||||||
private Long resumeId;
|
private Long resumeId;
|
||||||
|
|
||||||
/** 简历名称 */
|
/** 简历名称 */
|
||||||
|
@Size(max = 100, message = "简历名称长度不能超过100")
|
||||||
private String resumeName;
|
private String resumeName;
|
||||||
|
|
||||||
/** 目标岗位 */
|
/** 目标岗位 */
|
||||||
|
@Size(max = 100, message = "目标岗位长度不能超过100")
|
||||||
private String targetPosition;
|
private String targetPosition;
|
||||||
|
|
||||||
/** 头像URL */
|
/** 头像URL */
|
||||||
|
@Size(max = 500, message = "头像URL长度不能超过500")
|
||||||
private String avatarUrl;
|
private String avatarUrl;
|
||||||
|
|
||||||
/** 真实姓名 */
|
/** 真实姓名 */
|
||||||
|
@Size(max = 50, message = "真实姓名长度不能超过50")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
/** 邮箱 */
|
/** 邮箱 */
|
||||||
|
@Size(max = 100, message = "邮箱长度不能超过100")
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
/** 手机号码 */
|
/** 手机号码 */
|
||||||
|
@Size(max = 20, message = "手机号码长度不能超过20")
|
||||||
private String mobileNumber;
|
private String mobileNumber;
|
||||||
|
|
||||||
/** 所在城市 */
|
/** 所在城市 */
|
||||||
|
@Size(max = 50, message = "所在城市长度不能超过50")
|
||||||
private String city;
|
private String city;
|
||||||
|
|
||||||
/** 微信号 */
|
/** 微信号 */
|
||||||
|
@Size(max = 50, message = "微信号长度不能超过50")
|
||||||
private String wechatNumber;
|
private String wechatNumber;
|
||||||
|
|
||||||
/** 作品集链接 */
|
/** 作品集链接 */
|
||||||
|
@Size(max = 500, message = "作品集链接长度不能超过500")
|
||||||
private String portfolioUrl;
|
private String portfolioUrl;
|
||||||
|
|
||||||
/** 技能标签列表 */
|
/** 技能标签列表 */
|
||||||
@@ -50,4 +60,12 @@ public class ResumeParam {
|
|||||||
|
|
||||||
/** 个人概述 */
|
/** 个人概述 */
|
||||||
private String summary;
|
private String summary;
|
||||||
|
|
||||||
|
/** 兴趣爱好 */
|
||||||
|
@Size(max = 2000, message = "兴趣爱好长度不能超过2000")
|
||||||
|
private String hobbies;
|
||||||
|
|
||||||
|
/** 语言能力 */
|
||||||
|
@Size(max = 2000, message = "语言能力长度不能超过2000")
|
||||||
|
private String languageSkills;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.jiayunet.pojo.param.resume;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.Size;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,20 +15,23 @@ import java.util.List;
|
|||||||
public class ResumeProjectParam {
|
public class ResumeProjectParam {
|
||||||
|
|
||||||
/** 所属公司 */
|
/** 所属公司 */
|
||||||
|
@Size(max = 200, message = "所属公司长度不能超过200")
|
||||||
private String companyName;
|
private String companyName;
|
||||||
|
|
||||||
/** 项目名称 */
|
/** 项目名称 */
|
||||||
@NotBlank(message = "项目名称不能为空")
|
@Size(max = 200, message = "项目名称长度不能超过200")
|
||||||
private String projectName;
|
private String projectName;
|
||||||
|
|
||||||
/** 担任角色 */
|
/** 担任角色 */
|
||||||
|
@Size(max = 100, message = "担任角色长度不能超过100")
|
||||||
private String role;
|
private String role;
|
||||||
|
|
||||||
/** 开始时间,格式:2023.06 */
|
/** 开始时间,格式:2023.06 */
|
||||||
@NotBlank(message = "开始时间不能为空")
|
@Size(max = 10, message = "开始时间长度不能超过10")
|
||||||
private String startDate;
|
private String startDate;
|
||||||
|
|
||||||
/** 结束时间,格式:2023.09,至今则为空 */
|
/** 结束时间,格式:2023.09,至今则为空 */
|
||||||
|
@Size(max = 10, message = "结束时间长度不能超过10")
|
||||||
private String endDate;
|
private String endDate;
|
||||||
|
|
||||||
/** 描述段落 */
|
/** 描述段落 */
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.jiayunet.pojo.param.resume;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.Size;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,18 +15,19 @@ import java.util.List;
|
|||||||
public class ResumeWorkParam {
|
public class ResumeWorkParam {
|
||||||
|
|
||||||
/** 公司名称 */
|
/** 公司名称 */
|
||||||
@NotBlank(message = "公司名称不能为空")
|
@Size(max = 200, message = "公司名称长度不能超过200")
|
||||||
private String companyName;
|
private String companyName;
|
||||||
|
|
||||||
/** 职位 */
|
/** 职位 */
|
||||||
@NotBlank(message = "职位不能为空")
|
@Size(max = 100, message = "职位长度不能超过100")
|
||||||
private String position;
|
private String position;
|
||||||
|
|
||||||
/** 开始时间,格式:2023.06 */
|
/** 开始时间,格式:2023.06 */
|
||||||
@NotBlank(message = "开始时间不能为空")
|
@Size(max = 10, message = "开始时间长度不能超过10")
|
||||||
private String startDate;
|
private String startDate;
|
||||||
|
|
||||||
/** 结束时间,格式:2023.09,至今则为空 */
|
/** 结束时间,格式:2023.09,至今则为空 */
|
||||||
|
@Size(max = 10, message = "结束时间长度不能超过10")
|
||||||
private String endDate;
|
private String endDate;
|
||||||
|
|
||||||
/** 描述段落 */
|
/** 描述段落 */
|
||||||
|
|||||||
+4
-2
@@ -3,7 +3,7 @@ package org.jiayunet.pojo.param.userProfile;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.Size;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,13 +15,15 @@ import java.util.List;
|
|||||||
public class UserProfileCompetitionParam {
|
public class UserProfileCompetitionParam {
|
||||||
|
|
||||||
/** 竞赛名称 */
|
/** 竞赛名称 */
|
||||||
@NotBlank(message = "竞赛名称不能为空")
|
@Size(max = 200, message = "竞赛名称长度不能超过200")
|
||||||
private String competitionName;
|
private String competitionName;
|
||||||
|
|
||||||
/** 获奖情况,如全国二等奖 */
|
/** 获奖情况,如全国二等奖 */
|
||||||
|
@Size(max = 100, message = "获奖情况长度不能超过100")
|
||||||
private String award;
|
private String award;
|
||||||
|
|
||||||
/** 获奖时间,格式:2023.07 */
|
/** 获奖时间,格式:2023.07 */
|
||||||
|
@Size(max = 20, message = "获奖时间长度不能超过20")
|
||||||
private String awardDate;
|
private String awardDate;
|
||||||
|
|
||||||
/** 描述段落 */
|
/** 描述段落 */
|
||||||
|
|||||||
+5
-4
@@ -3,8 +3,7 @@ package org.jiayunet.pojo.param.userProfile;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.Size;
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,23 +15,25 @@ import java.util.List;
|
|||||||
public class UserProfileEducationParam {
|
public class UserProfileEducationParam {
|
||||||
|
|
||||||
/** 学校名称 */
|
/** 学校名称 */
|
||||||
@NotBlank(message = "学校名称不能为空")
|
@Size(max = 100, message = "学校名称长度不能超过100")
|
||||||
private String school;
|
private String school;
|
||||||
|
|
||||||
/** 专业 */
|
/** 专业 */
|
||||||
|
@Size(max = 100, message = "专业长度不能超过100")
|
||||||
private String major;
|
private String major;
|
||||||
|
|
||||||
/** 学历 1=大专 2=本科 3=硕士 4=博士 */
|
/** 学历 1=大专 2=本科 3=硕士 4=博士 */
|
||||||
@NotNull(message = "学历不能为空")
|
|
||||||
private Integer degree;
|
private Integer degree;
|
||||||
|
|
||||||
/** 学习形式 0=全日制 1=非全日制 */
|
/** 学习形式 0=全日制 1=非全日制 */
|
||||||
private Integer studyType;
|
private Integer studyType;
|
||||||
|
|
||||||
/** 开始时间,格式:2023.09 */
|
/** 开始时间,格式:2023.09 */
|
||||||
|
@Size(max = 10, message = "开始时间长度不能超过10")
|
||||||
private String startDate;
|
private String startDate;
|
||||||
|
|
||||||
/** 结束时间,格式:2024.06 */
|
/** 结束时间,格式:2024.06 */
|
||||||
|
@Size(max = 10, message = "结束时间长度不能超过10")
|
||||||
private String endDate;
|
private String endDate;
|
||||||
|
|
||||||
/** 描述段落 */
|
/** 描述段落 */
|
||||||
|
|||||||
+5
-4
@@ -3,7 +3,7 @@ package org.jiayunet.pojo.param.userProfile;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.Size;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,18 +15,19 @@ import java.util.List;
|
|||||||
public class UserProfileInternshipParam {
|
public class UserProfileInternshipParam {
|
||||||
|
|
||||||
/** 公司名称 */
|
/** 公司名称 */
|
||||||
@NotBlank(message = "公司名称不能为空")
|
@Size(max = 100, message = "公司名称长度不能超过100")
|
||||||
private String companyName;
|
private String companyName;
|
||||||
|
|
||||||
/** 职位 */
|
/** 职位 */
|
||||||
@NotBlank(message = "职位不能为空")
|
@Size(max = 100, message = "职位长度不能超过100")
|
||||||
private String position;
|
private String position;
|
||||||
|
|
||||||
/** 开始时间,格式:2023.06 */
|
/** 开始时间,格式:2023.06 */
|
||||||
@NotBlank(message = "开始时间不能为空")
|
@Size(max = 10, message = "开始时间长度不能超过10")
|
||||||
private String startDate;
|
private String startDate;
|
||||||
|
|
||||||
/** 结束时间,格式:2023.09,至今则为空 */
|
/** 结束时间,格式:2023.09,至今则为空 */
|
||||||
|
@Size(max = 10, message = "结束时间长度不能超过10")
|
||||||
private String endDate;
|
private String endDate;
|
||||||
|
|
||||||
/** 描述段落 */
|
/** 描述段落 */
|
||||||
|
|||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package org.jiayunet.pojo.param.userProfile;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 社团组织经历保存入参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UserProfileOrganizationParam {
|
||||||
|
|
||||||
|
/** 社团/组织名称 */
|
||||||
|
@Size(max = 200, message = "社团/组织名称长度不能超过200")
|
||||||
|
private String organizationName;
|
||||||
|
|
||||||
|
/** 担任角色 */
|
||||||
|
@Size(max = 100, message = "担任角色长度不能超过100")
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
/** 开始时间,格式:2023.06 */
|
||||||
|
@Size(max = 10, message = "开始时间长度不能超过10")
|
||||||
|
private String startDate;
|
||||||
|
|
||||||
|
/** 结束时间,格式:2023.09,至今则为空 */
|
||||||
|
@Size(max = 10, message = "结束时间长度不能超过10")
|
||||||
|
private String endDate;
|
||||||
|
|
||||||
|
/** 描述段落 */
|
||||||
|
private List<DescriptionParagraph> description;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package org.jiayunet.pojo.param.userProfile;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.Size;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,25 +14,31 @@ import java.util.List;
|
|||||||
public class UserProfileParam {
|
public class UserProfileParam {
|
||||||
|
|
||||||
/** 真实姓名 */
|
/** 真实姓名 */
|
||||||
|
@Size(max = 50, message = "真实姓名长度不能超过50")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
/** 邮箱 */
|
/** 邮箱 */
|
||||||
|
@Size(max = 100, message = "邮箱长度不能超过100")
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
/** 手机号码 */
|
/** 手机号码 */
|
||||||
|
@Size(max = 50, message = "手机号码长度不能超过50")
|
||||||
private String mobileNumber;
|
private String mobileNumber;
|
||||||
|
|
||||||
/** 身份证号 */
|
/** 身份证号 */
|
||||||
|
@Size(max = 50, message = "身份证号长度不能超过50")
|
||||||
private String idCard;
|
private String idCard;
|
||||||
|
|
||||||
/** 所在城市编码 */
|
/** 所在城市编码 */
|
||||||
@NotBlank(message = "所在城市不能为空")
|
@Size(max = 20, message = "所在城市编码长度不能超过20")
|
||||||
private String regionCode;
|
private String regionCode;
|
||||||
|
|
||||||
/** 微信号 */
|
/** 微信号 */
|
||||||
|
@Size(max = 50, message = "微信号长度不能超过50")
|
||||||
private String wechatNumber;
|
private String wechatNumber;
|
||||||
|
|
||||||
/** 作品集链接 */
|
/** 作品集链接 */
|
||||||
|
@Size(max = 500, message = "作品集链接长度不能超过500")
|
||||||
private String portfolioUrl;
|
private String portfolioUrl;
|
||||||
|
|
||||||
/** 工作年限 */
|
/** 工作年限 */
|
||||||
@@ -46,4 +52,12 @@ public class UserProfileParam {
|
|||||||
|
|
||||||
/** 证书标签列表 */
|
/** 证书标签列表 */
|
||||||
private List<String> certificates;
|
private List<String> certificates;
|
||||||
|
|
||||||
|
/** 兴趣爱好 */
|
||||||
|
@Size(max = 2000, message = "兴趣爱好长度不能超过2000")
|
||||||
|
private String hobbies;
|
||||||
|
|
||||||
|
/** 语言能力 */
|
||||||
|
@Size(max = 2000, message = "语言能力长度不能超过2000")
|
||||||
|
private String languageSkills;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -3,7 +3,7 @@ package org.jiayunet.pojo.param.userProfile;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.Size;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,20 +15,23 @@ import java.util.List;
|
|||||||
public class UserProfileProjectParam {
|
public class UserProfileProjectParam {
|
||||||
|
|
||||||
/** 所属公司 */
|
/** 所属公司 */
|
||||||
|
@Size(max = 100, message = "所属公司长度不能超过100")
|
||||||
private String companyName;
|
private String companyName;
|
||||||
|
|
||||||
/** 项目名称 */
|
/** 项目名称 */
|
||||||
@NotBlank(message = "项目名称不能为空")
|
@Size(max = 200, message = "项目名称长度不能超过200")
|
||||||
private String projectName;
|
private String projectName;
|
||||||
|
|
||||||
/** 担任角色 */
|
/** 担任角色 */
|
||||||
|
@Size(max = 100, message = "担任角色长度不能超过100")
|
||||||
private String role;
|
private String role;
|
||||||
|
|
||||||
/** 开始时间,格式:2023.06 */
|
/** 开始时间,格式:2023.06 */
|
||||||
@NotBlank(message = "开始时间不能为空")
|
@Size(max = 10, message = "开始时间长度不能超过10")
|
||||||
private String startDate;
|
private String startDate;
|
||||||
|
|
||||||
/** 结束时间,格式:2023.09,至今则为空 */
|
/** 结束时间,格式:2023.09,至今则为空 */
|
||||||
|
@Size(max = 10, message = "结束时间长度不能超过10")
|
||||||
private String endDate;
|
private String endDate;
|
||||||
|
|
||||||
/** 描述段落 */
|
/** 描述段落 */
|
||||||
|
|||||||
+5
-4
@@ -3,7 +3,7 @@ package org.jiayunet.pojo.param.userProfile;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.Size;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,18 +15,19 @@ import java.util.List;
|
|||||||
public class UserProfileWorkParam {
|
public class UserProfileWorkParam {
|
||||||
|
|
||||||
/** 公司名称 */
|
/** 公司名称 */
|
||||||
@NotBlank(message = "公司名称不能为空")
|
@Size(max = 100, message = "公司名称长度不能超过100")
|
||||||
private String companyName;
|
private String companyName;
|
||||||
|
|
||||||
/** 职位 */
|
/** 职位 */
|
||||||
@NotBlank(message = "职位不能为空")
|
@Size(max = 100, message = "职位长度不能超过100")
|
||||||
private String position;
|
private String position;
|
||||||
|
|
||||||
/** 开始时间,格式:2023.06 */
|
/** 开始时间,格式:2023.06 */
|
||||||
@NotBlank(message = "开始时间不能为空")
|
@Size(max = 10, message = "开始时间长度不能超过10")
|
||||||
private String startDate;
|
private String startDate;
|
||||||
|
|
||||||
/** 结束时间,格式:2023.09,至今则为空 */
|
/** 结束时间,格式:2023.09,至今则为空 */
|
||||||
|
@Size(max = 10, message = "结束时间长度不能超过10")
|
||||||
private String endDate;
|
private String endDate;
|
||||||
|
|
||||||
/** 描述段落 */
|
/** 描述段落 */
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.jiayunet.service;
|
||||||
|
|
||||||
|
import org.jiayunet.mapper.CompanyMapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司服务
|
||||||
|
* <p>主要功能:公司信息查询</p>
|
||||||
|
* <p>依赖服务:无</p>
|
||||||
|
* <p>使用的表:bg_company</p>
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class CompanyService {
|
||||||
|
@Autowired
|
||||||
|
private CompanyMapper companyMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询已完善公司(status=1)去重后的公司类型列表
|
||||||
|
* <p>Service层再次过滤NULL和空白值,兜底脏数据</p>
|
||||||
|
*/
|
||||||
|
public List<String> listCompanyTypes() {
|
||||||
|
return companyMapper.selectDistinctCompanyTypes().stream()
|
||||||
|
.filter(StringUtils::hasText)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,11 @@ import org.jiayunet.exception.BusinessException;
|
|||||||
import org.jiayunet.exception.BusinessExpCodeEnum;
|
import org.jiayunet.exception.BusinessExpCodeEnum;
|
||||||
import org.jiayunet.mapper.FuncPermissionMapper;
|
import org.jiayunet.mapper.FuncPermissionMapper;
|
||||||
import org.jiayunet.mapper.UserFuncPermissionStockMapper;
|
import org.jiayunet.mapper.UserFuncPermissionStockMapper;
|
||||||
|
import org.jiayunet.mapper.UserFuncUnlockMapper;
|
||||||
import org.jiayunet.mapper.UserFuncUsageLogMapper;
|
import org.jiayunet.mapper.UserFuncUsageLogMapper;
|
||||||
import org.jiayunet.pojo.po.FuncPermission;
|
import org.jiayunet.pojo.po.FuncPermission;
|
||||||
import org.jiayunet.pojo.po.UserFuncPermissionStock;
|
import org.jiayunet.pojo.po.UserFuncPermissionStock;
|
||||||
|
import org.jiayunet.pojo.po.UserFuncUnlock;
|
||||||
import org.jiayunet.pojo.po.UserFuncUsageLog;
|
import org.jiayunet.pojo.po.UserFuncUsageLog;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -38,15 +40,34 @@ public class FuncPermissionService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserFuncUsageLogMapper userFuncUsageLogMapper;
|
private UserFuncUsageLogMapper userFuncUsageLogMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserFuncUnlockMapper userFuncUnlockMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查当日是否已存在该业务资源的授权记录
|
||||||
|
* <p>存在说明用户当日已解锁该资源,可直接放行不再扣减</p>
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param funcCode 功能权限编码
|
||||||
|
* @param bizKey 业务资源标识实际值
|
||||||
|
* @return true=当日已授权
|
||||||
|
*/
|
||||||
|
public boolean existsTodayUnlock(Long userId, String funcCode, String bizKey) {
|
||||||
|
|
||||||
|
return userFuncUnlockMapper.exists(new LambdaQueryWrapper<UserFuncUnlock>().eq(UserFuncUnlock::getUserId, userId).eq(UserFuncUnlock::getFuncCode, funcCode).eq(UserFuncUnlock::getUnlockDate, LocalDate.now()).eq(UserFuncUnlock::getBizKey, bizKey));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验用户功能权限并扣减库存
|
* 校验用户功能权限并扣减库存
|
||||||
* <p>1. 查功能权限定义 2. 判断每日免费额度是否充足 3. 免费额度用完则查付费库存 4. 校验时间维度 5. 校验次数维度并原子扣减 6. 插入使用记录</p>
|
* <p>1. 查功能权限定义 2. 判断每日免费额度是否充足 3. 免费额度用完则查付费库存 4. 校验时间维度 5. 校验次数维度并原子扣减 6. 插入使用记录</p>
|
||||||
*
|
*
|
||||||
* @param userId 用户ID
|
* @param userId 用户ID
|
||||||
* @param funcCode 功能权限编码
|
* @param funcCode 功能权限编码
|
||||||
|
* @param bizKey 业务资源标识实际值,为空表示不做当日幂等
|
||||||
* @return 使用记录ID(用于异常回退)
|
* @return 使用记录ID(用于异常回退)
|
||||||
*/
|
*/
|
||||||
public Long checkAndDeduct(Long userId, String funcCode) {
|
public Long checkAndDeduct(Long userId, String funcCode, String bizKey) {
|
||||||
// 1. 查功能权限定义
|
// 1. 查功能权限定义
|
||||||
FuncPermission funcPermission = funcPermissionMapper.selectOne(
|
FuncPermission funcPermission = funcPermissionMapper.selectOne(
|
||||||
new LambdaQueryWrapper<FuncPermission>()
|
new LambdaQueryWrapper<FuncPermission>()
|
||||||
@@ -69,6 +90,7 @@ public class FuncPermissionService {
|
|||||||
);
|
);
|
||||||
if (todayUsed < dailyFreeCount) {
|
if (todayUsed < dailyFreeCount) {
|
||||||
// 免费额度未用完,插入使用记录,直接放行
|
// 免费额度未用完,插入使用记录,直接放行
|
||||||
|
insertUnlock(userId, funcCode, bizKey);
|
||||||
return insertUsageLog(userId, funcCode);
|
return insertUsageLog(userId, funcCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,6 +113,7 @@ public class FuncPermissionService {
|
|||||||
// 5. 次数维度校验
|
// 5. 次数维度校验
|
||||||
if (stock.getCountLimit() == 0) {
|
if (stock.getCountLimit() == 0) {
|
||||||
// 不限次,直接放行
|
// 不限次,直接放行
|
||||||
|
insertUnlock(userId, funcCode, bizKey);
|
||||||
return insertUsageLog(userId, funcCode);
|
return insertUsageLog(userId, funcCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,9 +129,30 @@ public class FuncPermissionService {
|
|||||||
throw new BusinessException(BusinessExpCodeEnum.PERMISSION_DENIED, "功能使用次数已用完");
|
throw new BusinessException(BusinessExpCodeEnum.PERMISSION_DENIED, "功能使用次数已用完");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insertUnlock(userId, funcCode, bizKey);
|
||||||
return insertUsageLog(userId, funcCode);
|
return insertUsageLog(userId, funcCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入当日解锁记录
|
||||||
|
* <p>bizKey 为空表示不做当日幂等,跳过插入</p>
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param funcCode 功能编码
|
||||||
|
* @param bizKey 业务资源标识实际值
|
||||||
|
*/
|
||||||
|
private void insertUnlock(Long userId, String funcCode, String bizKey) {
|
||||||
|
if (bizKey == null || bizKey.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UserFuncUnlock unlock = new UserFuncUnlock();
|
||||||
|
unlock.setUserId(userId);
|
||||||
|
unlock.setFuncCode(funcCode);
|
||||||
|
unlock.setUnlockDate(LocalDate.now());
|
||||||
|
unlock.setBizKey(bizKey);
|
||||||
|
userFuncUnlockMapper.insert(unlock);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插入使用记录
|
* 插入使用记录
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package org.jiayunet.service;
|
|||||||
|
|
||||||
import org.jiayunet.ai.AiChatAbility;
|
import org.jiayunet.ai.AiChatAbility;
|
||||||
import org.jiayunet.ai.AiResponseCleanTool;
|
import org.jiayunet.ai.AiResponseCleanTool;
|
||||||
|
import org.jiayunet.config.CacheConfig;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
import org.springframework.util.StopWatch;
|
import org.springframework.util.StopWatch;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
@@ -22,6 +24,9 @@ import org.jiayunet.pojo.po.*;
|
|||||||
import org.jiayunet.tool.HttpTool;
|
import org.jiayunet.tool.HttpTool;
|
||||||
import org.jiayunet.exception.BusinessException;
|
import org.jiayunet.exception.BusinessException;
|
||||||
import org.jiayunet.exception.BusinessExpCodeEnum;
|
import org.jiayunet.exception.BusinessExpCodeEnum;
|
||||||
|
import org.jiayunet.pojo.vo.HotCityVo;
|
||||||
|
import org.jiayunet.pojo.vo.HotIndustryVo;
|
||||||
|
import org.jiayunet.pojo.vo.HotJobCategoryVo;
|
||||||
import org.jiayunet.pojo.vo.JobApplyCountVo;
|
import org.jiayunet.pojo.vo.JobApplyCountVo;
|
||||||
import org.jiayunet.pojo.vo.JobFavoriteCountVo;
|
import org.jiayunet.pojo.vo.JobFavoriteCountVo;
|
||||||
import org.jiayunet.pojo.vo.JobListItemVo;
|
import org.jiayunet.pojo.vo.JobListItemVo;
|
||||||
@@ -82,6 +87,9 @@ public class JobService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private AiChatAbility aiChatAbility;
|
private AiChatAbility aiChatAbility;
|
||||||
|
|
||||||
|
/** 匹配度排序的候选池大小,匹配度无法在 SQL 层排序,只能在此范围内做全量算分后排序 */
|
||||||
|
public static final int CANDIDATE_POOL_SIZE = 5000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 岗位列表查询
|
* 岗位列表查询
|
||||||
* <p>方法逻辑流程:</p>
|
* <p>方法逻辑流程:</p>
|
||||||
@@ -127,7 +135,7 @@ public class JobService {
|
|||||||
// 6. 执行分页查询
|
// 6. 执行分页查询
|
||||||
Page<JobListItemVo> pageParam = param.toPage();
|
Page<JobListItemVo> pageParam = param.toPage();
|
||||||
pageParam.setSearchCount(false);
|
pageParam.setSearchCount(false);
|
||||||
Page<JobListItemVo> page = jobMapper.selectJobPage(pageParam, param.getJobIds(),param.getStatusFilter(), param.getKeyword(), expandedRegionCodes, expandedCategoryIds, expandedIndustryIds, param.getEmploymentType(), excludeJobIds, excludeCompanyIds, excludeRegionCodes, excludeIndustryIds, param.getRecruitCategory());
|
Page<JobListItemVo> page = jobMapper.selectJobPage(pageParam, param.getJobIds(),param.getStatusFilter(), param.getKeyword(), expandedRegionCodes, expandedCategoryIds, expandedIndustryIds, param.getEmploymentType(), excludeJobIds, excludeCompanyIds, excludeRegionCodes, excludeIndustryIds, param.getRecruitCategory(), param.getCompanyTypes(), param.getCompanyId(), param.getEducations(), param.getPublishStartTime(), param.getPublishEndTime());
|
||||||
|
|
||||||
// 7. 查询收藏状态
|
// 7. 查询收藏状态
|
||||||
List<Long> jobIds = page.getRecords().stream().map(JobListItemVo::getId).collect(Collectors.toList());
|
List<Long> jobIds = page.getRecords().stream().map(JobListItemVo::getId).collect(Collectors.toList());
|
||||||
@@ -148,12 +156,12 @@ public class JobService {
|
|||||||
dto.setStatus(vo.getStatus());
|
dto.setStatus(vo.getStatus());
|
||||||
Map<String, Integer> scoreMap = matchScoreMap.get(vo.getId());
|
Map<String, Integer> scoreMap = matchScoreMap.get(vo.getId());
|
||||||
if (scoreMap != null) {
|
if (scoreMap != null) {
|
||||||
JobMatchScoreDto matchScore = new JobMatchScoreDto(scoreMap.get("educationScore"), scoreMap.get("skillScore"), scoreMap.get("experienceScore"));
|
JobMatchScoreDto matchScore = new JobMatchScoreDto(scoreMap.get("educationScore"), scoreMap.get("skillScore"), scoreMap.get("experienceScore"), scoreMap.get("fitScore"));
|
||||||
dto.setMatchScore(scoreMap.get("totalScore"));
|
dto.setMatchScore(scoreMap.get("totalScore"));
|
||||||
dto.setMatchDetail(matchScore);
|
dto.setMatchDetail(matchScore);
|
||||||
} else {
|
} else {
|
||||||
dto.setMatchScore(0);
|
dto.setMatchScore(0);
|
||||||
dto.setMatchDetail(new JobMatchScoreDto(0, 0, 0));
|
dto.setMatchDetail(new JobMatchScoreDto(0, 0, 0, 0));
|
||||||
}
|
}
|
||||||
return dto;
|
return dto;
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
@@ -161,6 +169,110 @@ public class JobService {
|
|||||||
return new PageResult<>(page.getCurrent(), page.getSize(), page.getTotal(), dtoList);
|
return new PageResult<>(page.getCurrent(), page.getSize(), page.getTotal(), dtoList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位匹配度排序查询(候选池全量,不分页)
|
||||||
|
* <p>匹配度是内存计算出来的,无法在 SQL 层 ORDER BY,因此一次性取候选池全量算分后排序</p>
|
||||||
|
* <p>结果整体缓存,翻页由调用方对返回列表切片,避免每页重复计算</p>
|
||||||
|
*
|
||||||
|
* <p>方法逻辑流程:</p>
|
||||||
|
* <p>1. 复用 listJobs 查询候选池(含匹配度计算)</p>
|
||||||
|
* <p>2. 按匹配度倒序排序,同分按岗位ID倒序保证顺序稳定</p>
|
||||||
|
*
|
||||||
|
* <p>调用约定:缓存 key 由 param 与 userId 共同决定,且在方法执行前就已生成,
|
||||||
|
* 因此调用方必须先把 pageNum 置为 1、pageSize 置为 CANDIDATE_POOL_SIZE 再传入。
|
||||||
|
* 否则每一页都会生成独立缓存条目,缓存命中率恒为 0(方法内部无法补救)</p>
|
||||||
|
*
|
||||||
|
* @param param 查询条件,pageNum 须为 1、pageSize 须为 CANDIDATE_POOL_SIZE
|
||||||
|
* @param userId 用户ID,匹配度因人而异,参与缓存 key
|
||||||
|
* @return 按匹配度倒序排列的候选池全量列表
|
||||||
|
*/
|
||||||
|
@Cacheable(cacheNames = CacheConfig.JOB_MATCH_SORT)
|
||||||
|
public List<JobDto> listJobsSortedByMatch(JobQueryParam param, Long userId) {
|
||||||
|
|
||||||
|
// 1. 查询候选池(listJobs 内部已完成匹配度计算)
|
||||||
|
PageResult<JobDto> candidates = listJobs(param, userId);
|
||||||
|
List<JobDto> list = new ArrayList<>(candidates.getList());
|
||||||
|
|
||||||
|
// 2. 匹配度倒序,同分按岗位ID倒序(同分数量大,缺少二级排序键会导致翻页重复或遗漏)
|
||||||
|
list.sort(Comparator.comparingInt(JobDto::getMatchScore).reversed().thenComparing(JobDto::getId, Comparator.reverseOrder()));
|
||||||
|
|
||||||
|
log.info("匹配度排序完成 userId:{} 候选池:{}条", userId, list.size());
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位列表总数查询
|
||||||
|
* <p>过滤条件与 listJobs 完全一致(SQL 层共用 jobPageWhere 片段),不分页、不查收藏/投递状态、不算匹配度</p>
|
||||||
|
* <p>方法逻辑流程:</p>
|
||||||
|
* <p>1. 扩展筛选条件的子级(地区/岗位类型/行业)</p>
|
||||||
|
* <p>2. 查询用户不感兴趣记录</p>
|
||||||
|
* <p>3. 提取排除列表(岗位/公司/地区/行业)</p>
|
||||||
|
* <p>4. 执行统计查询</p>
|
||||||
|
*/
|
||||||
|
public Long countJobs(JobQueryParam param, Long userId) {
|
||||||
|
|
||||||
|
// 默认只查有效岗位
|
||||||
|
if (param.getStatusFilter() == null) {
|
||||||
|
param.setStatusFilter(Collections.singletonList(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 扩展筛选条件的子级
|
||||||
|
List<String> expandedRegionCodes = expandRegionCodes(param.getRegionCodes());
|
||||||
|
List<Long> expandedCategoryIds = expandCategoryIds(param.getCategoryIds());
|
||||||
|
List<Long> expandedIndustryIds = expandIndustryIds(param.getIndustryIds());
|
||||||
|
|
||||||
|
// 2. 查询用户不感兴趣记录
|
||||||
|
List<UserJobDislike> dislikes = userJobDislikeMapper.selectList(new LambdaQueryWrapper<UserJobDislike>().eq(UserJobDislike::getUserId, userId));
|
||||||
|
|
||||||
|
// 3. 提取排除列表
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
List<Long> excludeCompanyIds = dislikes.stream().map(UserJobDislike::getCompanyId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 4. 提取不感兴趣的地区(直接使用,不扩展子级)
|
||||||
|
List<String> excludeRegionCodes = dislikes.stream().map(UserJobDislike::getRegionCode).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 5. 提取不感兴趣的行业(直接使用,不扩展子级)
|
||||||
|
List<Long> excludeIndustryIds = dislikes.stream().map(UserJobDislike::getIndustryId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 6. 执行统计查询
|
||||||
|
Long total = jobMapper.countJobPage(param.getJobIds(), param.getStatusFilter(), param.getKeyword(), expandedRegionCodes, expandedCategoryIds, expandedIndustryIds, param.getEmploymentType(), excludeJobIds, excludeCompanyIds, excludeRegionCodes, excludeIndustryIds, param.getRecruitCategory(), param.getCompanyTypes(), param.getCompanyId(), param.getEducations(), param.getPublishStartTime(), param.getPublishEndTime());
|
||||||
|
|
||||||
|
return total == null ? 0L : total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热门行业查询(二级行业)
|
||||||
|
* <p>岗位关联公司、公司关联行业(bg_industry level=2),按上架岗位数量倒序取前 limit 个</p>
|
||||||
|
* <p>只返回行业ID和名称,岗位数量仅用于排序不对外暴露</p>
|
||||||
|
*/
|
||||||
|
@Cacheable(cacheNames = CacheConfig.HOT_INDUSTRY)
|
||||||
|
public List<HotIndustryVo> listHotIndustries(Integer limit) {
|
||||||
|
return jobMapper.selectHotIndustries(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热门岗位类型查询(二级岗位类型)
|
||||||
|
* <p>bg_job.category_id 存三级叶子,SQL 内自关联上溯到二级后按二级分组,按上架岗位数量倒序取前 limit 个</p>
|
||||||
|
* <p>只返回岗位类型ID和名称,岗位数量仅用于排序不对外暴露</p>
|
||||||
|
*/
|
||||||
|
@Cacheable(cacheNames = CacheConfig.HOT_JOB_CATEGORY)
|
||||||
|
public List<HotJobCategoryVo> listHotJobCategories(Integer limit) {
|
||||||
|
return jobMapper.selectHotJobCategories(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热门城市查询
|
||||||
|
* <p>岗位关联公司、公司关联地区,按上架岗位数量倒序取前 limit 个</p>
|
||||||
|
* <p>只返回地区编码和名称,岗位数量仅用于排序不对外暴露</p>
|
||||||
|
*/
|
||||||
|
@Cacheable(cacheNames = CacheConfig.HOT_CITY)
|
||||||
|
public List<HotCityVo> listHotCities(Integer limit) {
|
||||||
|
return jobMapper.selectHotCities(limit);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扩展地区编码(包含自身和所有子级)
|
* 扩展地区编码(包含自身和所有子级)
|
||||||
* <p>一次查询:code本身 + provinceCode匹配 + cityCode匹配</p>
|
* <p>一次查询:code本身 + provinceCode匹配 + cityCode匹配</p>
|
||||||
@@ -252,6 +364,8 @@ public class JobService {
|
|||||||
item.setMinExperience(job.getMinExperience());
|
item.setMinExperience(job.getMinExperience());
|
||||||
item.setRequiredMajorIds(job.getRequiredMajorIds());
|
item.setRequiredMajorIds(job.getRequiredMajorIds());
|
||||||
item.setMajorSensitivity(job.getMajorSensitivity());
|
item.setMajorSensitivity(job.getMajorSensitivity());
|
||||||
|
item.setCategoryId(job.getCategoryId());
|
||||||
|
item.setCompanyIndustryId(company.getIndustryId());
|
||||||
jobList.add(item);
|
jobList.add(item);
|
||||||
Map<Long, Map<String, Integer>> matchScoreMap = jobMatchService.batchCalculateMatchScore(jobList, userId);
|
Map<Long, Map<String, Integer>> matchScoreMap = jobMatchService.batchCalculateMatchScore(jobList, userId);
|
||||||
Map<String, Integer> scoreMap = matchScoreMap.get(jobId);
|
Map<String, Integer> scoreMap = matchScoreMap.get(jobId);
|
||||||
@@ -317,10 +431,10 @@ public class JobService {
|
|||||||
dto.setIsFavorite(count > 0);
|
dto.setIsFavorite(count > 0);
|
||||||
if (scoreMap != null) {
|
if (scoreMap != null) {
|
||||||
dto.setMatchScore(scoreMap.get("totalScore"));
|
dto.setMatchScore(scoreMap.get("totalScore"));
|
||||||
dto.setMatchDetail(new JobMatchScoreDto(scoreMap.get("educationScore"), scoreMap.get("skillScore"), scoreMap.get("experienceScore")));
|
dto.setMatchDetail(new JobMatchScoreDto(scoreMap.get("educationScore"), scoreMap.get("skillScore"), scoreMap.get("experienceScore"), scoreMap.get("fitScore")));
|
||||||
} else {
|
} else {
|
||||||
dto.setMatchScore(0);
|
dto.setMatchScore(0);
|
||||||
dto.setMatchDetail(new JobMatchScoreDto(0, 0, 0));
|
dto.setMatchDetail(new JobMatchScoreDto(0, 0, 0, 0));
|
||||||
}
|
}
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
@@ -565,6 +679,13 @@ public class JobService {
|
|||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计有效岗位总数(status=0)
|
||||||
|
*/
|
||||||
|
public Long countValidJobs() {
|
||||||
|
return jobMapper.selectCount(new LambdaQueryWrapper<Job>().eq(Job::getStatus, 0)) + 300000;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 收藏统计
|
* 收藏统计
|
||||||
* <p>方法逻辑流程:</p>
|
* <p>方法逻辑流程:</p>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import javax.servlet.http.Cookie;
|
import javax.servlet.http.Cookie;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
@@ -33,7 +34,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录服务
|
* 登录服务
|
||||||
* <p>依赖:SmsService(验证码发送与校验)、UserRegisterService(自动注册)</p>
|
* <p>依赖:SmsService(验证码发送与校验)、UserRegisterService(自动注册/超管注册)</p>
|
||||||
* <p>使用表:bg_user(查询/创建用户)</p>
|
* <p>使用表:bg_user(查询/创建用户)</p>
|
||||||
* <p>使用Redis:login:token:{userId}(登录令牌与多设备管理)</p>
|
* <p>使用Redis:login:token:{userId}(登录令牌与多设备管理)</p>
|
||||||
*
|
*
|
||||||
@@ -52,6 +53,9 @@ public class LoginService {
|
|||||||
@Value("${app.login.device_online_quantity:5}")
|
@Value("${app.login.device_online_quantity:5}")
|
||||||
private int deviceOnlineQuantity;
|
private int deviceOnlineQuantity;
|
||||||
|
|
||||||
|
@Value("${app.super_admin.password:}")
|
||||||
|
private String superAdminPassword;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private SmsService smsService;
|
private SmsService smsService;
|
||||||
|
|
||||||
@@ -106,6 +110,65 @@ public class LoginService {
|
|||||||
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "账号已被禁用");
|
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "账号已被禁用");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清除验证码
|
||||||
|
smsService.clear(mobileNumber, SmsTemplateEnum.UNIVERSAL);
|
||||||
|
|
||||||
|
|
||||||
|
return doLogin(user, request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超管密码登录
|
||||||
|
* <p>1. 校验超管密码 2. 查询用户,不存在则走超管注册 3. 检查用户状态 4. 完成登录(签发令牌、多设备管理、写Cookie)</p>
|
||||||
|
*
|
||||||
|
* @param mobileNumber 手机号
|
||||||
|
* @param password 超管密码,取 app.super_admin.password,未配置时禁用该登录方式
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public LoginVo superAdminLogin(String mobileNumber, String password, HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
Assert.hasText(mobileNumber, "手机号不能为空");
|
||||||
|
Assert.hasText(password, "超管密码不能为空");
|
||||||
|
|
||||||
|
// 未配置超管密码时该登录方式不可用
|
||||||
|
if (!StringUtils.hasText(superAdminPassword)) {
|
||||||
|
log.warn("超管登录被拒绝:未配置超管密码 mobileNumber:{}", mobileNumber);
|
||||||
|
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "超管密码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验超管密码
|
||||||
|
if (!superAdminPassword.equals(password)) {
|
||||||
|
log.warn("超管登录密码错误 mobileNumber:{} ip:{}", mobileNumber, HttpIpTool.gteRealIP(request));
|
||||||
|
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "超管密码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户
|
||||||
|
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getMobileNumber, mobileNumber));
|
||||||
|
|
||||||
|
// 用户不存在则走超管注册
|
||||||
|
if (user == null) {
|
||||||
|
user = userRegisterService.supperMasterRegister(mobileNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户状态
|
||||||
|
if (user.getStatus() != null && user.getStatus() == 1) {
|
||||||
|
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "账号已被禁用");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("超管登录成功 userId:{} mobileNumber:{} ip:{}", user.getId(), mobileNumber, HttpIpTool.gteRealIP(request));
|
||||||
|
|
||||||
|
return doLogin(user, request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成登录:签发令牌、维护多设备在线、写入Cookie
|
||||||
|
* <p>1. 生成uuId并签发JWT 2. 读取或初始化Redis登录信息 3. 过滤过期设备并按上限剔除最早设备 4. 追加当前设备后回写Redis 5. 设置Token Cookie</p>
|
||||||
|
*
|
||||||
|
* @param user 已通过身份校验的用户
|
||||||
|
* @param request 用于获取真实登录IP
|
||||||
|
* @param response 用于写入Token Cookie
|
||||||
|
* @return 登录结果
|
||||||
|
*/
|
||||||
|
public LoginVo doLogin(User user, HttpServletRequest request, HttpServletResponse response) {
|
||||||
// 生成JWT
|
// 生成JWT
|
||||||
String uuId = UUID.randomUUID().toString().replace("-", "");
|
String uuId = UUID.randomUUID().toString().replace("-", "");
|
||||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||||
@@ -150,8 +213,6 @@ public class LoginService {
|
|||||||
info.setLoginDevices(devices);
|
info.setLoginDevices(devices);
|
||||||
redisServerTool.set(redisKey, info, tokenExceedTime, TimeUnit.SECONDS);
|
redisServerTool.set(redisKey, info, tokenExceedTime, TimeUnit.SECONDS);
|
||||||
|
|
||||||
// 清除验证码
|
|
||||||
smsService.clear(mobileNumber, SmsTemplateEnum.UNIVERSAL);
|
|
||||||
|
|
||||||
// 设置Cookie
|
// 设置Cookie
|
||||||
Cookie cookie = new Cookie("Token", token);
|
Cookie cookie = new Cookie("Token", token);
|
||||||
|
|||||||
@@ -50,9 +50,10 @@ public class MemberGrantService {
|
|||||||
*
|
*
|
||||||
* @param userId 用户ID
|
* @param userId 用户ID
|
||||||
* @param days 会员天数
|
* @param days 会员天数
|
||||||
|
* @param memberType 会员类型 1=正式 2=试用;发正式必置正式,发试用时若已是正式则保持正式(一旦正式不可回退)
|
||||||
*/
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void grant(Long userId, int days) {
|
public void grant(Long userId, int days, int memberType) {
|
||||||
Instant expireTime = Instant.now().plus(days, ChronoUnit.DAYS);
|
Instant expireTime = Instant.now().plus(days, ChronoUnit.DAYS);
|
||||||
|
|
||||||
// 1. 续费会员
|
// 1. 续费会员
|
||||||
@@ -61,10 +62,16 @@ public class MemberGrantService {
|
|||||||
MemberUser newMember = new MemberUser();
|
MemberUser newMember = new MemberUser();
|
||||||
newMember.setUserId(userId);
|
newMember.setUserId(userId);
|
||||||
newMember.setExpireTime(expireTime);
|
newMember.setExpireTime(expireTime);
|
||||||
|
newMember.setMemberType(memberType);
|
||||||
memberUserMapper.insert(newMember);
|
memberUserMapper.insert(newMember);
|
||||||
} else {
|
} else {
|
||||||
Instant baseTime = memberUser.getExpireTime().isAfter(Instant.now()) ? memberUser.getExpireTime() : Instant.now();
|
Instant baseTime = memberUser.getExpireTime().isAfter(Instant.now()) ? memberUser.getExpireTime() : Instant.now();
|
||||||
memberUserMapper.update(null, new LambdaUpdateWrapper<MemberUser>().eq(MemberUser::getId, memberUser.getId()).set(MemberUser::getExpireTime, baseTime.plus(days, ChronoUnit.DAYS)));
|
LambdaUpdateWrapper<MemberUser> update = new LambdaUpdateWrapper<MemberUser>().eq(MemberUser::getId, memberUser.getId()).set(MemberUser::getExpireTime, baseTime.plus(days, ChronoUnit.DAYS));
|
||||||
|
// 发正式则升级为正式;发试用时不改类型,避免把已有的正式会员降级(一旦正式不可回退)
|
||||||
|
if (memberType == 1) {
|
||||||
|
update.set(MemberUser::getMemberType, 1);
|
||||||
|
}
|
||||||
|
memberUserMapper.update(null, update);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 发放功能权限
|
// 2. 发放功能权限
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ public class MemberProductService {
|
|||||||
|
|
||||||
// 查商品拿 durationDays,发放会员权益
|
// 查商品拿 durationDays,发放会员权益
|
||||||
MemberProduct product = memberProductMapper.selectById(order.getProductId());
|
MemberProduct product = memberProductMapper.selectById(order.getProductId());
|
||||||
memberGrantService.grant(order.getUserId(), product.getDurationDays());
|
memberGrantService.grant(order.getUserId(), product.getDurationDays(), 1); // 1=正式
|
||||||
messageService.sendToUser(3, order.getUserId(), "会员购买成功", "您已成功购买会员," + product.getDurationDays() + "天会员权益已到账", "memberOrder", order.getId());
|
messageService.sendToUser(3, order.getUserId(), "会员购买成功", "您已成功购买会员," + product.getDurationDays() + "天会员权益已到账", "memberOrder", order.getId());
|
||||||
|
|
||||||
log.info("会员支付成功处理完成 orderNo:{} userId:{} durationDays:{}", orderNo, order.getUserId(), product.getDurationDays());
|
log.info("会员支付成功处理完成 orderNo:{} userId:{} durationDays:{}", orderNo, order.getUserId(), product.getDurationDays());
|
||||||
|
|||||||
@@ -52,9 +52,7 @@ public class MemberStatusService {
|
|||||||
public MemberStatusDto status() {
|
public MemberStatusDto status() {
|
||||||
Long userId = UserSecurityTool.getUserId();
|
Long userId = UserSecurityTool.getUserId();
|
||||||
|
|
||||||
MemberUser memberUser = memberUserMapper.selectOne(
|
MemberUser memberUser = memberUserMapper.selectOne(new LambdaQueryWrapper<MemberUser>().eq(MemberUser::getUserId, userId));
|
||||||
new LambdaQueryWrapper<MemberUser>().eq(MemberUser::getUserId, userId)
|
|
||||||
);
|
|
||||||
|
|
||||||
MemberStatusDto dto = new MemberStatusDto();
|
MemberStatusDto dto = new MemberStatusDto();
|
||||||
if (memberUser == null) {
|
if (memberUser == null) {
|
||||||
@@ -63,6 +61,7 @@ public class MemberStatusService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dto.setIsMember(memberUser.getExpireTime().isAfter(Instant.now()));
|
dto.setIsMember(memberUser.getExpireTime().isAfter(Instant.now()));
|
||||||
|
dto.setMemberType(memberUser.getMemberType());
|
||||||
dto.setExpireTime(memberUser.getExpireTime());
|
dto.setExpireTime(memberUser.getExpireTime());
|
||||||
dto.setCreateTime(memberUser.getCreateTime());
|
dto.setCreateTime(memberUser.getCreateTime());
|
||||||
dto.setUpdateTime(memberUser.getUpdateTime());
|
dto.setUpdateTime(memberUser.getUpdateTime());
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
package org.jiayunet.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jiayunet.exception.BusinessException;
|
||||||
|
import org.jiayunet.exception.BusinessExpCodeEnum;
|
||||||
|
import org.jiayunet.mapper.CompanyMapper;
|
||||||
|
import org.jiayunet.mapper.RecruitAnnouncementBatchMapper;
|
||||||
|
import org.jiayunet.mapper.RecruitAnnouncementMapper;
|
||||||
|
import org.jiayunet.mapper.RecruitAnnouncementCategoryMapper;
|
||||||
|
import org.jiayunet.mapper.RecruitAnnouncementCityMapper;
|
||||||
|
import org.jiayunet.mapper.RecruitAnnouncementEducationMapper;
|
||||||
|
import org.jiayunet.mapper.RecruitAnnouncementIndustryMapper;
|
||||||
|
import org.jiayunet.mapper.RecruitAnnouncementTagMapper;
|
||||||
|
import org.jiayunet.mapper.RecruitAnnouncementYearMapper;
|
||||||
|
import org.jiayunet.pojo.PageResult;
|
||||||
|
import org.jiayunet.pojo.dto.recruitAnnouncement.RecruitAnnouncementDetailDto;
|
||||||
|
import org.jiayunet.pojo.dto.recruitAnnouncement.RecruitAnnouncementDto;
|
||||||
|
import org.jiayunet.pojo.dto.recruitAnnouncement.RecruitAnnouncementRecommendDto;
|
||||||
|
import org.jiayunet.pojo.dto.recruitAnnouncement.RecruitAnnouncementStatisticsDto;
|
||||||
|
import org.jiayunet.pojo.param.recruitAnnouncement.RecruitAnnouncementQueryParam;
|
||||||
|
import org.jiayunet.pojo.param.recruitAnnouncement.RecruitAnnouncementRecommendParam;
|
||||||
|
import org.jiayunet.pojo.po.Company;
|
||||||
|
import org.jiayunet.pojo.po.RecruitAnnouncement;
|
||||||
|
import org.jiayunet.pojo.po.RecruitAnnouncementBatch;
|
||||||
|
import org.jiayunet.pojo.po.RecruitAnnouncementCategory;
|
||||||
|
import org.jiayunet.pojo.po.RecruitAnnouncementCity;
|
||||||
|
import org.jiayunet.pojo.po.RecruitAnnouncementEducation;
|
||||||
|
import org.jiayunet.pojo.po.RecruitAnnouncementIndustry;
|
||||||
|
import org.jiayunet.pojo.po.RecruitAnnouncementTag;
|
||||||
|
import org.jiayunet.pojo.po.RecruitAnnouncementYear;
|
||||||
|
import org.jiayunet.pojo.vo.RecruitAnnouncementListItemVo;
|
||||||
|
import org.jiayunet.pojo.vo.RecruitAnnouncementStatisticsVo;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 招聘公告服务
|
||||||
|
* <p>依赖:无</p>
|
||||||
|
* <p>使用表:bg_recruit_announcement(公告主表)、bg_recruit_announcement_year(招聘届数)、
|
||||||
|
* bg_recruit_announcement_batch(批次)、bg_recruit_announcement_tag(标签组)、
|
||||||
|
* bg_recruit_announcement_city(热招城市)、bg_recruit_announcement_category(岗位大类)、
|
||||||
|
* bg_recruit_announcement_industry(行业)、bg_recruit_announcement_education(学历要求)</p>
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class RecruitAnnouncementService {
|
||||||
|
|
||||||
|
/** 统计时间边界所用时区,固定东八区,避免受JVM默认时区影响 */
|
||||||
|
private static final ZoneId STAT_ZONE = ZoneId.of("Asia/Shanghai");
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RecruitAnnouncementMapper recruitAnnouncementMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CompanyMapper companyMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RecruitAnnouncementBatchMapper batchMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RecruitAnnouncementCategoryMapper categoryMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RecruitAnnouncementCityMapper cityMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RecruitAnnouncementEducationMapper educationMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RecruitAnnouncementIndustryMapper industryMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RecruitAnnouncementTagMapper tagMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RecruitAnnouncementYearMapper yearMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询公告最后更新时间
|
||||||
|
* <p>按id倒序取第一条的创建时间,无数据时返回null</p>
|
||||||
|
*/
|
||||||
|
public Instant getLastUpdateTime() {
|
||||||
|
return recruitAnnouncementMapper.selectLastCreateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询招聘公告列表
|
||||||
|
* <p>1. 主表分页查询(多值维度不参与) 2. 按本页id批量查6张从表 3. Java侧分组组装DTO</p>
|
||||||
|
*/
|
||||||
|
public PageResult<RecruitAnnouncementDto> pageAnnouncement(RecruitAnnouncementQueryParam param) {
|
||||||
|
|
||||||
|
//查询数据
|
||||||
|
Page<RecruitAnnouncementListItemVo> pageParam = param.toPage();
|
||||||
|
pageParam.setSearchCount(false);
|
||||||
|
Page<RecruitAnnouncementListItemVo> page = recruitAnnouncementMapper.selectAnnouncementPage(pageParam, param.getKeyword(), param.getPublishTimeStart(), param.getPublishTimeEnd(), param.getApplyEndTimeStart(), param.getApplyEndTimeEnd(), param.getCompanyTypes(), param.getEducationNames(), param.getRecruitYears(), param.getBatchNames(), param.getCategoryNames(), param.getCityNames(), param.getIndustryNames(), param.getTagNames());
|
||||||
|
|
||||||
|
List<Long> announcementIds = page.getRecords().stream().map(RecruitAnnouncementListItemVo::getId).collect(Collectors.toList());
|
||||||
|
if (announcementIds.isEmpty()) {
|
||||||
|
return new PageResult<>(page.getCurrent(), page.getSize(), page.getTotal(), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 按本页id批量查从表,各自按公告id分组
|
||||||
|
Map<Long, List<String>> industryMap = industryMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementIndustry>().in(RecruitAnnouncementIndustry::getAnnouncementId, announcementIds)).stream().collect(Collectors.groupingBy(RecruitAnnouncementIndustry::getAnnouncementId, Collectors.mapping(RecruitAnnouncementIndustry::getIndustryName, Collectors.toList())));
|
||||||
|
Map<Long, List<String>> categoryMap = categoryMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementCategory>().in(RecruitAnnouncementCategory::getAnnouncementId, announcementIds)).stream().collect(Collectors.groupingBy(RecruitAnnouncementCategory::getAnnouncementId, Collectors.mapping(RecruitAnnouncementCategory::getCategoryName, Collectors.toList())));
|
||||||
|
Map<Long, List<String>> cityMap = cityMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementCity>().in(RecruitAnnouncementCity::getAnnouncementId, announcementIds)).stream().collect(Collectors.groupingBy(RecruitAnnouncementCity::getAnnouncementId, Collectors.mapping(RecruitAnnouncementCity::getCityName, Collectors.toList())));
|
||||||
|
Map<Long, List<String>> educationMap = educationMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementEducation>().in(RecruitAnnouncementEducation::getAnnouncementId, announcementIds)).stream().collect(Collectors.groupingBy(RecruitAnnouncementEducation::getAnnouncementId, Collectors.mapping(RecruitAnnouncementEducation::getEducationName, Collectors.toList())));
|
||||||
|
Map<Long, List<Integer>> yearMap = yearMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementYear>().in(RecruitAnnouncementYear::getAnnouncementId, announcementIds)).stream().collect(Collectors.groupingBy(RecruitAnnouncementYear::getAnnouncementId, Collectors.mapping(RecruitAnnouncementYear::getRecruitYear, Collectors.toList())));
|
||||||
|
Map<Long, List<String>> batchMap = batchMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementBatch>().in(RecruitAnnouncementBatch::getAnnouncementId, announcementIds)).stream().collect(Collectors.groupingBy(RecruitAnnouncementBatch::getAnnouncementId, Collectors.mapping(RecruitAnnouncementBatch::getBatchName, Collectors.toList())));
|
||||||
|
|
||||||
|
// 组装返回数据
|
||||||
|
List<RecruitAnnouncementDto> dtoList = page.getRecords().stream().map(vo -> {
|
||||||
|
RecruitAnnouncementDto dto = new RecruitAnnouncementDto();
|
||||||
|
BeanUtils.copyProperties(vo, dto);
|
||||||
|
dto.setIndustryNames(industryMap.getOrDefault(vo.getId(), List.of()));
|
||||||
|
dto.setCategoryNames(categoryMap.getOrDefault(vo.getId(), List.of()));
|
||||||
|
dto.setCityNames(cityMap.getOrDefault(vo.getId(), List.of()));
|
||||||
|
dto.setEducationNames(educationMap.getOrDefault(vo.getId(), List.of()));
|
||||||
|
dto.setRecruitYears(yearMap.getOrDefault(vo.getId(), List.of()));
|
||||||
|
dto.setBatchNames(batchMap.getOrDefault(vo.getId(), List.of()));
|
||||||
|
return dto;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
return new PageResult<>(page.getCurrent(), page.getSize(), page.getTotal(), dtoList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询推荐公告
|
||||||
|
* <p>1. 校验公告存在 2. 取该公告的行业与岗位大类 3. 命中任一维度即推荐 4. 按本页id组装分类与城市</p>
|
||||||
|
*/
|
||||||
|
public PageResult<RecruitAnnouncementRecommendDto> pageRecommend(RecruitAnnouncementRecommendParam param) {
|
||||||
|
|
||||||
|
//校验公告存在
|
||||||
|
Long announcementId = param.getAnnouncementId();
|
||||||
|
RecruitAnnouncement announcement = recruitAnnouncementMapper.selectById(announcementId);
|
||||||
|
if (announcement == null || announcement.getStatus() == 0) {
|
||||||
|
throw new BusinessException(BusinessExpCodeEnum.DATA_NOT_EXIST, "公告不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
//取该公告的行业与岗位大类作为推荐依据
|
||||||
|
List<String> industryNames = industryMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementIndustry>().eq(RecruitAnnouncementIndustry::getAnnouncementId, announcementId)).stream().map(RecruitAnnouncementIndustry::getIndustryName).collect(Collectors.toList());
|
||||||
|
List<String> categoryNames = categoryMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementCategory>().eq(RecruitAnnouncementCategory::getAnnouncementId, announcementId)).stream().map(RecruitAnnouncementCategory::getCategoryName).collect(Collectors.toList());
|
||||||
|
|
||||||
|
//查询数据
|
||||||
|
Page<RecruitAnnouncementListItemVo> pageParam = param.toPage();
|
||||||
|
pageParam.setSearchCount(false);
|
||||||
|
if (industryNames.isEmpty() && categoryNames.isEmpty()) {
|
||||||
|
return new PageResult<>(pageParam.getCurrent(), pageParam.getSize(), 0L, List.of());
|
||||||
|
}
|
||||||
|
Page<RecruitAnnouncementListItemVo> page = recruitAnnouncementMapper.selectRecommendPage(pageParam, industryNames, categoryNames);
|
||||||
|
|
||||||
|
List<Long> announcementIds = page.getRecords().stream().map(RecruitAnnouncementListItemVo::getId).collect(Collectors.toList());
|
||||||
|
if (announcementIds.isEmpty()) {
|
||||||
|
return new PageResult<>(page.getCurrent(), page.getSize(), page.getTotal(), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按本页id批量查从表,各自按公告id分组
|
||||||
|
Map<Long, List<String>> categoryMap = categoryMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementCategory>().in(RecruitAnnouncementCategory::getAnnouncementId, announcementIds)).stream().collect(Collectors.groupingBy(RecruitAnnouncementCategory::getAnnouncementId, Collectors.mapping(RecruitAnnouncementCategory::getCategoryName, Collectors.toList())));
|
||||||
|
Map<Long, List<String>> cityMap = cityMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementCity>().in(RecruitAnnouncementCity::getAnnouncementId, announcementIds)).stream().collect(Collectors.groupingBy(RecruitAnnouncementCity::getAnnouncementId, Collectors.mapping(RecruitAnnouncementCity::getCityName, Collectors.toList())));
|
||||||
|
|
||||||
|
// 组装返回数据
|
||||||
|
List<RecruitAnnouncementRecommendDto> dtoList = page.getRecords().stream().map(vo -> {
|
||||||
|
RecruitAnnouncementRecommendDto dto = new RecruitAnnouncementRecommendDto();
|
||||||
|
BeanUtils.copyProperties(vo, dto);
|
||||||
|
dto.setCategoryNames(categoryMap.getOrDefault(vo.getId(), List.of()));
|
||||||
|
dto.setCityNames(cityMap.getOrDefault(vo.getId(), List.of()));
|
||||||
|
return dto;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
return new PageResult<>(page.getCurrent(), page.getSize(), page.getTotal(), dtoList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询公告详情
|
||||||
|
* <p>1. 查公告主表(不存在或已失效则报错) 2. 补公司logo与类型 3. 按公告id查7张从表组装多值维度</p>
|
||||||
|
*/
|
||||||
|
public RecruitAnnouncementDetailDto getDetail(Long id) {
|
||||||
|
|
||||||
|
// 查询公告
|
||||||
|
RecruitAnnouncement announcement = recruitAnnouncementMapper.selectById(id);
|
||||||
|
if (announcement == null || announcement.getStatus() == 0 || announcement.getCleanStatus() == 0) {
|
||||||
|
throw new BusinessException(BusinessExpCodeEnum.DATA_NOT_EXIST, "公告不存在");
|
||||||
|
}
|
||||||
|
RecruitAnnouncementDetailDto dto = new RecruitAnnouncementDetailDto();
|
||||||
|
BeanUtils.copyProperties(announcement, dto);
|
||||||
|
|
||||||
|
// 补公司logo与类型
|
||||||
|
if (announcement.getCompanyId() != null) {
|
||||||
|
Company company = companyMapper.selectOne(new LambdaQueryWrapper<Company>().select(Company::getLogoUrl, Company::getCompanyType).eq(Company::getId, announcement.getCompanyId()));
|
||||||
|
if (company != null) {
|
||||||
|
dto.setLogoUrl(company.getLogoUrl());
|
||||||
|
dto.setCompanyType(company.getCompanyType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组装多值维度
|
||||||
|
dto.setIndustryNames(industryMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementIndustry>().eq(RecruitAnnouncementIndustry::getAnnouncementId, id)).stream().map(RecruitAnnouncementIndustry::getIndustryName).collect(Collectors.toList()));
|
||||||
|
dto.setCategoryNames(categoryMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementCategory>().eq(RecruitAnnouncementCategory::getAnnouncementId, id)).stream().map(RecruitAnnouncementCategory::getCategoryName).collect(Collectors.toList()));
|
||||||
|
dto.setCityNames(cityMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementCity>().eq(RecruitAnnouncementCity::getAnnouncementId, id)).stream().map(RecruitAnnouncementCity::getCityName).collect(Collectors.toList()));
|
||||||
|
dto.setEducationNames(educationMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementEducation>().eq(RecruitAnnouncementEducation::getAnnouncementId, id)).stream().map(RecruitAnnouncementEducation::getEducationName).collect(Collectors.toList()));
|
||||||
|
dto.setRecruitYears(yearMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementYear>().eq(RecruitAnnouncementYear::getAnnouncementId, id)).stream().map(RecruitAnnouncementYear::getRecruitYear).collect(Collectors.toList()));
|
||||||
|
dto.setBatchNames(batchMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementBatch>().eq(RecruitAnnouncementBatch::getAnnouncementId, id)).stream().map(RecruitAnnouncementBatch::getBatchName).collect(Collectors.toList()));
|
||||||
|
dto.setTagNames(tagMapper.selectList(new LambdaQueryWrapper<RecruitAnnouncementTag>().eq(RecruitAnnouncementTag::getAnnouncementId, id)).stream().map(RecruitAnnouncementTag::getTagName).collect(Collectors.toList()));
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据公告id查询公告地址
|
||||||
|
* <p>只查单列,公告不存在时返回null</p>
|
||||||
|
*/
|
||||||
|
public String getAnnouncementUrl(Long id) {
|
||||||
|
RecruitAnnouncement po = recruitAnnouncementMapper.selectOne(new LambdaQueryWrapper<RecruitAnnouncement>().select(RecruitAnnouncement::getAnnouncementUrl).eq(RecruitAnnouncement::getId, id));
|
||||||
|
return po == null ? null : po.getAnnouncementUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据公告id查询岗位投递地址
|
||||||
|
* <p>只查单列,公告不存在时返回null</p>
|
||||||
|
*/
|
||||||
|
public String getApplyUrl(Long id) {
|
||||||
|
RecruitAnnouncement po = recruitAnnouncementMapper.selectOne(new LambdaQueryWrapper<RecruitAnnouncement>().select(RecruitAnnouncement::getApplyUrl).eq(RecruitAnnouncement::getId, id));
|
||||||
|
return po == null ? null : po.getApplyUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据公告id查询投递邮箱
|
||||||
|
* <p>只查单列,公告不存在时返回null</p>
|
||||||
|
*/
|
||||||
|
public String getApplyEmail(Long id) {
|
||||||
|
RecruitAnnouncement po = recruitAnnouncementMapper.selectOne(new LambdaQueryWrapper<RecruitAnnouncement>().select(RecruitAnnouncement::getApplyEmail).eq(RecruitAnnouncement::getId, id));
|
||||||
|
return po == null ? null : po.getApplyEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计公告数量(今日新增/本月新增/累计)
|
||||||
|
* <p>1. 按东八区算出今日零点、本月1号零点 2. 一次SQL条件聚合出三个计数 3. 转DTO返回</p>
|
||||||
|
*/
|
||||||
|
public RecruitAnnouncementStatisticsDto getStatistics() {
|
||||||
|
LocalDate today = LocalDate.now(STAT_ZONE);
|
||||||
|
Instant dayStart = today.atStartOfDay(STAT_ZONE).toInstant();
|
||||||
|
Instant monthStart = today.withDayOfMonth(1).atStartOfDay(STAT_ZONE).toInstant();
|
||||||
|
RecruitAnnouncementStatisticsVo vo = recruitAnnouncementMapper.selectStatistics(dayStart, monthStart);
|
||||||
|
RecruitAnnouncementStatisticsDto dto = new RecruitAnnouncementStatisticsDto();
|
||||||
|
BeanUtils.copyProperties(vo, dto);
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询公告批次列表
|
||||||
|
* <p>按batch_name分组去重后返回</p>
|
||||||
|
*/
|
||||||
|
public List<String> listBatchName() {
|
||||||
|
return batchMapper.selectBatchNameGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询公告岗位大类列表
|
||||||
|
* <p>按category_name分组去重后返回</p>
|
||||||
|
*/
|
||||||
|
public List<String> listCategoryName() {
|
||||||
|
return categoryMapper.selectCategoryNameGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询公告热招城市列表
|
||||||
|
* <p>按city_name分组去重后返回</p>
|
||||||
|
*/
|
||||||
|
public List<String> listCityName() {
|
||||||
|
return cityMapper.selectCityNameGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询公告学历要求列表
|
||||||
|
* <p>按education_name分组去重后返回</p>
|
||||||
|
*/
|
||||||
|
public List<String> listEducationName() {
|
||||||
|
return educationMapper.selectEducationNameGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询公告行业列表
|
||||||
|
* <p>按industry_name分组去重后返回</p>
|
||||||
|
*/
|
||||||
|
public List<String> listIndustryName() {
|
||||||
|
return industryMapper.selectIndustryNameGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询公告标签列表
|
||||||
|
* <p>按tag_name分组去重后返回</p>
|
||||||
|
*/
|
||||||
|
public List<String> listTagName() {
|
||||||
|
return tagMapper.selectTagNameGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询公告招聘届数列表
|
||||||
|
* <p>按recruit_year分组去重后返回</p>
|
||||||
|
*/
|
||||||
|
public List<Integer> listRecruitYear() {
|
||||||
|
return yearMapper.selectRecruitYearGroup();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import org.jiayunet.tool.UserSecurityTool;
|
|||||||
import org.jiayunet.tool.server.RedisServerTool;
|
import org.jiayunet.tool.server.RedisServerTool;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
@@ -42,6 +43,12 @@ public class UserManageService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RedisServerTool redisServerTool;
|
private RedisServerTool redisServerTool;
|
||||||
|
|
||||||
|
@Value("${app.reward.invite-milestone-count:30}")
|
||||||
|
private int inviteMilestoneCount;
|
||||||
|
|
||||||
|
@Value("${app.reward.invite-milestone-days:150}")
|
||||||
|
private int inviteMilestoneDays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查看个人信息
|
* 查看个人信息
|
||||||
* <p>根据当前登录用户ID查询用户基本信息</p>
|
* <p>根据当前登录用户ID查询用户基本信息</p>
|
||||||
@@ -99,9 +106,12 @@ public class UserManageService {
|
|||||||
List<UserInvite> invites = userInviteMapper.selectList(new LambdaQueryWrapper<UserInvite>().eq(UserInvite::getInviterId, userId));
|
List<UserInvite> invites = userInviteMapper.selectList(new LambdaQueryWrapper<UserInvite>().eq(UserInvite::getInviterId, userId));
|
||||||
|
|
||||||
InviteStatsDto dto = new InviteStatsDto();
|
InviteStatsDto dto = new InviteStatsDto();
|
||||||
|
long total = invites.size();
|
||||||
dto.setInviteCount((long) invites.size());
|
dto.setInviteCount(total);
|
||||||
|
dto.setMilestoneProgress(total % inviteMilestoneCount);
|
||||||
dto.setRewardDays(invites.stream().mapToInt(i -> i.getRewardDays() == null ? 0 : i.getRewardDays()).sum());
|
dto.setRewardDays(invites.stream().mapToInt(i -> i.getRewardDays() == null ? 0 : i.getRewardDays()).sum());
|
||||||
|
dto.setMilestoneTarget(inviteMilestoneCount);
|
||||||
|
dto.setMilestoneDays(inviteMilestoneDays);
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import org.jiayunet.tool.UserSecurityTool;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -23,7 +25,8 @@ import java.util.stream.IntStream;
|
|||||||
* <p>依赖:无</p>
|
* <p>依赖:无</p>
|
||||||
* <p>使用表:bg_user_profile(主表CRUD)、bg_user_profile_education(教育经历)、
|
* <p>使用表:bg_user_profile(主表CRUD)、bg_user_profile_education(教育经历)、
|
||||||
* bg_user_profile_work(工作经历)、bg_user_profile_internship(实习经历)、
|
* bg_user_profile_work(工作经历)、bg_user_profile_internship(实习经历)、
|
||||||
* bg_user_profile_project(项目经历)、bg_user_profile_competition(竞赛经历)</p>
|
* bg_user_profile_project(项目经历)、bg_user_profile_competition(竞赛经历)、
|
||||||
|
* bg_user_profile_organization(社团组织经历)</p>
|
||||||
*
|
*
|
||||||
* @author zk
|
* @author zk
|
||||||
*/
|
*/
|
||||||
@@ -49,6 +52,9 @@ public class UserProfileService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserProfileCompetitionMapper competitionMapper;
|
private UserProfileCompetitionMapper competitionMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserProfileOrganizationMapper organizationMapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private org.jiayunet.service.UserProfileAnalyzeService userProfileAnalyzeService;
|
private org.jiayunet.service.UserProfileAnalyzeService userProfileAnalyzeService;
|
||||||
|
|
||||||
@@ -70,6 +76,9 @@ public class UserProfileService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserResumeCompetitionMapper resumeCompetitionMapper;
|
private UserResumeCompetitionMapper resumeCompetitionMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserResumeOrganizationMapper resumeOrganizationMapper;
|
||||||
|
|
||||||
/** 学历文本→枚举映射 */
|
/** 学历文本→枚举映射 */
|
||||||
private static final Map<String, Integer> DEGREE_MAP = Map.of(
|
private static final Map<String, Integer> DEGREE_MAP = Map.of(
|
||||||
"大专", 1, "本科", 2, "硕士", 3, "博士", 4
|
"大专", 1, "本科", 2, "硕士", 3, "博士", 4
|
||||||
@@ -80,6 +89,16 @@ public class UserProfileService {
|
|||||||
"全日制", 0, "非全日制", 1
|
"全日制", 0, "非全日制", 1
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** 学历枚举→文本映射(个人资料→简历反向映射) */
|
||||||
|
private static final Map<Integer, String> REVERSE_DEGREE_MAP = Map.of(
|
||||||
|
1, "大专", 2, "本科", 3, "硕士", 4, "博士"
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 学习形式枚举→文本映射(个人资料→简历反向映射) */
|
||||||
|
private static final Map<Integer, String> REVERSE_STUDY_TYPE_MAP = Map.of(
|
||||||
|
0, "全日制", 1, "非全日制"
|
||||||
|
);
|
||||||
|
|
||||||
// ==================== 主表 ====================
|
// ==================== 主表 ====================
|
||||||
|
|
||||||
/** 查询当前用户个人资料 */
|
/** 查询当前用户个人资料 */
|
||||||
@@ -89,9 +108,14 @@ public class UserProfileService {
|
|||||||
new LambdaQueryWrapper<UserProfile>().eq(UserProfile::getUserId, userId));
|
new LambdaQueryWrapper<UserProfile>().eq(UserProfile::getUserId, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** JSON字段的TypeHandler,UpdateWrapper显式set时需指定,否则JSON字段不会正确序列化 */
|
||||||
|
private static final String JSON_TYPE_HANDLER = "typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存个人资料主表(upsert)
|
* 保存个人资料主表(upsert)
|
||||||
* <p>1. 按userId查询是否存在 2. 存在则更新,不存在则插入</p>
|
* <p>1. 按userId查询是否存在 2. 存在则更新,不存在则插入</p>
|
||||||
|
* <p>更新时用LambdaUpdateWrapper显式set用户可编辑字段(允许清空为null,绕过全局update-strategy=not_null),
|
||||||
|
* AI派生字段(schoolRank/companyPrestige/majorIds/honors等)不在更新范围内,保留原值</p>
|
||||||
*/
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void saveProfile(UserProfile profile) {
|
public void saveProfile(UserProfile profile) {
|
||||||
@@ -100,11 +124,20 @@ public class UserProfileService {
|
|||||||
new LambdaQueryWrapper<UserProfile>().eq(UserProfile::getUserId, userId));
|
new LambdaQueryWrapper<UserProfile>().eq(UserProfile::getUserId, userId));
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
profile.setId(existing.getId());
|
userProfileMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<UserProfile>()
|
||||||
profile.setUserId(userId);
|
.eq(UserProfile::getId, existing.getId())
|
||||||
profile.setCreateTime(existing.getCreateTime());
|
.set(UserProfile::getName, profile.getName())
|
||||||
profile.setUpdateTime(now);
|
.set(UserProfile::getEmail, profile.getEmail())
|
||||||
userProfileMapper.updateById(profile);
|
.set(UserProfile::getMobileNumber, profile.getMobileNumber())
|
||||||
|
.set(UserProfile::getIdCard, profile.getIdCard())
|
||||||
|
.set(UserProfile::getRegionCode, profile.getRegionCode())
|
||||||
|
.set(UserProfile::getWechatNumber, profile.getWechatNumber())
|
||||||
|
.set(UserProfile::getPortfolioUrl, profile.getPortfolioUrl())
|
||||||
|
.set(UserProfile::getSkills, profile.getSkills(), JSON_TYPE_HANDLER)
|
||||||
|
.set(UserProfile::getCertificates, profile.getCertificates(), JSON_TYPE_HANDLER)
|
||||||
|
.set(UserProfile::getHobbies, profile.getHobbies())
|
||||||
|
.set(UserProfile::getLanguageSkills, profile.getLanguageSkills())
|
||||||
|
.set(UserProfile::getUpdateTime, now));
|
||||||
} else {
|
} else {
|
||||||
profile.setUserId(userId);
|
profile.setUserId(userId);
|
||||||
profile.setCreateTime(now);
|
profile.setCreateTime(now);
|
||||||
@@ -298,15 +331,81 @@ public class UserProfileService {
|
|||||||
userProfileAnalyzeService.analyzeUserProfile(userId);
|
userProfileAnalyzeService.analyzeUserProfile(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 社团组织经历 ====================
|
||||||
|
|
||||||
|
/** 查询社团组织经历列表 */
|
||||||
|
public List<UserProfileOrganization> listOrganization() {
|
||||||
|
Long userId = UserSecurityTool.getUserId();
|
||||||
|
return organizationMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<UserProfileOrganization>()
|
||||||
|
.eq(UserProfileOrganization::getUserId, userId)
|
||||||
|
.orderByAsc(UserProfileOrganization::getSortOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存社团组织经历列表(先删后插)
|
||||||
|
* <p>1. 删除该用户所有社团组织经历 2. 批量插入新数据</p>
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void saveOrganizationList(List<UserProfileOrganization> list) {
|
||||||
|
Long userId = UserSecurityTool.getUserId();
|
||||||
|
Long profileId = getOrCreateProfileId(userId);
|
||||||
|
organizationMapper.delete(
|
||||||
|
new LambdaQueryWrapper<UserProfileOrganization>().eq(UserProfileOrganization::getUserId, userId));
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Instant now = Instant.now();
|
||||||
|
IntStream.range(0, list.size()).forEach(i -> {
|
||||||
|
UserProfileOrganization item = list.get(i);
|
||||||
|
item.setUserId(userId);
|
||||||
|
item.setProfileId(profileId);
|
||||||
|
item.setSortOrder(i);
|
||||||
|
item.setCreateTime(now);
|
||||||
|
item.setUpdateTime(now);
|
||||||
|
});
|
||||||
|
organizationMapper.batchInsert(list);
|
||||||
|
userProfileAnalyzeService.analyzeUserProfile(userId);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 从简历同步到个人资料 ====================
|
// ==================== 从简历同步到个人资料 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据简历ID同步更新个人资料
|
* 根据简历ID同步更新个人资料(AI分析同步执行,阻塞至分析完成)
|
||||||
* <p>1. 校验简历归属 2. 读取简历主表+5子表 3. 事务内覆盖写入Profile主表+5子表 4. 触发一次异步AI分析</p>
|
* <p>1. 校验简历归属 2. 读取简历主表+6子表 3. 事务内覆盖写入Profile主表+6子表 4. 同步执行一次AI分析</p>
|
||||||
|
* <p>适用于前端需要拿到分析结果的场景(如简历上传解析后立即展示)</p>
|
||||||
*/
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void syncProfileFromResume(Long resumeId) {
|
public void syncProfileFromResume(Long resumeId) {
|
||||||
Long userId = UserSecurityTool.getUserId();
|
Long userId = UserSecurityTool.getUserId();
|
||||||
|
copyResumeToProfile(userId, resumeId);
|
||||||
|
// 事务内所有数据写入完成,同步执行AI分析并等待完成
|
||||||
|
userProfileAnalyzeService.analyzeUserProfileSync(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据简历ID同步更新个人资料(AI分析异步执行,立即返回)
|
||||||
|
* <p>1. 校验简历归属 2. 读取简历主表+6子表 3. 事务内覆盖写入Profile主表+6子表 4. 事务提交后异步触发AI分析</p>
|
||||||
|
* <p>适用于同步只是副作用、不需要等待分析结果的场景(如设置默认简历)</p>
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void syncProfileFromResumeAsync(Long resumeId) {
|
||||||
|
Long userId = UserSecurityTool.getUserId();
|
||||||
|
copyResumeToProfile(userId, resumeId);
|
||||||
|
// AI分析必须等事务提交后再触发,否则异步线程读不到本次写入的数据
|
||||||
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
|
@Override
|
||||||
|
public void afterCommit() {
|
||||||
|
userProfileAnalyzeService.analyzeUserProfile(userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将简历数据覆盖写入个人资料(主表+6子表),不触发AI分析
|
||||||
|
* <p>由 syncProfileFromResume / syncProfileFromResumeAsync 复用,须在事务内调用</p>
|
||||||
|
*/
|
||||||
|
private void copyResumeToProfile(Long userId, Long resumeId) {
|
||||||
|
|
||||||
// 1. 校验简历归属当前用户
|
// 1. 校验简历归属当前用户
|
||||||
UserResume resume = userResumeMapper.selectById(resumeId);
|
UserResume resume = userResumeMapper.selectById(resumeId);
|
||||||
@@ -314,12 +413,13 @@ public class UserProfileService {
|
|||||||
throw new BusinessException(BusinessExpCodeEnum.PERMISSION_DENIED, "简历不存在或无权操作");
|
throw new BusinessException(BusinessExpCodeEnum.PERMISSION_DENIED, "简历不存在或无权操作");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 读取简历5张子表
|
// 2. 读取简历6张子表
|
||||||
List<UserResumeEducation> resumeEducationList = resumeEducationMapper.selectList(new LambdaQueryWrapper<UserResumeEducation>().eq(UserResumeEducation::getResumeId, resumeId).orderByAsc(UserResumeEducation::getSortOrder));
|
List<UserResumeEducation> resumeEducationList = resumeEducationMapper.selectList(new LambdaQueryWrapper<UserResumeEducation>().eq(UserResumeEducation::getResumeId, resumeId).orderByAsc(UserResumeEducation::getSortOrder));
|
||||||
List<UserResumeWork> resumeWorkList = resumeWorkMapper.selectList(new LambdaQueryWrapper<UserResumeWork>().eq(UserResumeWork::getResumeId, resumeId).orderByAsc(UserResumeWork::getSortOrder));
|
List<UserResumeWork> resumeWorkList = resumeWorkMapper.selectList(new LambdaQueryWrapper<UserResumeWork>().eq(UserResumeWork::getResumeId, resumeId).orderByAsc(UserResumeWork::getSortOrder));
|
||||||
List<UserResumeInternship> resumeInternshipList = resumeInternshipMapper.selectList(new LambdaQueryWrapper<UserResumeInternship>().eq(UserResumeInternship::getResumeId, resumeId).orderByAsc(UserResumeInternship::getSortOrder));
|
List<UserResumeInternship> resumeInternshipList = resumeInternshipMapper.selectList(new LambdaQueryWrapper<UserResumeInternship>().eq(UserResumeInternship::getResumeId, resumeId).orderByAsc(UserResumeInternship::getSortOrder));
|
||||||
List<UserResumeProject> resumeProjectList = resumeProjectMapper.selectList(new LambdaQueryWrapper<UserResumeProject>().eq(UserResumeProject::getResumeId, resumeId).orderByAsc(UserResumeProject::getSortOrder));
|
List<UserResumeProject> resumeProjectList = resumeProjectMapper.selectList(new LambdaQueryWrapper<UserResumeProject>().eq(UserResumeProject::getResumeId, resumeId).orderByAsc(UserResumeProject::getSortOrder));
|
||||||
List<UserResumeCompetition> resumeCompetitionList = resumeCompetitionMapper.selectList(new LambdaQueryWrapper<UserResumeCompetition>().eq(UserResumeCompetition::getResumeId, resumeId).orderByAsc(UserResumeCompetition::getSortOrder));
|
List<UserResumeCompetition> resumeCompetitionList = resumeCompetitionMapper.selectList(new LambdaQueryWrapper<UserResumeCompetition>().eq(UserResumeCompetition::getResumeId, resumeId).orderByAsc(UserResumeCompetition::getSortOrder));
|
||||||
|
List<UserResumeOrganization> resumeOrganizationList = resumeOrganizationMapper.selectList(new LambdaQueryWrapper<UserResumeOrganization>().eq(UserResumeOrganization::getResumeId, resumeId).orderByAsc(UserResumeOrganization::getSortOrder));
|
||||||
|
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
|
|
||||||
@@ -329,15 +429,20 @@ public class UserProfileService {
|
|||||||
Long profileId;
|
Long profileId;
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
profileId = existing.getId();
|
profileId = existing.getId();
|
||||||
existing.setName(resume.getName());
|
// A语义:严格镜像简历共有字段(含清空为null),与子表先删后插的镜像行为一致;
|
||||||
existing.setEmail(resume.getEmail());
|
// idCard/regionCode及AI派生字段(schoolRank/majorIds等)为个人资料独有,不在同步范围
|
||||||
existing.setMobileNumber(resume.getMobileNumber());
|
userProfileMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<UserProfile>()
|
||||||
existing.setWechatNumber(resume.getWechatNumber());
|
.eq(UserProfile::getId, profileId)
|
||||||
existing.setPortfolioUrl(resume.getPortfolioUrl());
|
.set(UserProfile::getName, resume.getName())
|
||||||
existing.setSkills(resume.getSkills());
|
.set(UserProfile::getEmail, resume.getEmail())
|
||||||
existing.setCertificates(resume.getCertificates());
|
.set(UserProfile::getMobileNumber, resume.getMobileNumber())
|
||||||
existing.setUpdateTime(now);
|
.set(UserProfile::getWechatNumber, resume.getWechatNumber())
|
||||||
userProfileMapper.updateById(existing);
|
.set(UserProfile::getPortfolioUrl, resume.getPortfolioUrl())
|
||||||
|
.set(UserProfile::getSkills, resume.getSkills(), JSON_TYPE_HANDLER)
|
||||||
|
.set(UserProfile::getCertificates, resume.getCertificates(), JSON_TYPE_HANDLER)
|
||||||
|
.set(UserProfile::getHobbies, resume.getHobbies())
|
||||||
|
.set(UserProfile::getLanguageSkills, resume.getLanguageSkills())
|
||||||
|
.set(UserProfile::getUpdateTime, now));
|
||||||
} else {
|
} else {
|
||||||
UserProfile profile = new UserProfile();
|
UserProfile profile = new UserProfile();
|
||||||
profile.setUserId(userId);
|
profile.setUserId(userId);
|
||||||
@@ -348,6 +453,8 @@ public class UserProfileService {
|
|||||||
profile.setPortfolioUrl(resume.getPortfolioUrl());
|
profile.setPortfolioUrl(resume.getPortfolioUrl());
|
||||||
profile.setSkills(resume.getSkills());
|
profile.setSkills(resume.getSkills());
|
||||||
profile.setCertificates(resume.getCertificates());
|
profile.setCertificates(resume.getCertificates());
|
||||||
|
profile.setHobbies(resume.getHobbies());
|
||||||
|
profile.setLanguageSkills(resume.getLanguageSkills());
|
||||||
profile.setCreateTime(now);
|
profile.setCreateTime(now);
|
||||||
profile.setUpdateTime(now);
|
profile.setUpdateTime(now);
|
||||||
userProfileMapper.insert(profile);
|
userProfileMapper.insert(profile);
|
||||||
@@ -461,8 +568,205 @@ public class UserProfileService {
|
|||||||
competitionMapper.batchInsert(profileCompetitionList);
|
competitionMapper.batchInsert(profileCompetitionList);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. 事务内所有数据写入完成,触发一次异步AI分析
|
// 9. 同步社团组织经历
|
||||||
userProfileAnalyzeService.analyzeUserProfile(userId);
|
organizationMapper.delete(new LambdaQueryWrapper<UserProfileOrganization>().eq(UserProfileOrganization::getUserId, userId));
|
||||||
|
if (!resumeOrganizationList.isEmpty()) {
|
||||||
|
List<UserProfileOrganization> profileOrganizationList = IntStream.range(0, resumeOrganizationList.size()).mapToObj(i -> {
|
||||||
|
UserResumeOrganization src = resumeOrganizationList.get(i);
|
||||||
|
UserProfileOrganization dest = new UserProfileOrganization();
|
||||||
|
dest.setProfileId(profileId);
|
||||||
|
dest.setUserId(userId);
|
||||||
|
dest.setOrganizationName(src.getOrganizationName());
|
||||||
|
dest.setRole(src.getRole());
|
||||||
|
dest.setStartDate(src.getStartDate());
|
||||||
|
dest.setEndDate(src.getEndDate());
|
||||||
|
dest.setDescription(src.getDescription());
|
||||||
|
dest.setSortOrder(i);
|
||||||
|
dest.setCreateTime(now);
|
||||||
|
dest.setUpdateTime(now);
|
||||||
|
return dest;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
organizationMapper.batchInsert(profileOrganizationList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 从个人资料生成简历 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据当前用户的个人资料创建一份新简历
|
||||||
|
* <p>syncProfileFromResume 的反向操作:读取个人资料主表+6子表,新建一份简历(名"个人资料简历")及对应子表,返回新简历ID</p>
|
||||||
|
* <p>1. 校验个人资料存在 2. 读取个人资料6子表 3. 新建简历主表(复制共有字段,简历独有字段targetPosition/avatarUrl/city/summary留空)
|
||||||
|
* 4. 转换并批量插入6张简历子表(含degree/studyType反向映射) 5. 返回新简历ID</p>
|
||||||
|
* <p>注意:这是创建新简历(非覆盖),不删除已有简历;简历侧无AI分析,故不触发分析</p>
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Long createResumeFromProfile() {
|
||||||
|
Long userId = UserSecurityTool.getUserId();
|
||||||
|
|
||||||
|
// 1. 校验个人资料存在
|
||||||
|
UserProfile profile = userProfileMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<UserProfile>().eq(UserProfile::getUserId, userId));
|
||||||
|
if (profile == null) {
|
||||||
|
throw new BusinessException(BusinessExpCodeEnum.DATA_NOT_EXIST, "个人资料不存在");
|
||||||
|
}
|
||||||
|
Long profileId = profile.getId();
|
||||||
|
|
||||||
|
// 2. 读取个人资料6张子表
|
||||||
|
List<UserProfileEducation> profileEducationList = educationMapper.selectList(new LambdaQueryWrapper<UserProfileEducation>().eq(UserProfileEducation::getProfileId, profileId).orderByAsc(UserProfileEducation::getSortOrder));
|
||||||
|
List<UserProfileWork> profileWorkList = workMapper.selectList(new LambdaQueryWrapper<UserProfileWork>().eq(UserProfileWork::getProfileId, profileId).orderByAsc(UserProfileWork::getSortOrder));
|
||||||
|
List<UserProfileInternship> profileInternshipList = internshipMapper.selectList(new LambdaQueryWrapper<UserProfileInternship>().eq(UserProfileInternship::getProfileId, profileId).orderByAsc(UserProfileInternship::getSortOrder));
|
||||||
|
List<UserProfileProject> profileProjectList = projectMapper.selectList(new LambdaQueryWrapper<UserProfileProject>().eq(UserProfileProject::getProfileId, profileId).orderByAsc(UserProfileProject::getSortOrder));
|
||||||
|
List<UserProfileCompetition> profileCompetitionList = competitionMapper.selectList(new LambdaQueryWrapper<UserProfileCompetition>().eq(UserProfileCompetition::getProfileId, profileId).orderByAsc(UserProfileCompetition::getSortOrder));
|
||||||
|
List<UserProfileOrganization> profileOrganizationList = organizationMapper.selectList(new LambdaQueryWrapper<UserProfileOrganization>().eq(UserProfileOrganization::getProfileId, profileId).orderByAsc(UserProfileOrganization::getSortOrder));
|
||||||
|
|
||||||
|
Instant now = Instant.now();
|
||||||
|
|
||||||
|
// 3. 新建简历主表(复制共有字段,简历独有字段targetPosition/avatarUrl/city/summary留空)
|
||||||
|
UserResume resume = new UserResume();
|
||||||
|
resume.setUserId(userId);
|
||||||
|
resume.setResumeName("个人资料简历");
|
||||||
|
resume.setName(profile.getName());
|
||||||
|
resume.setEmail(profile.getEmail());
|
||||||
|
resume.setMobileNumber(profile.getMobileNumber());
|
||||||
|
resume.setWechatNumber(profile.getWechatNumber());
|
||||||
|
resume.setPortfolioUrl(profile.getPortfolioUrl());
|
||||||
|
resume.setSkills(profile.getSkills());
|
||||||
|
resume.setCertificates(profile.getCertificates());
|
||||||
|
resume.setHobbies(profile.getHobbies());
|
||||||
|
resume.setLanguageSkills(profile.getLanguageSkills());
|
||||||
|
// 无简历时新建的这份自动设为默认,与 UserResumeService.saveResume 逻辑一致
|
||||||
|
boolean hasResume = userResumeMapper.selectCount(new LambdaQueryWrapper<UserResume>().eq(UserResume::getUserId, userId)) > 0;
|
||||||
|
resume.setIsDefault(hasResume ? 0 : 1);
|
||||||
|
resume.setSortOrder(0);
|
||||||
|
resume.setCreateTime(now);
|
||||||
|
resume.setUpdateTime(now);
|
||||||
|
userResumeMapper.insert(resume);
|
||||||
|
Long resumeId = resume.getId();
|
||||||
|
|
||||||
|
// 4. 转换并插入简历教育经历(含degree/studyType枚举→文本反向映射)
|
||||||
|
if (!profileEducationList.isEmpty()) {
|
||||||
|
List<UserResumeEducation> resumeEducationList = IntStream.range(0, profileEducationList.size()).mapToObj(i -> {
|
||||||
|
UserProfileEducation src = profileEducationList.get(i);
|
||||||
|
UserResumeEducation dest = new UserResumeEducation();
|
||||||
|
dest.setResumeId(resumeId);
|
||||||
|
dest.setUserId(userId);
|
||||||
|
dest.setSchool(src.getSchool());
|
||||||
|
dest.setMajor(src.getMajor());
|
||||||
|
dest.setDegree(Objects.isNull(src.getDegree()) ? null : REVERSE_DEGREE_MAP.get(src.getDegree()));
|
||||||
|
dest.setStudyType(Objects.isNull(src.getStudyType()) ? null : REVERSE_STUDY_TYPE_MAP.get(src.getStudyType()));
|
||||||
|
dest.setStartDate(src.getStartDate());
|
||||||
|
dest.setEndDate(src.getEndDate());
|
||||||
|
dest.setDescription(src.getDescription());
|
||||||
|
dest.setSortOrder(i);
|
||||||
|
dest.setCreateTime(now);
|
||||||
|
dest.setUpdateTime(now);
|
||||||
|
return dest;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
resumeEducationMapper.batchInsert(resumeEducationList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 转换并插入简历工作经历
|
||||||
|
if (!profileWorkList.isEmpty()) {
|
||||||
|
List<UserResumeWork> resumeWorkList = IntStream.range(0, profileWorkList.size()).mapToObj(i -> {
|
||||||
|
UserProfileWork src = profileWorkList.get(i);
|
||||||
|
UserResumeWork dest = new UserResumeWork();
|
||||||
|
dest.setResumeId(resumeId);
|
||||||
|
dest.setUserId(userId);
|
||||||
|
dest.setCompanyName(src.getCompanyName());
|
||||||
|
dest.setPosition(src.getPosition());
|
||||||
|
dest.setStartDate(src.getStartDate());
|
||||||
|
dest.setEndDate(src.getEndDate());
|
||||||
|
dest.setDescription(src.getDescription());
|
||||||
|
dest.setSortOrder(i);
|
||||||
|
dest.setCreateTime(now);
|
||||||
|
dest.setUpdateTime(now);
|
||||||
|
return dest;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
resumeWorkMapper.batchInsert(resumeWorkList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 转换并插入简历实习经历
|
||||||
|
if (!profileInternshipList.isEmpty()) {
|
||||||
|
List<UserResumeInternship> resumeInternshipList = IntStream.range(0, profileInternshipList.size()).mapToObj(i -> {
|
||||||
|
UserProfileInternship src = profileInternshipList.get(i);
|
||||||
|
UserResumeInternship dest = new UserResumeInternship();
|
||||||
|
dest.setResumeId(resumeId);
|
||||||
|
dest.setUserId(userId);
|
||||||
|
dest.setCompanyName(src.getCompanyName());
|
||||||
|
dest.setPosition(src.getPosition());
|
||||||
|
dest.setStartDate(src.getStartDate());
|
||||||
|
dest.setEndDate(src.getEndDate());
|
||||||
|
dest.setDescription(src.getDescription());
|
||||||
|
dest.setSortOrder(i);
|
||||||
|
dest.setCreateTime(now);
|
||||||
|
dest.setUpdateTime(now);
|
||||||
|
return dest;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
resumeInternshipMapper.batchInsert(resumeInternshipList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 转换并插入简历项目经历
|
||||||
|
if (!profileProjectList.isEmpty()) {
|
||||||
|
List<UserResumeProject> resumeProjectList = IntStream.range(0, profileProjectList.size()).mapToObj(i -> {
|
||||||
|
UserProfileProject src = profileProjectList.get(i);
|
||||||
|
UserResumeProject dest = new UserResumeProject();
|
||||||
|
dest.setResumeId(resumeId);
|
||||||
|
dest.setUserId(userId);
|
||||||
|
dest.setCompanyName(src.getCompanyName());
|
||||||
|
dest.setProjectName(src.getProjectName());
|
||||||
|
dest.setRole(src.getRole());
|
||||||
|
dest.setStartDate(src.getStartDate());
|
||||||
|
dest.setEndDate(src.getEndDate());
|
||||||
|
dest.setDescription(src.getDescription());
|
||||||
|
dest.setSortOrder(i);
|
||||||
|
dest.setCreateTime(now);
|
||||||
|
dest.setUpdateTime(now);
|
||||||
|
return dest;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
resumeProjectMapper.batchInsert(resumeProjectList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 转换并插入简历竞赛经历
|
||||||
|
if (!profileCompetitionList.isEmpty()) {
|
||||||
|
List<UserResumeCompetition> resumeCompetitionList = IntStream.range(0, profileCompetitionList.size()).mapToObj(i -> {
|
||||||
|
UserProfileCompetition src = profileCompetitionList.get(i);
|
||||||
|
UserResumeCompetition dest = new UserResumeCompetition();
|
||||||
|
dest.setResumeId(resumeId);
|
||||||
|
dest.setUserId(userId);
|
||||||
|
dest.setCompetitionName(src.getCompetitionName());
|
||||||
|
dest.setAward(src.getAward());
|
||||||
|
dest.setAwardDate(src.getAwardDate());
|
||||||
|
dest.setDescription(src.getDescription());
|
||||||
|
dest.setSortOrder(i);
|
||||||
|
dest.setCreateTime(now);
|
||||||
|
dest.setUpdateTime(now);
|
||||||
|
return dest;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
resumeCompetitionMapper.batchInsert(resumeCompetitionList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 转换并插入简历社团组织经历
|
||||||
|
if (!profileOrganizationList.isEmpty()) {
|
||||||
|
List<UserResumeOrganization> resumeOrganizationList = IntStream.range(0, profileOrganizationList.size()).mapToObj(i -> {
|
||||||
|
UserProfileOrganization src = profileOrganizationList.get(i);
|
||||||
|
UserResumeOrganization dest = new UserResumeOrganization();
|
||||||
|
dest.setResumeId(resumeId);
|
||||||
|
dest.setUserId(userId);
|
||||||
|
dest.setOrganizationName(src.getOrganizationName());
|
||||||
|
dest.setRole(src.getRole());
|
||||||
|
dest.setStartDate(src.getStartDate());
|
||||||
|
dest.setEndDate(src.getEndDate());
|
||||||
|
dest.setDescription(src.getDescription());
|
||||||
|
dest.setSortOrder(i);
|
||||||
|
dest.setCreateTime(now);
|
||||||
|
dest.setUpdateTime(now);
|
||||||
|
return dest;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
resumeOrganizationMapper.batchInsert(resumeOrganizationList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. 返回新简历ID
|
||||||
|
return resumeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 内部方法 ====================
|
// ==================== 内部方法 ====================
|
||||||
|
|||||||
@@ -61,6 +61,43 @@ public class UserRegisterService {
|
|||||||
@Value("${app.reward.invite-days:7}")
|
@Value("${app.reward.invite-days:7}")
|
||||||
private int inviteDays;
|
private int inviteDays;
|
||||||
|
|
||||||
|
@Value("${app.reward.invite-milestone-count:30}")
|
||||||
|
private int inviteMilestoneCount;
|
||||||
|
|
||||||
|
@Value("${app.reward.invite-milestone-days:150}")
|
||||||
|
private int inviteMilestoneDays;
|
||||||
|
|
||||||
|
@Value("${app.super_admin.register_gift_days:365}")
|
||||||
|
private int superAdminRegisterGiftDays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超级管理注册新用户
|
||||||
|
* <p>1. 创建用户并生成邀请码 2. 发放会员,天数取 app.super_admin.register_gift_days</p>
|
||||||
|
*
|
||||||
|
* @param mobileNumber 手机号
|
||||||
|
* @return 新注册的用户
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public User supperMasterRegister(String mobileNumber) {
|
||||||
|
// 创建用户
|
||||||
|
User user = new User();
|
||||||
|
user.setMobileNumber(mobileNumber);
|
||||||
|
user.setNick("用户" + mobileNumber.substring(mobileNumber.length() - 4));
|
||||||
|
user.setInviteCode(generateInviteCode());
|
||||||
|
user.setStatus(0);
|
||||||
|
user.setIsDelete(0L);
|
||||||
|
user.setCreateTime(Instant.now());
|
||||||
|
userMapper.insert(user);
|
||||||
|
|
||||||
|
|
||||||
|
// 发放新用户注册会员权益
|
||||||
|
memberGrantService.grant(user.getId(), superAdminRegisterGiftDays, 1); // 1=正式
|
||||||
|
messageService.sendToUser(1, user.getId(), "欢迎加入OfferPie", "注册成功,已赠送您" + superAdminRegisterGiftDays + "天会员权益,快来体验AI求职助手吧!", null, null);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册新用户
|
* 注册新用户
|
||||||
* <p>1. 创建用户并生成邀请码 2. 若携带邀请码则绑定邀请关系</p>
|
* <p>1. 创建用户并生成邀请码 2. 若携带邀请码则绑定邀请关系</p>
|
||||||
@@ -87,7 +124,7 @@ public class UserRegisterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 发放新用户注册会员权益
|
// 发放新用户注册会员权益
|
||||||
memberGrantService.grant(user.getId(), registerDays);
|
memberGrantService.grant(user.getId(), registerDays, 2); // 2=试用
|
||||||
messageService.sendToUser(1, user.getId(), "欢迎加入OfferPie", "注册成功,已赠送您" + registerDays + "天会员权益,快来体验AI求职助手吧!", null, null);
|
messageService.sendToUser(1, user.getId(), "欢迎加入OfferPie", "注册成功,已赠送您" + registerDays + "天会员权益,快来体验AI求职助手吧!", null, null);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
@@ -121,8 +158,17 @@ public class UserRegisterService {
|
|||||||
userInviteMapper.insert(invite);
|
userInviteMapper.insert(invite);
|
||||||
|
|
||||||
// 发放邀请人会员权益
|
// 发放邀请人会员权益
|
||||||
memberGrantService.grant(inviter.getId(), inviteDays);
|
memberGrantService.grant(inviter.getId(), inviteDays, 2); // 2=试用
|
||||||
messageService.sendToUser(1, inviter.getId(), "邀请奖励到账", "您邀请的好友已成功注册," + inviteDays + "天会员权益已到账", null, null);
|
messageService.sendToUser(1, inviter.getId(), "邀请奖励到账", "您邀请的好友已成功注册," + inviteDays + "天会员权益已到账", null, null);
|
||||||
|
|
||||||
|
// 里程碑奖励:每满N人额外发放
|
||||||
|
long totalInvited = userInviteMapper.selectCount(new LambdaQueryWrapper<UserInvite>().eq(UserInvite::getInviterId, inviter.getId()));
|
||||||
|
if (totalInvited > 0 && totalInvited % inviteMilestoneCount == 0) {
|
||||||
|
memberGrantService.grant(inviter.getId(), inviteMilestoneDays, 2);
|
||||||
|
messageService.sendToUser(1, inviter.getId(), "里程碑奖励到账",
|
||||||
|
"恭喜您累计邀请满" + totalInvited + "位好友,额外赠送" + inviteMilestoneDays + "天会员权益!", null, null);
|
||||||
|
log.info("邀请里程碑奖励 inviterId:{} totalInvited:{} days:{}", inviter.getId(), totalInvited, inviteMilestoneDays);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ import java.util.stream.IntStream;
|
|||||||
* <p>依赖:无</p>
|
* <p>依赖:无</p>
|
||||||
* <p>使用表:bg_user_resume(简历主表CRUD)、bg_user_resume_education(教育经历)、
|
* <p>使用表:bg_user_resume(简历主表CRUD)、bg_user_resume_education(教育经历)、
|
||||||
* bg_user_resume_work(工作经历)、bg_user_resume_internship(实习经历)、
|
* bg_user_resume_work(工作经历)、bg_user_resume_internship(实习经历)、
|
||||||
* bg_user_resume_project(项目经历)、bg_user_resume_competition(竞赛经历)</p>
|
* bg_user_resume_project(项目经历)、bg_user_resume_competition(竞赛经历)、
|
||||||
|
* bg_user_resume_organization(社团组织经历)</p>
|
||||||
*
|
*
|
||||||
* @author zk
|
* @author zk
|
||||||
*/
|
*/
|
||||||
@@ -37,9 +38,15 @@ public class UserResumeService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private MemberUserMapper memberUserMapper;
|
private MemberUserMapper memberUserMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserProfileService userProfileService;
|
||||||
|
|
||||||
@Value("${app.resume.max-count:5}")
|
@Value("${app.resume.max-count:5}")
|
||||||
private int maxResumeCount;
|
private int maxResumeCount;
|
||||||
|
|
||||||
|
/** JSON字段的TypeHandler,UpdateWrapper显式set时需指定,否则JSON字段不会正确序列化 */
|
||||||
|
private static final String JSON_TYPE_HANDLER = "typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler";
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserResumeEducationMapper educationMapper;
|
private UserResumeEducationMapper educationMapper;
|
||||||
|
|
||||||
@@ -55,6 +62,9 @@ public class UserResumeService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserResumeCompetitionMapper competitionMapper;
|
private UserResumeCompetitionMapper competitionMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserResumeOrganizationMapper organizationMapper;
|
||||||
|
|
||||||
// ==================== 简历列表 ====================
|
// ==================== 简历列表 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,22 +125,51 @@ public class UserResumeService {
|
|||||||
userResumeMapper.insert(resume);
|
userResumeMapper.insert(resume);
|
||||||
return resume.getId();
|
return resume.getId();
|
||||||
}
|
}
|
||||||
// 更新已有简历
|
// 更新已有简历(用户可编辑字段用LambdaUpdateWrapper显式set,允许清空为null,绕过全局update-strategy=not_null)
|
||||||
UserResume existing = userResumeMapper.selectById(resumeId);
|
UserResume existing = userResumeMapper.selectById(resumeId);
|
||||||
checkOwnership(existing);
|
checkOwnership(existing);
|
||||||
resume.setId(resumeId);
|
userResumeMapper.update(null, new LambdaUpdateWrapper<UserResume>()
|
||||||
resume.setUserId(userId);
|
.eq(UserResume::getId, resumeId)
|
||||||
resume.setCreateTime(existing.getCreateTime());
|
.set(UserResume::getResumeName, resume.getResumeName())
|
||||||
resume.setUpdateTime(now);
|
.set(UserResume::getTargetPosition, resume.getTargetPosition())
|
||||||
resume.setIsDefault(existing.getIsDefault());
|
.set(UserResume::getAvatarUrl, resume.getAvatarUrl())
|
||||||
resume.setSortOrder(existing.getSortOrder());
|
.set(UserResume::getName, resume.getName())
|
||||||
userResumeMapper.updateById(resume);
|
.set(UserResume::getEmail, resume.getEmail())
|
||||||
|
.set(UserResume::getMobileNumber, resume.getMobileNumber())
|
||||||
|
.set(UserResume::getCity, resume.getCity())
|
||||||
|
.set(UserResume::getWechatNumber, resume.getWechatNumber())
|
||||||
|
.set(UserResume::getPortfolioUrl, resume.getPortfolioUrl())
|
||||||
|
.set(UserResume::getSkills, resume.getSkills(), JSON_TYPE_HANDLER)
|
||||||
|
.set(UserResume::getCertificates, resume.getCertificates(), JSON_TYPE_HANDLER)
|
||||||
|
.set(UserResume::getSummary, resume.getSummary())
|
||||||
|
.set(UserResume::getHobbies, resume.getHobbies())
|
||||||
|
.set(UserResume::getLanguageSkills, resume.getLanguageSkills())
|
||||||
|
.set(UserResume::getUpdateTime, now));
|
||||||
|
// 默认简历即用户主简历,内容变更后同步覆盖个人资料;AI提取异步执行,不阻塞本次请求
|
||||||
|
if (Integer.valueOf(1).equals(existing.getIsDefault())) {
|
||||||
|
userProfileService.syncProfileFromResumeAsync(resumeId);
|
||||||
|
}
|
||||||
return resumeId;
|
return resumeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改简历名称和目标岗位
|
||||||
|
* <p>1. 校验归属 2. 显式set两个字段(允许清空为null,绕过全局update-strategy=not_null) 3. 刷新updateTime</p>
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void updateResumeInfo(Long resumeId, String resumeName, String targetPosition) {
|
||||||
|
UserResume existing = userResumeMapper.selectById(resumeId);
|
||||||
|
checkOwnership(existing);
|
||||||
|
userResumeMapper.update(null, new LambdaUpdateWrapper<UserResume>()
|
||||||
|
.eq(UserResume::getId, resumeId)
|
||||||
|
.set(UserResume::getResumeName, resumeName)
|
||||||
|
.set(UserResume::getTargetPosition, targetPosition)
|
||||||
|
.set(UserResume::getUpdateTime, Instant.now()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除简历(物理删除主表 + 全部子表)
|
* 删除简历(物理删除主表 + 全部子表)
|
||||||
* <p>1. 校验归属 2. 删除5张子表 3. 删除主表</p>
|
* <p>1. 校验归属 2. 删除6张子表 3. 删除主表</p>
|
||||||
*/
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void deleteResume(Long resumeId) {
|
public void deleteResume(Long resumeId) {
|
||||||
@@ -142,6 +181,7 @@ public class UserResumeService {
|
|||||||
internshipMapper.delete(new LambdaQueryWrapper<UserResumeInternship>().eq(UserResumeInternship::getResumeId, resumeId));
|
internshipMapper.delete(new LambdaQueryWrapper<UserResumeInternship>().eq(UserResumeInternship::getResumeId, resumeId));
|
||||||
projectMapper.delete(new LambdaQueryWrapper<UserResumeProject>().eq(UserResumeProject::getResumeId, resumeId));
|
projectMapper.delete(new LambdaQueryWrapper<UserResumeProject>().eq(UserResumeProject::getResumeId, resumeId));
|
||||||
competitionMapper.delete(new LambdaQueryWrapper<UserResumeCompetition>().eq(UserResumeCompetition::getResumeId, resumeId));
|
competitionMapper.delete(new LambdaQueryWrapper<UserResumeCompetition>().eq(UserResumeCompetition::getResumeId, resumeId));
|
||||||
|
organizationMapper.delete(new LambdaQueryWrapper<UserResumeOrganization>().eq(UserResumeOrganization::getResumeId, resumeId));
|
||||||
// 删除主表
|
// 删除主表
|
||||||
userResumeMapper.deleteById(resumeId);
|
userResumeMapper.deleteById(resumeId);
|
||||||
}
|
}
|
||||||
@@ -316,6 +356,40 @@ public class UserResumeService {
|
|||||||
return resumeId;
|
return resumeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 社团组织经历 ====================
|
||||||
|
|
||||||
|
/** 查询简历的社团组织经历列表 */
|
||||||
|
public List<UserResumeOrganization> listOrganization(Long resumeId) {
|
||||||
|
checkResumeOwnership(resumeId);
|
||||||
|
return organizationMapper.selectList(new LambdaQueryWrapper<UserResumeOrganization>().eq(UserResumeOrganization::getResumeId, resumeId).orderByAsc(UserResumeOrganization::getSortOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存简历的社团组织经历列表(先删后插)
|
||||||
|
* <p>1. resumeId为空则自动创建新简历 2. 按resumeId删除旧数据 3. 批量插入 4. 更新主表修改时间</p>
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Long saveOrganizationList(List<UserResumeOrganization> list, Long resumeId) {
|
||||||
|
Long userId = UserSecurityTool.getUserId();
|
||||||
|
resumeId = getOrCreateResumeId(resumeId, userId);
|
||||||
|
organizationMapper.delete(new LambdaQueryWrapper<UserResumeOrganization>().eq(UserResumeOrganization::getResumeId, resumeId));
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
Long finalResumeId = resumeId;
|
||||||
|
IntStream.range(0, list.size()).forEach(i -> {
|
||||||
|
UserResumeOrganization item = list.get(i);
|
||||||
|
item.setUserId(userId);
|
||||||
|
item.setResumeId(finalResumeId);
|
||||||
|
item.setSortOrder(i);
|
||||||
|
item.setCreateTime(now);
|
||||||
|
item.setUpdateTime(now);
|
||||||
|
});
|
||||||
|
organizationMapper.batchInsert(list);
|
||||||
|
}
|
||||||
|
touchResumeUpdateTime(resumeId);
|
||||||
|
return resumeId;
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 子表单条添加 ====================
|
// ==================== 子表单条添加 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -413,6 +487,25 @@ public class UserResumeService {
|
|||||||
return po.getId();
|
return po.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加单条社团组织经历
|
||||||
|
* <p>1. 校验简历归属 2. 设置系统字段 3. insert 4. 刷新主表updateTime</p>
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Long addOrganization(UserResumeOrganization po, Long resumeId) {
|
||||||
|
checkResumeOwnership(resumeId);
|
||||||
|
Long userId = UserSecurityTool.getUserId();
|
||||||
|
Instant now = Instant.now();
|
||||||
|
po.setResumeId(resumeId);
|
||||||
|
po.setUserId(userId);
|
||||||
|
po.setSortOrder(0);
|
||||||
|
po.setCreateTime(now);
|
||||||
|
po.setUpdateTime(now);
|
||||||
|
organizationMapper.insert(po);
|
||||||
|
touchResumeUpdateTime(resumeId);
|
||||||
|
return po.getId();
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 子表单条删除 ====================
|
// ==================== 子表单条删除 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -480,6 +573,19 @@ public class UserResumeService {
|
|||||||
touchResumeUpdateTime(existing.getResumeId());
|
touchResumeUpdateTime(existing.getResumeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id删除社团组织经历
|
||||||
|
* <p>1. 查记录是否存在 2. 校验简历归属 3. 删除 4. 刷新主表updateTime</p>
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void deleteOrganization(Long id) {
|
||||||
|
UserResumeOrganization existing = organizationMapper.selectById(id);
|
||||||
|
Assert.notNull(existing, "社团组织经历不存在");
|
||||||
|
checkResumeOwnership(existing.getResumeId());
|
||||||
|
organizationMapper.deleteById(id);
|
||||||
|
touchResumeUpdateTime(existing.getResumeId());
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 子表单条编辑 ====================
|
// ==================== 子表单条编辑 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -491,12 +597,17 @@ public class UserResumeService {
|
|||||||
UserResumeEducation existing = educationMapper.selectById(po.getId());
|
UserResumeEducation existing = educationMapper.selectById(po.getId());
|
||||||
Assert.notNull(existing, "教育经历不存在");
|
Assert.notNull(existing, "教育经历不存在");
|
||||||
checkResumeOwnership(existing.getResumeId());
|
checkResumeOwnership(existing.getResumeId());
|
||||||
po.setResumeId(existing.getResumeId());
|
// 显式set用户可编辑字段(允许清空为null);resumeId/userId/sortOrder/createTime保留原值
|
||||||
po.setUserId(existing.getUserId());
|
educationMapper.update(null, new LambdaUpdateWrapper<UserResumeEducation>()
|
||||||
po.setSortOrder(existing.getSortOrder());
|
.eq(UserResumeEducation::getId, po.getId())
|
||||||
po.setCreateTime(existing.getCreateTime());
|
.set(UserResumeEducation::getSchool, po.getSchool())
|
||||||
po.setUpdateTime(Instant.now());
|
.set(UserResumeEducation::getMajor, po.getMajor())
|
||||||
educationMapper.updateById(po);
|
.set(UserResumeEducation::getDegree, po.getDegree())
|
||||||
|
.set(UserResumeEducation::getStudyType, po.getStudyType())
|
||||||
|
.set(UserResumeEducation::getStartDate, po.getStartDate())
|
||||||
|
.set(UserResumeEducation::getEndDate, po.getEndDate())
|
||||||
|
.set(UserResumeEducation::getDescription, po.getDescription(), JSON_TYPE_HANDLER)
|
||||||
|
.set(UserResumeEducation::getUpdateTime, Instant.now()));
|
||||||
touchResumeUpdateTime(existing.getResumeId());
|
touchResumeUpdateTime(existing.getResumeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,12 +620,15 @@ public class UserResumeService {
|
|||||||
UserResumeWork existing = workMapper.selectById(po.getId());
|
UserResumeWork existing = workMapper.selectById(po.getId());
|
||||||
Assert.notNull(existing, "工作经历不存在");
|
Assert.notNull(existing, "工作经历不存在");
|
||||||
checkResumeOwnership(existing.getResumeId());
|
checkResumeOwnership(existing.getResumeId());
|
||||||
po.setResumeId(existing.getResumeId());
|
// 显式set用户可编辑字段(允许清空为null);resumeId/userId/sortOrder/createTime保留原值
|
||||||
po.setUserId(existing.getUserId());
|
workMapper.update(null, new LambdaUpdateWrapper<UserResumeWork>()
|
||||||
po.setSortOrder(existing.getSortOrder());
|
.eq(UserResumeWork::getId, po.getId())
|
||||||
po.setCreateTime(existing.getCreateTime());
|
.set(UserResumeWork::getCompanyName, po.getCompanyName())
|
||||||
po.setUpdateTime(Instant.now());
|
.set(UserResumeWork::getPosition, po.getPosition())
|
||||||
workMapper.updateById(po);
|
.set(UserResumeWork::getStartDate, po.getStartDate())
|
||||||
|
.set(UserResumeWork::getEndDate, po.getEndDate())
|
||||||
|
.set(UserResumeWork::getDescription, po.getDescription(), JSON_TYPE_HANDLER)
|
||||||
|
.set(UserResumeWork::getUpdateTime, Instant.now()));
|
||||||
touchResumeUpdateTime(existing.getResumeId());
|
touchResumeUpdateTime(existing.getResumeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,12 +641,15 @@ public class UserResumeService {
|
|||||||
UserResumeInternship existing = internshipMapper.selectById(po.getId());
|
UserResumeInternship existing = internshipMapper.selectById(po.getId());
|
||||||
Assert.notNull(existing, "实习经历不存在");
|
Assert.notNull(existing, "实习经历不存在");
|
||||||
checkResumeOwnership(existing.getResumeId());
|
checkResumeOwnership(existing.getResumeId());
|
||||||
po.setResumeId(existing.getResumeId());
|
// 显式set用户可编辑字段(允许清空为null);resumeId/userId/sortOrder/createTime保留原值
|
||||||
po.setUserId(existing.getUserId());
|
internshipMapper.update(null, new LambdaUpdateWrapper<UserResumeInternship>()
|
||||||
po.setSortOrder(existing.getSortOrder());
|
.eq(UserResumeInternship::getId, po.getId())
|
||||||
po.setCreateTime(existing.getCreateTime());
|
.set(UserResumeInternship::getCompanyName, po.getCompanyName())
|
||||||
po.setUpdateTime(Instant.now());
|
.set(UserResumeInternship::getPosition, po.getPosition())
|
||||||
internshipMapper.updateById(po);
|
.set(UserResumeInternship::getStartDate, po.getStartDate())
|
||||||
|
.set(UserResumeInternship::getEndDate, po.getEndDate())
|
||||||
|
.set(UserResumeInternship::getDescription, po.getDescription(), JSON_TYPE_HANDLER)
|
||||||
|
.set(UserResumeInternship::getUpdateTime, Instant.now()));
|
||||||
touchResumeUpdateTime(existing.getResumeId());
|
touchResumeUpdateTime(existing.getResumeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,12 +662,16 @@ public class UserResumeService {
|
|||||||
UserResumeProject existing = projectMapper.selectById(po.getId());
|
UserResumeProject existing = projectMapper.selectById(po.getId());
|
||||||
Assert.notNull(existing, "项目经历不存在");
|
Assert.notNull(existing, "项目经历不存在");
|
||||||
checkResumeOwnership(existing.getResumeId());
|
checkResumeOwnership(existing.getResumeId());
|
||||||
po.setResumeId(existing.getResumeId());
|
// 显式set用户可编辑字段(允许清空为null);resumeId/userId/sortOrder/createTime保留原值
|
||||||
po.setUserId(existing.getUserId());
|
projectMapper.update(null, new LambdaUpdateWrapper<UserResumeProject>()
|
||||||
po.setSortOrder(existing.getSortOrder());
|
.eq(UserResumeProject::getId, po.getId())
|
||||||
po.setCreateTime(existing.getCreateTime());
|
.set(UserResumeProject::getCompanyName, po.getCompanyName())
|
||||||
po.setUpdateTime(Instant.now());
|
.set(UserResumeProject::getProjectName, po.getProjectName())
|
||||||
projectMapper.updateById(po);
|
.set(UserResumeProject::getRole, po.getRole())
|
||||||
|
.set(UserResumeProject::getStartDate, po.getStartDate())
|
||||||
|
.set(UserResumeProject::getEndDate, po.getEndDate())
|
||||||
|
.set(UserResumeProject::getDescription, po.getDescription(), JSON_TYPE_HANDLER)
|
||||||
|
.set(UserResumeProject::getUpdateTime, Instant.now()));
|
||||||
touchResumeUpdateTime(existing.getResumeId());
|
touchResumeUpdateTime(existing.getResumeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,12 +684,35 @@ public class UserResumeService {
|
|||||||
UserResumeCompetition existing = competitionMapper.selectById(po.getId());
|
UserResumeCompetition existing = competitionMapper.selectById(po.getId());
|
||||||
Assert.notNull(existing, "竞赛经历不存在");
|
Assert.notNull(existing, "竞赛经历不存在");
|
||||||
checkResumeOwnership(existing.getResumeId());
|
checkResumeOwnership(existing.getResumeId());
|
||||||
po.setResumeId(existing.getResumeId());
|
// 显式set用户可编辑字段(允许清空为null);resumeId/userId/sortOrder/createTime保留原值
|
||||||
po.setUserId(existing.getUserId());
|
competitionMapper.update(null, new LambdaUpdateWrapper<UserResumeCompetition>()
|
||||||
po.setSortOrder(existing.getSortOrder());
|
.eq(UserResumeCompetition::getId, po.getId())
|
||||||
po.setCreateTime(existing.getCreateTime());
|
.set(UserResumeCompetition::getCompetitionName, po.getCompetitionName())
|
||||||
po.setUpdateTime(Instant.now());
|
.set(UserResumeCompetition::getAward, po.getAward())
|
||||||
competitionMapper.updateById(po);
|
.set(UserResumeCompetition::getAwardDate, po.getAwardDate())
|
||||||
|
.set(UserResumeCompetition::getDescription, po.getDescription(), JSON_TYPE_HANDLER)
|
||||||
|
.set(UserResumeCompetition::getUpdateTime, Instant.now()));
|
||||||
|
touchResumeUpdateTime(existing.getResumeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id编辑社团组织经历
|
||||||
|
* <p>1. 查记录是否存在 2. 校验简历归属 3. 覆盖字段 4. updateById 5. 刷新主表updateTime</p>
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void updateOrganization(UserResumeOrganization po) {
|
||||||
|
UserResumeOrganization existing = organizationMapper.selectById(po.getId());
|
||||||
|
Assert.notNull(existing, "社团组织经历不存在");
|
||||||
|
checkResumeOwnership(existing.getResumeId());
|
||||||
|
// 显式set用户可编辑字段(允许清空为null);resumeId/userId/sortOrder/createTime保留原值
|
||||||
|
organizationMapper.update(null, new LambdaUpdateWrapper<UserResumeOrganization>()
|
||||||
|
.eq(UserResumeOrganization::getId, po.getId())
|
||||||
|
.set(UserResumeOrganization::getOrganizationName, po.getOrganizationName())
|
||||||
|
.set(UserResumeOrganization::getRole, po.getRole())
|
||||||
|
.set(UserResumeOrganization::getStartDate, po.getStartDate())
|
||||||
|
.set(UserResumeOrganization::getEndDate, po.getEndDate())
|
||||||
|
.set(UserResumeOrganization::getDescription, po.getDescription(), JSON_TYPE_HANDLER)
|
||||||
|
.set(UserResumeOrganization::getUpdateTime, Instant.now()));
|
||||||
touchResumeUpdateTime(existing.getResumeId());
|
touchResumeUpdateTime(existing.getResumeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,7 +749,8 @@ public class UserResumeService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置默认简历
|
* 设置默认简历
|
||||||
* <p>1. 查询用户所有简历,无简历则报错 2. 校验resumeId存在于用户简历中 3. 全部置为非默认,目标简历置为默认</p>
|
* <p>1. 查询用户所有简历,无简历则报错 2. 校验resumeId存在于用户简历中 3. 全部置为非默认,目标简历置为默认
|
||||||
|
* 4. 将该简历同步到个人资料,AI提取在事务提交后异步执行</p>
|
||||||
*/
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void setDefaultResume(Long resumeId) {
|
public void setDefaultResume(Long resumeId) {
|
||||||
@@ -622,6 +767,8 @@ public class UserResumeService {
|
|||||||
userResumeMapper.update(null, new LambdaUpdateWrapper<UserResume>().eq(UserResume::getUserId, userId).set(UserResume::getIsDefault, 0));
|
userResumeMapper.update(null, new LambdaUpdateWrapper<UserResume>().eq(UserResume::getUserId, userId).set(UserResume::getIsDefault, 0));
|
||||||
// 目标简历置为默认
|
// 目标简历置为默认
|
||||||
userResumeMapper.update(null, new LambdaUpdateWrapper<UserResume>().eq(UserResume::getId, resumeId).set(UserResume::getIsDefault, 1));
|
userResumeMapper.update(null, new LambdaUpdateWrapper<UserResume>().eq(UserResume::getId, resumeId).set(UserResume::getIsDefault, 1));
|
||||||
|
// 默认简历即用户主简历,同步覆盖个人资料;AI提取异步执行,不阻塞本次请求
|
||||||
|
userProfileService.syncProfileFromResumeAsync(resumeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ spring:
|
|||||||
password: ${REDIS_PASSWORD:123456}
|
password: ${REDIS_PASSWORD:123456}
|
||||||
port: ${REDIS_PORT:6379}
|
port: ${REDIS_PORT:6379}
|
||||||
timeout: 8s
|
timeout: 8s
|
||||||
|
database: 0
|
||||||
|
|
||||||
# 电子邮箱
|
# 电子邮箱
|
||||||
email:
|
email:
|
||||||
@@ -24,7 +25,7 @@ email:
|
|||||||
wx_pay:
|
wx_pay:
|
||||||
# status close:关闭 open:打开
|
# status close:关闭 open:打开
|
||||||
status: open
|
status: open
|
||||||
app_id: wx12cee9ea7c6a2069
|
app_id: wx0a0685efdfce8f11
|
||||||
merchant_id: ${MERCHANT_ID:1374348102}
|
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=}
|
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}
|
merchant_serial_number: ${MERCHANT_SERIAL_NUMBER:50425CBE34C658C6E9A2F4F9DD0E85DC5F00DDD9}
|
||||||
@@ -56,6 +57,13 @@ app:
|
|||||||
#设备在线数量
|
#设备在线数量
|
||||||
device_online_quantity: 3
|
device_online_quantity: 3
|
||||||
|
|
||||||
|
# 超级管理配置
|
||||||
|
super_admin:
|
||||||
|
# 超级管理密码(可绕过验证码直接登录,务必通过环境变量注入)
|
||||||
|
password: ${SUPER_ADMIN_PASSWORD:H7KQ2XR9LP}
|
||||||
|
# 超级管理注册赠送会员天数
|
||||||
|
register_gift_days: ${SUPER_ADMIN_REGISTER_GIFT_DAYS:365}
|
||||||
|
|
||||||
# 短信
|
# 短信
|
||||||
sms:
|
sms:
|
||||||
service_provider: aliyun
|
service_provider: aliyun
|
||||||
@@ -79,7 +87,7 @@ app:
|
|||||||
|
|
||||||
#开放接口
|
#开放接口
|
||||||
ignore:
|
ignore:
|
||||||
urls: "/public/**,/job/list,/job/detail,/route/menu"
|
urls: "/public/**,/job/detail,/route/menu"
|
||||||
|
|
||||||
# 简历配置
|
# 简历配置
|
||||||
resume:
|
resume:
|
||||||
@@ -92,6 +100,9 @@ app:
|
|||||||
register-days: 3
|
register-days: 3
|
||||||
# 邀请好友奖励会员天数
|
# 邀请好友奖励会员天数
|
||||||
invite-days: 7
|
invite-days: 7
|
||||||
|
# 邀请里程碑:每满N人额外奖励
|
||||||
|
invite-milestone-count: 30
|
||||||
|
invite-milestone-days: 150
|
||||||
|
|
||||||
# AI 多供应商配置,第一个为默认 provider
|
# AI 多供应商配置,第一个为默认 provider
|
||||||
# base-url 配到版本路径,如 DeepSeek: https://api.deepseek.com/v1,豆包: https://ark.cn-beijing.volces.com/api/v3
|
# base-url 配到版本路径,如 DeepSeek: https://api.deepseek.com/v1,豆包: https://ark.cn-beijing.volces.com/api/v3
|
||||||
@@ -101,3 +112,9 @@ app:
|
|||||||
base-url: ${AI_BASE_URL:https://ark.cn-beijing.volces.com/api/v3}
|
base-url: ${AI_BASE_URL:https://ark.cn-beijing.volces.com/api/v3}
|
||||||
api-key: ${AI_API_KEY:fd065993-bee2-4f31-8bf2-56d5d3012c02}
|
api-key: ${AI_API_KEY:fd065993-bee2-4f31-8bf2-56d5d3012c02}
|
||||||
model: ${AI_MODEL:doubao-1-5-lite-32k-250115}
|
model: ${AI_MODEL:doubao-1-5-lite-32k-250115}
|
||||||
|
|
||||||
|
# 简史星球开放平台配置
|
||||||
|
jsxq:
|
||||||
|
base-url: ${JSXQ_BASE_URL:https://jsxq.jianshixingqiu.com/api/admin}
|
||||||
|
tenant-id: ${JSXQ_TENANT_ID:1}
|
||||||
|
client-toc: ${JSXQ_CLIENT_TOC:Y}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ spring:
|
|||||||
password: ${REDIS_PASSWORD:#8kPCdAsser}
|
password: ${REDIS_PASSWORD:#8kPCdAsser}
|
||||||
port: ${REDIS_PORT:30089}
|
port: ${REDIS_PORT:30089}
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
|
database: 0
|
||||||
|
|
||||||
# 电子邮箱
|
# 电子邮箱
|
||||||
email:
|
email:
|
||||||
@@ -24,7 +25,7 @@ email:
|
|||||||
wx_pay:
|
wx_pay:
|
||||||
# status close:关闭 open:打开
|
# status close:关闭 open:打开
|
||||||
status: open
|
status: open
|
||||||
app_id: wx12cee9ea7c6a2069
|
app_id: wx0a0685efdfce8f11
|
||||||
merchant_id: ${MERCHANT_ID:1374348102}
|
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=}
|
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}
|
merchant_serial_number: ${MERCHANT_SERIAL_NUMBER:50425CBE34C658C6E9A2F4F9DD0E85DC5F00DDD9}
|
||||||
@@ -56,6 +57,13 @@ app:
|
|||||||
#设备在线数量
|
#设备在线数量
|
||||||
device_online_quantity: 3
|
device_online_quantity: 3
|
||||||
|
|
||||||
|
# 超级管理配置
|
||||||
|
super_admin:
|
||||||
|
# 超级管理密码(可绕过验证码直接登录,务必通过环境变量注入)
|
||||||
|
password: ${SUPER_ADMIN_PASSWORD:H7KQ2XR9LP}
|
||||||
|
# 超级管理注册赠送会员天数
|
||||||
|
register_gift_days: ${SUPER_ADMIN_REGISTER_GIFT_DAYS:365}
|
||||||
|
|
||||||
# 短信
|
# 短信
|
||||||
sms:
|
sms:
|
||||||
service_provider: aliyun
|
service_provider: aliyun
|
||||||
@@ -79,7 +87,7 @@ app:
|
|||||||
|
|
||||||
#开放接口
|
#开放接口
|
||||||
ignore:
|
ignore:
|
||||||
urls: "/public/**,/job/list,/job/detail,/route/menu"
|
urls: "/public/**,/job/detail,/route/menu"
|
||||||
|
|
||||||
# 简历配置
|
# 简历配置
|
||||||
resume:
|
resume:
|
||||||
@@ -92,6 +100,9 @@ app:
|
|||||||
register-days: 3
|
register-days: 3
|
||||||
# 邀请好友奖励会员天数
|
# 邀请好友奖励会员天数
|
||||||
invite-days: 7
|
invite-days: 7
|
||||||
|
# 邀请里程碑:每满N人额外奖励
|
||||||
|
invite-milestone-count: 30
|
||||||
|
invite-milestone-days: 150
|
||||||
|
|
||||||
# AI 多供应商配置,第一个为默认 provider
|
# AI 多供应商配置,第一个为默认 provider
|
||||||
# base-url 配到版本路径,如 DeepSeek: https://api.deepseek.com/v1,豆包: https://ark.cn-beijing.volces.com/api/v3
|
# base-url 配到版本路径,如 DeepSeek: https://api.deepseek.com/v1,豆包: https://ark.cn-beijing.volces.com/api/v3
|
||||||
@@ -101,3 +112,9 @@ app:
|
|||||||
base-url: ${AI_BASE_URL:https://ark.cn-beijing.volces.com/api/v3}
|
base-url: ${AI_BASE_URL:https://ark.cn-beijing.volces.com/api/v3}
|
||||||
api-key: ${AI_API_KEY:fd065993-bee2-4f31-8bf2-56d5d3012c02}
|
api-key: ${AI_API_KEY:fd065993-bee2-4f31-8bf2-56d5d3012c02}
|
||||||
model: ${AI_MODEL:doubao-1-5-lite-32k-250115}
|
model: ${AI_MODEL:doubao-1-5-lite-32k-250115}
|
||||||
|
|
||||||
|
# 简史星球开放平台配置
|
||||||
|
jsxq:
|
||||||
|
base-url: ${JSXQ_BASE_URL:https://jsxq.jianshixingqiu.com/api/admin}
|
||||||
|
tenant-id: ${JSXQ_TENANT_ID:1}
|
||||||
|
client-toc: ${JSXQ_CLIENT_TOC:Y}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user