Ver código fonte

Merge branch 'dev/xmy' of http://8.152.4.3:3000/yp/sj-backend into dev/xmy

西格玛许 6 dias atrás
pai
commit
ec8d35dd25
34 arquivos alterados com 1636 adições e 4 exclusões
  1. 16 0
      ruoyi-admin/src/main/resources/application-dev.yml
  2. 6 0
      ruoyi-admin/src/main/resources/application-prod.yml
  3. 2 2
      ruoyi-admin/src/main/resources/application.yml
  4. 21 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/config/TencentMapGeoProperties.java
  5. 48 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/config/WithdrawConfig.java
  6. 181 1
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/MainTrainingController.java
  7. 11 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalAuthController.java
  8. 4 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainCompanyApply.java
  9. 2 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainPosition.java
  10. 2 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainPostApply.java
  11. 140 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainTraining.java
  12. 68 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainTrainingVideo.java
  13. 3 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/MainCompanyApplyBo.java
  14. 2 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/MainPositionBo.java
  15. 8 1
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/MainPostApplyBo.java
  16. 148 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/MainTrainingBo.java
  17. 15 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/dto/GeoPointDto.java
  18. 7 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainCompanyApplyVo.java
  19. 6 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainPositionVo.java
  20. 6 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainPostApplyVo.java
  21. 49 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainTrainingLearnRecordVo.java
  22. 40 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainTrainingParticipantVo.java
  23. 61 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainTrainingVideoVo.java
  24. 163 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainTrainingVo.java
  25. 120 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/MainTrainingMapper.java
  26. 8 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/MainTrainingVideoMapper.java
  27. 8 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/CompanyGeoService.java
  28. 89 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IMainTrainingService.java
  29. 2 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainAuditServiceImpl.java
  30. 12 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainCompanyApplyServiceImpl.java
  31. 243 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainTrainingServiceImpl.java
  32. 69 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/TencentMapCompanyGeoServiceImpl.java
  33. 72 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/WithdrawServiceImpl.java
  34. 4 0
      ruoyi-modules/ruoyi-main/src/main/resources/mapper/MainAuditMapper.xml

+ 16 - 0
ruoyi-admin/src/main/resources/application-dev.yml

@@ -279,3 +279,19 @@ justauth:
       client-id: 10**********6
       client-secret: 1f7d08**********5b7**********29e
       redirect-uri: ${justauth.address}/social-callback?source=gitea
+
+tencent:
+  map:
+    geo:
+      # 企业入驻办公地址转经纬度所需的腾讯位置服务 WebService Key
+      key: "LA5BZ-BDQL7-NHIXK-P1H5G-EIVSH-GPBQY"
+
+manage:
+  jumpTo: http://localhost:90/
+
+withdraw:
+  auditMode: auto
+  autoAuditRule:
+    maxAmount: 10000
+    maxDailyCount: 10
+    maxDailyAmount: 50000

+ 6 - 0
ruoyi-admin/src/main/resources/application-prod.yml

@@ -270,3 +270,9 @@ justauth:
       client-id: 10**********6
       client-secret: 1f7d08**********5b7**********29e
       redirect-uri: ${justauth.address}/social-callback?source=gitea
+
+tencent:
+  map:
+    geo:
+      # 企业入驻办公地址转经纬度所需的腾讯位置服务 WebService Key
+      key: "LA5BZ-BDQL7-NHIXK-P1H5G-EIVSH-GPBQY"

+ 2 - 2
ruoyi-admin/src/main/resources/application.yml

@@ -74,9 +74,9 @@ spring:
   servlet:
     multipart:
       # 单个文件大小
-      max-file-size: 20MB
+      max-file-size: 10GB
       # 设置总上传的文件大小
-      max-request-size: 50MB
+      max-request-size: 10GB
   mvc:
     # 设置静态资源路径 防止所有请求都去查静态资源
     static-path-pattern: /static/**

+ 21 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/config/TencentMapGeoProperties.java

@@ -0,0 +1,21 @@
+package org.dromara.main.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "tencent.map.geo")
+public class TencentMapGeoProperties {
+
+    /**
+     * 腾讯位置服务 WebService Key
+     */
+    private String key;
+
+    /**
+     * 地址解析接口地址
+     */
+    private String geocodeUrl = "https://apis.map.qq.com/ws/geocoder/v1/";
+}

+ 48 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/config/WithdrawConfig.java

@@ -0,0 +1,48 @@
+package org.dromara.main.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+
+/**
+ * 提现配置
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "withdraw")
+public class WithdrawConfig {
+
+    /**
+     * 审核模式:auto=自动审核,manual=人工审核
+     */
+    private String auditMode = "manual";
+
+    /**
+     * 自动审核规则
+     */
+    private AutoAuditRule autoAuditRule = new AutoAuditRule();
+
+    @Data
+    public static class AutoAuditRule {
+        /**
+         * 单笔最大自动审核金额
+         */
+        private BigDecimal maxAmount = new BigDecimal("10000");
+
+        /**
+         * 单日最大提现次数
+         */
+        private Integer maxDailyCount = 10;
+
+        /**
+         * 单日最大提现总额
+         */
+        private BigDecimal maxDailyAmount = new BigDecimal("50000");
+    }
+
+    public boolean isAutoAudit() {
+        return "auto".equalsIgnoreCase(auditMode);
+    }
+}

+ 181 - 1
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/MainTrainingController.java

@@ -1,4 +1,184 @@
 package org.dromara.main.controller;
 
-public class MainTrainingController {
+import cn.hutool.core.util.ObjectUtil;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.main.domain.bo.MainTrainingBo;
+import org.dromara.main.domain.vo.MainTrainingLearnRecordVo;
+import org.dromara.main.domain.vo.MainTrainingParticipantVo;
+import org.dromara.main.domain.vo.MainTrainingVo;
+import org.dromara.main.service.IMainTrainingService;
+import org.dromara.system.domain.vo.SysOssUploadVo;
+import org.dromara.system.domain.vo.SysOssVo;
+import org.dromara.system.service.ISysOssService;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.List;
+
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/main/training")
+public class MainTrainingController extends BaseController {
+
+    private final IMainTrainingService mainTrainingService;
+    private final ISysOssService ossService;
+
+    /**
+     * 查询培训列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo<MainTrainingVo> list(MainTrainingBo bo, PageQuery pageQuery) {
+        return mainTrainingService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 查询线下培训列表
+     */
+    @GetMapping("/offline/list")
+    public TableDataInfo<MainTrainingVo> offlineList(MainTrainingBo bo, PageQuery pageQuery) {
+        return mainTrainingService.queryOfflinePageList(bo, pageQuery);
+    }
+
+    /**
+     * 获取培训详细信息
+     *
+     * @param id 主键
+     */
+    @GetMapping("/{id}")
+    public R<MainTrainingVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        return R.ok(mainTrainingService.queryById(id));
+    }
+
+    /**
+     * 获取线下培训详细信息
+     */
+    @GetMapping("/offline/{id}")
+    public R<MainTrainingVo> getOfflineInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        MainTrainingVo vo = mainTrainingService.queryOfflineById(id);
+        return vo == null ? R.fail("线下培训不存在") : R.ok(vo);
+    }
+
+    /**
+     * 查询视频培训学习记录分页
+     */
+    @GetMapping("/{id}/learn-records")
+    public TableDataInfo<MainTrainingLearnRecordVo> learnRecords(@NotNull(message = "主键不能为空") @PathVariable Long id, PageQuery pageQuery) {
+        return mainTrainingService.queryLearnRecordPage(id, pageQuery);
+    }
+
+    /**
+     * 查询线下培训参与人分页
+     */
+    @GetMapping("/offline/{id}/participants")
+    public TableDataInfo<MainTrainingParticipantVo> offlineParticipants(@NotNull(message = "主键不能为空") @PathVariable Long id, PageQuery pageQuery) {
+        return mainTrainingService.queryOfflineParticipantPage(id, pageQuery);
+    }
+
+    /**
+     * 导出视频培训学习记录
+     */
+    @Log(title = "培训管理", businessType = BusinessType.EXPORT)
+    @PostMapping("/{id}/learn-records/export")
+    public void exportLearnRecords(@NotNull(message = "主键不能为空") @PathVariable Long id, @RequestBody List<Long> studentIds, HttpServletResponse response) {
+        List<MainTrainingLearnRecordVo> list = mainTrainingService.queryLearnRecordExportList(id, studentIds);
+        ExcelUtil.exportExcel(list, "学习记录", MainTrainingLearnRecordVo.class, response);
+    }
+
+    /**
+     * 导出线下培训参与人
+     */
+    @Log(title = "培训管理", businessType = BusinessType.EXPORT)
+    @PostMapping("/offline/{id}/participants/export")
+    public void exportOfflineParticipants(@NotNull(message = "主键不能为空") @PathVariable Long id, @RequestBody List<Long> studentIds, HttpServletResponse response) {
+        List<MainTrainingParticipantVo> list = mainTrainingService.queryOfflineParticipantExportList(id, studentIds);
+        ExcelUtil.exportExcel(list, "线下培训参与人", MainTrainingParticipantVo.class, response);
+    }
+
+    /**
+     * 新增培训
+     */
+    @Log(title = "培训管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@RequestBody MainTrainingBo bo) {
+        return toAjax(mainTrainingService.insertByBo(bo));
+    }
+
+    /**
+     * 培训文件上传
+     */
+    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public R<SysOssUploadVo> upload(@RequestPart("file") MultipartFile file) {
+        if (ObjectUtil.isNull(file)) {
+            return R.fail("上传文件不能为空");
+        }
+        SysOssVo oss = ossService.upload(file);
+        SysOssUploadVo uploadVo = new SysOssUploadVo();
+        uploadVo.setUrl(oss.getUrl());
+        uploadVo.setFileName(oss.getOriginalName());
+        uploadVo.setOssId(oss.getOssId().toString());
+        return R.ok(uploadVo);
+    }
+
+    /**
+     * 修改培训
+     */
+    @Log(title = "培训管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@RequestBody MainTrainingBo bo) {
+        return toAjax(mainTrainingService.updateByBo(bo));
+    }
+
+    /**
+     * 删除培训
+     *
+     * @param ids 主键串
+     */
+    @Log(title = "培训管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
+        return toAjax(mainTrainingService.deleteWithValidByIds(List.of(ids), true));
+    }
+
+    /**
+     * 删除线下培训
+     */
+    @Log(title = "培训管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/offline/{id}")
+    public R<Void> removeOffline(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        return toAjax(mainTrainingService.deleteOfflineById(id));
+    }
+
+    /**
+     * 更新培训状态(上架/下架)
+     */
+    @Log(title = "培训管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/updateStatus")
+    public R<Void> updateStatus(@RequestBody MainTrainingBo bo) {
+        return toAjax(mainTrainingService.updateStatus(bo.getId(), bo.getStatus()));
+    }
+
+    /**
+     * 更新线下培训状态(上架/下架)
+     */
+    @Log(title = "培训管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/offline/updateStatus")
+    public R<Void> updateOfflineStatus(@RequestBody MainTrainingBo bo) {
+        return toAjax(mainTrainingService.updateOfflineStatus(bo.getId(), bo.getStatus()));
+    }
 }

+ 11 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalAuthController.java

@@ -84,6 +84,9 @@ public class PortalAuthController extends BaseController {
     @Value("${spring.profiles.active:}")
     private String activeProfile;
 
+    @Value("${manage.jumpTo:}")
+    private String manageJumpTo;
+
     /**
      * 发送短信验证码
      *
@@ -220,6 +223,14 @@ public class PortalAuthController extends BaseController {
         return R.ok(result);
     }
 
+    @SaIgnore
+    @GetMapping("/publicConfig")
+    public R<Map<String, String>> getPublicConfig() {
+        Map<String, String> result = new LinkedHashMap<>(1);
+        result.put("manageJumpTo", manageJumpTo);
+        return R.ok(result);
+    }
+
     /**
      * 退出登录
      */

+ 4 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainCompanyApply.java

@@ -7,6 +7,8 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import org.dromara.common.mybatis.core.domain.BaseEntity;
 
+import java.math.BigDecimal;
+
 @Data
 @EqualsAndHashCode(callSuper = true)
 @TableName("main_company_apply")
@@ -27,6 +29,8 @@ public class MainCompanyApply extends BaseEntity {
     private String surname;
     private String name;
     private String mobile;
+    private BigDecimal latitude;
+    private BigDecimal longitude;
     private Integer applyStatus;
     private Long auditId;
     private String tenantId;

+ 2 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainPosition.java

@@ -32,6 +32,8 @@ public class MainPosition extends BaseEntity {
     private String educationRequirement;
     private String salaryType;
     private String salaryRange;
+    private BigDecimal minSalary;
+    private BigDecimal maxSalary;
     private Integer recruitNum;
     private Date registrationStartDate;
     private Date registrationEndDate;

+ 2 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainPostApply.java

@@ -33,6 +33,8 @@ public class MainPostApply extends BaseEntity {
     private String educationRequirement;
     private String salaryType;
     private String salaryRange;
+    private BigDecimal minSalary;
+    private BigDecimal maxSalary;
     private Integer recruitNum;
     private Date registrationStartDate;
     private Date registrationEndDate;

+ 140 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainTraining.java

@@ -0,0 +1,140 @@
+package org.dromara.main.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.util.Date;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("main_training")
+public class MainTraining extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 培训ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 培训类型: video=视频培训, offline=线下培训, live=直播培训
+     */
+    private String trainingType;
+
+    /**
+     * 培训名称
+     */
+    private String name;
+
+    /**
+     * 培训描述
+     */
+    private String description;
+
+    /**
+     * 封面图ID(关联 sys_oss.oss_id)
+     */
+    private Long thumbnail;
+
+    /**
+     * 岗位类型(全职/兼职/实习)
+     */
+    private String jobType;
+
+    /**
+     * 岗位等级(A1/A2/B1/B2)
+     */
+    private String jobLevel;
+
+    /**
+     * 岗位名称
+     */
+    private String job;
+
+    /**
+     * 排序号
+     */
+    private Integer sortOrder;
+
+    /**
+     * 状态: 0=下架, 1=上架
+     */
+    private Integer status;
+
+    /**
+     * 总时长
+     */
+    private String duration;
+
+    /**
+     * 上架时间
+     */
+    private Date publishTime;
+
+    /**
+     * 培训城市
+     */
+    private String city;
+
+    /**
+     * 培训区域
+     */
+    private String area;
+
+    /**
+     * 详细地址
+     */
+    private String addressDetail;
+
+    /**
+     * 培训开始时间
+     */
+    private Date trainingStartTime;
+
+    /**
+     * 培训结束时间
+     */
+    private Date trainingEndTime;
+
+    /**
+     * 报名开始时间
+     */
+    private Date applyStartTime;
+
+    /**
+     * 报名结束时间
+     */
+    private Date applyEndTime;
+
+    /**
+     * 主办单位
+     */
+    private String organizer;
+
+    /**
+     * 标签(逗号分隔)
+     */
+    private String tags;
+
+    /**
+     * 租户编号
+     */
+    private String tenantId;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 删除标志(0代表存在 1代表删除)
+     */
+    @TableLogic
+    private String delFlag;
+}

+ 68 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainTrainingVideo.java

@@ -0,0 +1,68 @@
+package org.dromara.main.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("main_training_video")
+public class MainTrainingVideo extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 视频ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 关联培训ID
+     */
+    private Long trainingId;
+
+    /**
+     * 视频名称
+     */
+    private String name;
+
+    /**
+     * 视频文件ID(关联 sys_oss.oss_id)
+     */
+    private Long ossId;
+
+    /**
+     * 视频文件URL
+     */
+    private String fileUrl;
+
+    /**
+     * 文件大小
+     */
+    private String fileSize;
+
+    /**
+     * 文件格式
+     */
+    private String fileType;
+
+    /**
+     * 视频时长
+     */
+    private String duration;
+
+    /**
+     * 排序
+     */
+    private Integer sortOrder;
+
+    /**
+     * 删除标志(0代表存在 1代表删除)
+     */
+    @TableLogic
+    private String delFlag;
+}

+ 3 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/MainCompanyApplyBo.java

@@ -13,6 +13,7 @@ import org.dromara.common.mybatis.core.domain.BaseEntity;
 import org.dromara.main.domain.MainCompanyApply;
 
 import java.io.Serial;
+import java.math.BigDecimal;
 
 @Data
 @EqualsAndHashCode(callSuper = true)
@@ -56,6 +57,8 @@ public class MainCompanyApplyBo extends BaseEntity {
     @NotBlank(message = "手机号不能为空", groups = {AddGroup.class, EditGroup.class})
     @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
     private String mobile;
+    private BigDecimal latitude;
+    private BigDecimal longitude;
     private Integer applyStatus;
     @NotBlank(message = "验证码不能为空", groups = {AddGroup.class})
     private String smsCode;

+ 2 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/MainPositionBo.java

@@ -37,6 +37,8 @@ public class MainPositionBo implements Serializable {
     private String educationRequirement;
     private String salaryType;
     private String salaryRange;
+    private BigDecimal minSalary;
+    private BigDecimal maxSalary;
     private Integer recruitNum;
     private Date registrationStartDate;
     private Date registrationEndDate;

+ 8 - 1
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/MainPostApplyBo.java

@@ -9,7 +9,12 @@ import org.dromara.main.domain.MainPostApply;
 import java.math.BigDecimal;
 import java.util.Date;
 
-
+/**
+ * 岗位申请BO
+ *
+ * @author [Your Name]
+ * @date [Current Date]
+ */
 @Data
 @EqualsAndHashCode(callSuper = true)
 @AutoMapper(target = MainPostApply.class, reverseConvertGenerate = false)
@@ -29,6 +34,8 @@ public class MainPostApplyBo extends BaseEntity {
     private String educationRequirement;
     private String salaryType;
     private String salaryRange;
+    private BigDecimal minSalary;
+    private BigDecimal maxSalary;
     private Integer recruitNum;
     private Date registrationStartDate;
     private Date registrationEndDate;

+ 148 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/MainTrainingBo.java

@@ -0,0 +1,148 @@
+package org.dromara.main.domain.bo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.main.domain.MainTraining;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.util.Date;
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = MainTraining.class, reverseConvertGenerate = false)
+public class MainTrainingBo extends BaseEntity {
+
+    /**
+     * 培训ID
+     */
+    private Long id;
+
+    /**
+     * 培训类型: video=视频培训, offline=线下培训, live=直播培训
+     */
+    private String trainingType;
+
+    /**
+     * 培训名称
+     */
+    private String name;
+
+    /**
+     * 培训描述
+     */
+    private String description;
+
+    /**
+     * 封面图ID
+     */
+    private Long thumbnail;
+
+    /**
+     * 岗位类型
+     */
+    private String jobType;
+
+    /**
+     * 岗位等级
+     */
+    private String jobLevel;
+
+    /**
+     * 岗位名称
+     */
+    private String job;
+
+    /**
+     * 排序号
+     */
+    private Integer sortOrder;
+
+    /**
+     * 状态: 0=下架, 1=上架
+     */
+    private Integer status;
+
+    /**
+     * 总时长
+     */
+    private String duration;
+
+    /**
+     * 培训城市
+     */
+    private String city;
+
+    /**
+     * 培训区域
+     */
+    private String area;
+
+    /**
+     * 详细地址
+     */
+    private String addressDetail;
+
+    /**
+     * 培训开始时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date trainingStartTime;
+
+    /**
+     * 培训结束时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date trainingEndTime;
+
+    /**
+     * 报名开始时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date applyStartTime;
+
+    /**
+     * 报名结束时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date applyEndTime;
+
+    /**
+     * 主办单位
+     */
+    private String organizer;
+
+    /**
+     * 标签(逗号分隔)
+     */
+    private String tags;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 视频列表(新增/修改时使用)
+     */
+    private List<TrainingVideoBo> videoList;
+
+    @Data
+    public static class TrainingVideoBo {
+        private Long id;
+        private String name;
+        private Long ossId;
+        private String fileUrl;
+        private String fileSize;
+        private String fileType;
+        private String duration;
+        private Integer sortOrder;
+    }
+}

+ 15 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/dto/GeoPointDto.java

@@ -0,0 +1,15 @@
+package org.dromara.main.domain.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@AllArgsConstructor
+public class GeoPointDto {
+
+    private BigDecimal latitude;
+
+    private BigDecimal longitude;
+}

+ 7 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainCompanyApplyVo.java

@@ -7,6 +7,7 @@ import lombok.Data;
 import org.dromara.main.domain.MainCompanyApply;
 
 import java.io.Serial;
+import java.math.BigDecimal;
 import java.io.Serializable;
 import java.util.Date;
 
@@ -50,6 +51,12 @@ public class MainCompanyApplyVo implements Serializable {
     @ExcelProperty(value = "手机号")
     private String mobile;
 
+    @ExcelProperty(value = "纬度")
+    private BigDecimal latitude;
+
+    @ExcelProperty(value = "经度")
+    private BigDecimal longitude;
+
     @ExcelProperty(value = "申请状态")
     private Integer applyStatus;
 

+ 6 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainPositionVo.java

@@ -58,6 +58,12 @@ public class MainPositionVo implements Serializable {
     @ExcelProperty(value = "薪资范围")
     private String salaryRange;
 
+    @ExcelProperty(value = "最小工资")
+    private BigDecimal minSalary;
+
+    @ExcelProperty(value = "最大工资")
+    private BigDecimal maxSalary;
+
     @ExcelProperty(value = "招聘人数")
     private Integer recruitNum;
 

+ 6 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainPostApplyVo.java

@@ -59,6 +59,12 @@ public class MainPostApplyVo implements Serializable {
     @ExcelProperty(value = "薪资范围")
     private String salaryRange;
 
+    @ExcelProperty(value = "最小工资")
+    private BigDecimal minSalary;
+
+    @ExcelProperty(value = "最大工资")
+    private BigDecimal maxSalary;
+
     @ExcelProperty(value = "招聘人数")
     private Integer recruitNum;
 

+ 49 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainTrainingLearnRecordVo.java

@@ -0,0 +1,49 @@
+package org.dromara.main.domain.vo;
+
+import cn.idev.excel.annotation.ExcelProperty;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import org.dromara.common.translation.annotation.Translation;
+import org.dromara.common.translation.constant.TransConstant;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+public class MainTrainingLearnRecordVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @ExcelProperty(value = "学员ID")
+    private Long studentId;
+
+    @ExcelProperty(value = "姓名")
+    private String name;
+
+    @ExcelProperty(value = "手机号")
+    private String mobile;
+
+    private Long avatar;
+
+    @Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "avatar")
+    private String avatarUrl;
+
+    @ExcelProperty(value = "已学习时长(分钟)")
+    private Integer learnedTime;
+
+    @ExcelProperty(value = "剩余时长(分钟)")
+    private Integer remainingTime;
+
+    @ExcelProperty(value = "学习进度(%)")
+    private Integer progress;
+
+    @ExcelProperty(value = "完成时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date finishTime;
+
+    @ExcelProperty(value = "上次学习时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date lastLearnTime;
+}

+ 40 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainTrainingParticipantVo.java

@@ -0,0 +1,40 @@
+package org.dromara.main.domain.vo;
+
+import cn.idev.excel.annotation.ExcelProperty;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import org.dromara.common.translation.annotation.Translation;
+import org.dromara.common.translation.constant.TransConstant;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+public class MainTrainingParticipantVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @ExcelProperty(value = "学员ID")
+    private Long studentId;
+
+    @ExcelProperty(value = "姓名")
+    private String name;
+
+    @ExcelProperty(value = "手机号")
+    private String mobile;
+
+    private Long avatar;
+
+    @Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "avatar")
+    private String avatarUrl;
+
+    @ExcelProperty(value = "报名时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date enrollTime;
+
+    @ExcelProperty(value = "签到时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date checkInTime;
+}

+ 61 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainTrainingVideoVo.java

@@ -0,0 +1,61 @@
+package org.dromara.main.domain.vo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.main.domain.MainTrainingVideo;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@AutoMapper(target = MainTrainingVideo.class)
+public class MainTrainingVideoVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 视频ID
+     */
+    private Long id;
+
+    /**
+     * 关联培训ID
+     */
+    private Long trainingId;
+
+    /**
+     * 视频名称
+     */
+    private String name;
+
+    /**
+     * 视频文件ID
+     */
+    private Long ossId;
+
+    /**
+     * 视频文件URL
+     */
+    private String fileUrl;
+
+    /**
+     * 文件大小
+     */
+    private String fileSize;
+
+    /**
+     * 文件格式
+     */
+    private String fileType;
+
+    /**
+     * 视频时长
+     */
+    private String duration;
+
+    /**
+     * 排序
+     */
+    private Integer sortOrder;
+}

+ 163 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainTrainingVo.java

@@ -0,0 +1,163 @@
+package org.dromara.main.domain.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.common.translation.annotation.Translation;
+import org.dromara.common.translation.constant.TransConstant;
+import org.dromara.main.domain.MainTraining;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+@Data
+@AutoMapper(target = MainTraining.class)
+public class MainTrainingVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 培训ID
+     */
+    private Long id;
+
+    /**
+     * 培训类型
+     */
+    private String trainingType;
+
+    /**
+     * 培训名称
+     */
+    private String name;
+
+    /**
+     * 培训描述
+     */
+    private String description;
+
+    /**
+     * 封面图ID
+     */
+    private Long thumbnail;
+
+    /**
+     * 封面图URL(自动翻译)
+     */
+    @Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "thumbnail")
+    private String thumbnailUrl;
+
+    /**
+     * 岗位类型
+     */
+    private String jobType;
+
+    /**
+     * 岗位等级
+     */
+    private String jobLevel;
+
+    /**
+     * 岗位名称
+     */
+    private String job;
+
+    /**
+     * 排序号
+     */
+    private Integer sortOrder;
+
+    /**
+     * 状态: 0=下架, 1=上架
+     */
+    private Integer status;
+
+    /**
+     * 总时长
+     */
+    private String duration;
+
+    /**
+     * 上架时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date publishTime;
+
+    /**
+     * 培训城市
+     */
+    private String city;
+
+    /**
+     * 培训区域
+     */
+    private String area;
+
+    /**
+     * 详细地址
+     */
+    private String addressDetail;
+
+    /**
+     * 培训开始时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date trainingStartTime;
+
+    /**
+     * 培训结束时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date trainingEndTime;
+
+    /**
+     * 报名开始时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date applyStartTime;
+
+    /**
+     * 报名结束时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date applyEndTime;
+
+    /**
+     * 主办单位
+     */
+    private String organizer;
+
+    /**
+     * 标签
+     */
+    private String tags;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /**
+     * 学习记录数(视频培训用)
+     */
+    private Integer learnCount;
+
+    /**
+     * 参与人数(线下培训用)
+     */
+    private Integer participantCount;
+
+    /**
+     * 视频列表
+     */
+    private List<MainTrainingVideoVo> videoList;
+}

+ 120 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/MainTrainingMapper.java

@@ -0,0 +1,120 @@
+package org.dromara.main.mapper;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.main.domain.MainTraining;
+import org.dromara.main.domain.vo.MainTrainingLearnRecordVo;
+import org.dromara.main.domain.vo.MainTrainingParticipantVo;
+import org.dromara.main.domain.vo.MainTrainingVo;
+
+import java.util.List;
+
+@Mapper
+public interface MainTrainingMapper extends BaseMapperPlus<MainTraining, MainTrainingVo> {
+
+    /**
+     * 统计培训的学习记录数
+     */
+    @Select("SELECT COUNT(*) FROM main_training_learn_record WHERE training_id = #{trainingId} AND del_flag = '0'")
+    Long countLearnRecords(@Param("trainingId") Long trainingId);
+
+    /**
+     * 统计培训的报名人数
+     */
+    @Select("SELECT COUNT(*) FROM main_training_enrollment WHERE training_id = #{trainingId} AND del_flag = '0'")
+    Long countEnrollments(@Param("trainingId") Long trainingId);
+
+    /**
+     * 查询视频培训学习记录分页
+     */
+    @Select("""
+        SELECT r.student_id AS studentId,
+               s.name AS name,
+               s.mobile AS mobile,
+               s.avatar AS avatar,
+               r.learned_time AS learnedTime,
+               r.remaining_time AS remainingTime,
+               r.progress AS progress,
+               r.finish_time AS finishTime,
+               r.last_learn_time AS lastLearnTime
+          FROM main_training_learn_record r
+          LEFT JOIN main_student s ON s.id = r.student_id AND s.del_flag = '0'
+         WHERE r.training_id = #{trainingId}
+           AND r.del_flag = '0'
+         ORDER BY r.last_learn_time DESC, r.id DESC
+        """)
+    IPage<MainTrainingLearnRecordVo> selectLearnRecordPage(Page<MainTrainingLearnRecordVo> page, @Param("trainingId") Long trainingId);
+
+    /**
+     * 按学员ID导出视频培训学习记录
+     */
+    @Select("""
+        <script>
+        SELECT r.student_id AS studentId,
+               s.name AS name,
+               s.mobile AS mobile,
+               s.avatar AS avatar,
+               r.learned_time AS learnedTime,
+               r.remaining_time AS remainingTime,
+               r.progress AS progress,
+               r.finish_time AS finishTime,
+               r.last_learn_time AS lastLearnTime
+          FROM main_training_learn_record r
+          LEFT JOIN main_student s ON s.id = r.student_id AND s.del_flag = '0'
+         WHERE r.training_id = #{trainingId}
+           AND r.del_flag = '0'
+           AND r.student_id IN
+           <foreach collection='studentIds' item='studentId' open='(' separator=',' close=')'>
+             #{studentId}
+           </foreach>
+         ORDER BY r.last_learn_time DESC, r.id DESC
+        </script>
+        """)
+    List<MainTrainingLearnRecordVo> selectLearnRecordListByStudentIds(@Param("trainingId") Long trainingId, @Param("studentIds") List<Long> studentIds);
+
+    /**
+     * 查询线下培训参与人分页
+     */
+    @Select("""
+        SELECT s.id AS studentId,
+               s.name AS name,
+               s.mobile AS mobile,
+               s.avatar AS avatar,
+               e.enroll_time AS enrollTime,
+               e.check_in_time AS checkInTime
+          FROM main_training_enrollment e
+          LEFT JOIN main_student s ON s.id = e.student_id AND s.del_flag = '0'
+         WHERE e.training_id = #{trainingId}
+           AND e.del_flag = '0'
+         ORDER BY e.enroll_time DESC, e.id DESC
+        """)
+    IPage<MainTrainingParticipantVo> selectOfflineParticipantPage(Page<MainTrainingParticipantVo> page, @Param("trainingId") Long trainingId);
+
+    /**
+     * 按学员ID导出线下培训参与人
+     */
+    @Select("""
+        <script>
+        SELECT s.id AS studentId,
+               s.name AS name,
+               s.mobile AS mobile,
+               s.avatar AS avatar,
+               e.enroll_time AS enrollTime,
+               e.check_in_time AS checkInTime
+          FROM main_training_enrollment e
+          LEFT JOIN main_student s ON s.id = e.student_id AND s.del_flag = '0'
+         WHERE e.training_id = #{trainingId}
+           AND e.del_flag = '0'
+           AND e.student_id IN
+           <foreach collection='studentIds' item='studentId' open='(' separator=',' close=')'>
+             #{studentId}
+           </foreach>
+         ORDER BY e.enroll_time DESC, e.id DESC
+        </script>
+        """)
+    List<MainTrainingParticipantVo> selectOfflineParticipantListByStudentIds(@Param("trainingId") Long trainingId, @Param("studentIds") List<Long> studentIds);
+}

+ 8 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/MainTrainingVideoMapper.java

@@ -0,0 +1,8 @@
+package org.dromara.main.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.main.domain.MainTrainingVideo;
+import org.dromara.main.domain.vo.MainTrainingVideoVo;
+
+public interface MainTrainingVideoMapper extends BaseMapperPlus<MainTrainingVideo, MainTrainingVideoVo> {
+}

+ 8 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/CompanyGeoService.java

@@ -0,0 +1,8 @@
+package org.dromara.main.service;
+
+import org.dromara.main.domain.dto.GeoPointDto;
+
+public interface CompanyGeoService {
+
+    GeoPointDto geocode(String address);
+}

+ 89 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IMainTrainingService.java

@@ -0,0 +1,89 @@
+package org.dromara.main.service;
+
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.main.domain.bo.MainTrainingBo;
+import org.dromara.main.domain.vo.MainTrainingLearnRecordVo;
+import org.dromara.main.domain.vo.MainTrainingParticipantVo;
+import org.dromara.main.domain.vo.MainTrainingVo;
+
+import java.util.Collection;
+import java.util.List;
+
+public interface IMainTrainingService {
+
+    /**
+     * 查询培训详情
+     */
+    MainTrainingVo queryById(Long id);
+
+    /**
+     * 分页查询培训列表
+     */
+    TableDataInfo<MainTrainingVo> queryPageList(MainTrainingBo bo, PageQuery pageQuery);
+
+    /**
+     * 分页查询线下培训列表
+     */
+    TableDataInfo<MainTrainingVo> queryOfflinePageList(MainTrainingBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询培训列表
+     */
+    List<MainTrainingVo> queryList(MainTrainingBo bo);
+
+    /**
+     * 查询线下培训详情
+     */
+    MainTrainingVo queryOfflineById(Long id);
+
+    /**
+     * 查询视频培训学习记录分页
+     */
+    TableDataInfo<MainTrainingLearnRecordVo> queryLearnRecordPage(Long trainingId, PageQuery pageQuery);
+
+    /**
+     * 查询线下培训参与人分页
+     */
+    TableDataInfo<MainTrainingParticipantVo> queryOfflineParticipantPage(Long trainingId, PageQuery pageQuery);
+
+    /**
+     * 按勾选学员导出视频培训学习记录
+     */
+    List<MainTrainingLearnRecordVo> queryLearnRecordExportList(Long trainingId, List<Long> studentIds);
+
+    /**
+     * 按勾选学员导出线下培训参与人
+     */
+    List<MainTrainingParticipantVo> queryOfflineParticipantExportList(Long trainingId, List<Long> studentIds);
+
+    /**
+     * 新增培训
+     */
+    Boolean insertByBo(MainTrainingBo bo);
+
+    /**
+     * 修改培训
+     */
+    Boolean updateByBo(MainTrainingBo bo);
+
+    /**
+     * 校验并批量删除培训
+     */
+    Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+
+    /**
+     * 删除线下培训
+     */
+    Boolean deleteOfflineById(Long id);
+
+    /**
+     * 更新培训状态(上架/下架)
+     */
+    Boolean updateStatus(Long id, Integer status);
+
+    /**
+     * 更新线下培训状态
+     */
+    Boolean updateOfflineStatus(Long id, Integer status);
+}

+ 2 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainAuditServiceImpl.java

@@ -220,6 +220,8 @@ public class MainAuditServiceImpl implements IMainAuditService {
         position.setEducationRequirement(postApply.getEducationRequirement());
         position.setSalaryType(postApply.getSalaryType());
         position.setSalaryRange(postApply.getSalaryRange());
+        position.setMinSalary(postApply.getMinSalary());
+        position.setMaxSalary(postApply.getMaxSalary());
         position.setRecruitNum(postApply.getRecruitNum());
         position.setRegistrationStartDate(postApply.getRegistrationStartDate());
         position.setRegistrationEndDate(postApply.getRegistrationEndDate());

+ 12 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainCompanyApplyServiceImpl.java

@@ -16,9 +16,11 @@ import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.main.domain.MainAudit;
 import org.dromara.main.domain.MainCompanyApply;
 import org.dromara.main.domain.bo.MainCompanyApplyBo;
+import org.dromara.main.domain.dto.GeoPointDto;
 import org.dromara.main.domain.vo.MainCompanyApplyVo;
 import org.dromara.main.mapper.MainAuditMapper;
 import org.dromara.main.mapper.MainCompanyApplyMapper;
+import org.dromara.main.service.CompanyGeoService;
 import org.dromara.main.service.IMainCompanyApplyService;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -32,6 +34,7 @@ public class MainCompanyApplyServiceImpl implements IMainCompanyApplyService {
 
     private final MainCompanyApplyMapper baseMapper;
     private final MainAuditMapper mainAuditMapper;
+    private final CompanyGeoService companyGeoService;
 
     @Override
     public TableDataInfo<MainCompanyApplyVo> queryPageList(MainCompanyApplyBo bo, PageQuery pageQuery) {
@@ -54,6 +57,7 @@ public class MainCompanyApplyServiceImpl implements IMainCompanyApplyService {
     @Override
     public Boolean insertByBo(MainCompanyApplyBo bo) {
         MainCompanyApply add = MapstructUtils.convert(bo, MainCompanyApply.class);
+        fillGeoPoint(add);
         validEntityBeforeSave(add);
         boolean flag = baseMapper.insert(add) > 0;
         if (flag) {
@@ -65,6 +69,7 @@ public class MainCompanyApplyServiceImpl implements IMainCompanyApplyService {
     @Override
     public Boolean updateByBo(MainCompanyApplyBo bo) {
         MainCompanyApply update = MapstructUtils.convert(bo, MainCompanyApply.class);
+        fillGeoPoint(update);
         validEntityBeforeSave(update);
         return baseMapper.updateById(update) > 0;
     }
@@ -133,6 +138,7 @@ public class MainCompanyApplyServiceImpl implements IMainCompanyApplyService {
         apply.setApplyStatus(0);
         apply.setAuthLetter(parseFirstOssId(bo.getAuthLetter()));
         apply.setAvatar(parseFirstOssId(bo.getAvatar()));
+        fillGeoPoint(apply);
 
         validEntityBeforeSave(apply);
         boolean insertResult = baseMapper.insert(apply) > 0;
@@ -182,4 +188,10 @@ public class MainCompanyApplyServiceImpl implements IMainCompanyApplyService {
             throw new ServiceException("上传文件标识格式错误");
         }
     }
+
+    private void fillGeoPoint(MainCompanyApply entity) {
+        GeoPointDto geoPoint = companyGeoService.geocode(entity.getOfficeAddress());
+        entity.setLatitude(geoPoint.getLatitude());
+        entity.setLongitude(geoPoint.getLongitude());
+    }
 }

+ 243 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainTrainingServiceImpl.java

@@ -0,0 +1,243 @@
+package org.dromara.main.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.main.domain.MainTraining;
+import org.dromara.main.domain.MainTrainingVideo;
+import org.dromara.main.domain.bo.MainTrainingBo;
+import org.dromara.main.domain.vo.MainTrainingLearnRecordVo;
+import org.dromara.main.domain.vo.MainTrainingParticipantVo;
+import org.dromara.main.domain.vo.MainTrainingVo;
+import org.dromara.main.domain.vo.MainTrainingVideoVo;
+import org.dromara.main.mapper.MainTrainingMapper;
+import org.dromara.main.mapper.MainTrainingVideoMapper;
+import org.dromara.main.service.IMainTrainingService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+
+@RequiredArgsConstructor
+@Service
+public class MainTrainingServiceImpl implements IMainTrainingService {
+
+    private final MainTrainingMapper baseMapper;
+    private final MainTrainingVideoMapper videoMapper;
+
+    @Override
+    public MainTrainingVo queryById(Long id) {
+        MainTrainingVo vo = baseMapper.selectVoById(id);
+        if (vo != null) {
+            // 查询关联视频列表
+            List<MainTrainingVideoVo> videoList = videoMapper.selectVoList(
+                new LambdaQueryWrapper<MainTrainingVideo>()
+                    .eq(MainTrainingVideo::getTrainingId, id)
+                    .orderByAsc(MainTrainingVideo::getSortOrder)
+            );
+            vo.setVideoList(videoList);
+            fillExtraInfo(vo);
+        }
+        return vo;
+    }
+
+    @Override
+    public TableDataInfo<MainTrainingVo> queryPageList(MainTrainingBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<MainTraining> lqw = buildQueryWrapper(bo);
+        Page<MainTrainingVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+
+        // 填充额外信息
+        for (MainTrainingVo vo : result.getRecords()) {
+            fillExtraInfo(vo);
+        }
+
+        return TableDataInfo.build(result);
+    }
+
+    @Override
+    public TableDataInfo<MainTrainingVo> queryOfflinePageList(MainTrainingBo bo, PageQuery pageQuery) {
+        bo.setTrainingType("offline");
+        return queryPageList(bo, pageQuery);
+    }
+
+    @Override
+    public List<MainTrainingVo> queryList(MainTrainingBo bo) {
+        LambdaQueryWrapper<MainTraining> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    @Override
+    public MainTrainingVo queryOfflineById(Long id) {
+        MainTrainingVo vo = queryById(id);
+        if (vo == null || !"offline".equals(vo.getTrainingType())) {
+            return null;
+        }
+        return vo;
+    }
+
+    @Override
+    public TableDataInfo<MainTrainingLearnRecordVo> queryLearnRecordPage(Long trainingId, PageQuery pageQuery) {
+        Page<MainTrainingLearnRecordVo> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize());
+        return TableDataInfo.build(baseMapper.selectLearnRecordPage(page, trainingId));
+    }
+
+    @Override
+    public TableDataInfo<MainTrainingParticipantVo> queryOfflineParticipantPage(Long trainingId, PageQuery pageQuery) {
+        Page<MainTrainingParticipantVo> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize());
+        return TableDataInfo.build(baseMapper.selectOfflineParticipantPage(page, trainingId));
+    }
+
+    @Override
+    public List<MainTrainingLearnRecordVo> queryLearnRecordExportList(Long trainingId, List<Long> studentIds) {
+        List<Long> validIds = studentIds.stream().filter(Objects::nonNull).toList();
+        if (validIds.isEmpty()) {
+            return List.of();
+        }
+        return baseMapper.selectLearnRecordListByStudentIds(trainingId, validIds);
+    }
+
+    @Override
+    public List<MainTrainingParticipantVo> queryOfflineParticipantExportList(Long trainingId, List<Long> studentIds) {
+        List<Long> validIds = studentIds.stream().filter(Objects::nonNull).toList();
+        if (validIds.isEmpty()) {
+            return List.of();
+        }
+        return baseMapper.selectOfflineParticipantListByStudentIds(trainingId, validIds);
+    }
+
+    private LambdaQueryWrapper<MainTraining> buildQueryWrapper(MainTrainingBo bo) {
+        LambdaQueryWrapper<MainTraining> lqw = Wrappers.lambdaQuery();
+        lqw.eq(StringUtils.isNotBlank(bo.getTrainingType()), MainTraining::getTrainingType, bo.getTrainingType());
+        lqw.like(StringUtils.isNotBlank(bo.getName()), MainTraining::getName, bo.getName());
+        lqw.eq(ObjectUtil.isNotNull(bo.getStatus()), MainTraining::getStatus, bo.getStatus());
+        lqw.eq(StringUtils.isNotBlank(bo.getJobType()), MainTraining::getJobType, bo.getJobType());
+        lqw.eq(StringUtils.isNotBlank(bo.getJobLevel()), MainTraining::getJobLevel, bo.getJobLevel());
+        lqw.orderByAsc(MainTraining::getSortOrder);
+        lqw.orderByDesc(MainTraining::getCreateTime);
+        return lqw;
+    }
+
+    /**
+     * 填充额外统计信息
+     */
+    private void fillExtraInfo(MainTrainingVo vo) {
+        if ("video".equals(vo.getTrainingType())) {
+            Long count = baseMapper.countLearnRecords(vo.getId());
+            vo.setLearnCount(count != null ? count.intValue() : 0);
+        } else if ("offline".equals(vo.getTrainingType())) {
+            Long count = baseMapper.countEnrollments(vo.getId());
+            vo.setParticipantCount(count != null ? count.intValue() : 0);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean insertByBo(MainTrainingBo bo) {
+        MainTraining add = BeanUtil.toBean(bo, MainTraining.class);
+        // 如果上架,设置上架时间
+        if (add.getStatus() != null && add.getStatus() == 1) {
+            add.setPublishTime(new Date());
+        }
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            bo.setId(add.getId());
+            // 保存视频子表
+            saveVideoList(add.getId(), bo.getVideoList());
+        }
+        return flag;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updateByBo(MainTrainingBo bo) {
+        MainTraining update = BeanUtil.toBean(bo, MainTraining.class);
+        boolean flag = baseMapper.updateById(update) > 0;
+        if (flag && bo.getVideoList() != null) {
+            // 先删除旧视频,再插入新视频
+            videoMapper.delete(
+                new LambdaQueryWrapper<MainTrainingVideo>()
+                    .eq(MainTrainingVideo::getTrainingId, bo.getId())
+            );
+            saveVideoList(bo.getId(), bo.getVideoList());
+        }
+        return flag;
+    }
+
+    /**
+     * 保存视频子表
+     */
+    private void saveVideoList(Long trainingId, List<MainTrainingBo.TrainingVideoBo> videoList) {
+        if (videoList == null || videoList.isEmpty()) {
+            return;
+        }
+        for (int i = 0; i < videoList.size(); i++) {
+            MainTrainingBo.TrainingVideoBo videoBo = videoList.get(i);
+            MainTrainingVideo video = new MainTrainingVideo();
+            video.setTrainingId(trainingId);
+            video.setName(videoBo.getName());
+            video.setOssId(videoBo.getOssId());
+            video.setFileUrl(videoBo.getFileUrl());
+            video.setFileSize(videoBo.getFileSize());
+            video.setFileType(videoBo.getFileType());
+            video.setDuration(videoBo.getDuration());
+            video.setSortOrder(videoBo.getSortOrder() != null ? videoBo.getSortOrder() : i + 1);
+            videoMapper.insert(video);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
+        if (isValid) {
+            // 业务校验
+        }
+        // 同时删除视频子表
+        for (Long id : ids) {
+            videoMapper.delete(
+                new LambdaQueryWrapper<MainTrainingVideo>()
+                    .eq(MainTrainingVideo::getTrainingId, id)
+            );
+        }
+        return baseMapper.deleteBatchIds(ids) > 0;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteOfflineById(Long id) {
+        MainTraining training = baseMapper.selectById(id);
+        if (training == null || !"offline".equals(training.getTrainingType())) {
+            return false;
+        }
+        return deleteWithValidByIds(List.of(id), true);
+    }
+
+    @Override
+    public Boolean updateStatus(Long id, Integer status) {
+        MainTraining training = new MainTraining();
+        training.setId(id);
+        training.setStatus(status);
+        // 上架时设置上架时间
+        if (status != null && status == 1) {
+            training.setPublishTime(new Date());
+        }
+        return baseMapper.updateById(training) > 0;
+    }
+
+    @Override
+    public Boolean updateOfflineStatus(Long id, Integer status) {
+        MainTraining training = baseMapper.selectById(id);
+        if (training == null || !"offline".equals(training.getTrainingType())) {
+            return false;
+        }
+        return updateStatus(id, status);
+    }
+}

+ 69 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/TencentMapCompanyGeoServiceImpl.java

@@ -0,0 +1,69 @@
+package org.dromara.main.service.impl;
+
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.main.config.TencentMapGeoProperties;
+import org.dromara.main.domain.dto.GeoPointDto;
+import org.dromara.main.service.CompanyGeoService;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 基于腾讯位置服务地址解析接口,将地址转换为经纬度。
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TencentMapCompanyGeoServiceImpl implements CompanyGeoService {
+
+    private final TencentMapGeoProperties tencentMapGeoProperties;
+
+    @Override
+    public GeoPointDto geocode(String address) {
+        if (StringUtils.isBlank(address)) {
+            throw new ServiceException("办公地址不能为空");
+        }
+        if (StringUtils.isBlank(tencentMapGeoProperties.getKey())) {
+            throw new ServiceException("未配置腾讯地图地理编码 Key");
+        }
+
+        Map<String, Object> params = new HashMap<>(4);
+        params.put("key", tencentMapGeoProperties.getKey());
+        params.put("address", address.trim());
+        params.put("output", "json");
+
+        try {
+            String response = HttpUtil.get(tencentMapGeoProperties.getGeocodeUrl(), params);
+            JSONObject result = JSONUtil.parseObj(response);
+            Integer status = result.getInt("status");
+            if (status == null || status != 0) {
+                String message = result.getStr("message");
+                log.error("腾讯地图地理编码失败,address={}, status={}, message={}", address, status, message);
+                throw new ServiceException(StringUtils.format("地址解析失败: {}", StringUtils.blankToDefault(message, "腾讯地图接口调用失败")));
+            }
+
+            JSONObject resultNode = result.getJSONObject("result");
+            JSONObject location = resultNode == null ? null : resultNode.getJSONObject("location");
+            if (location == null) {
+                throw new ServiceException("地址解析失败: 未匹配到经纬度,请检查办公地址");
+            }
+
+            BigDecimal latitude = BigDecimal.valueOf(location.getDouble("lat"));
+            BigDecimal longitude = BigDecimal.valueOf(location.getDouble("lng"));
+            return new GeoPointDto(latitude, longitude);
+        } catch (ServiceException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            log.error("调用腾讯地图地理编码接口异常,address={}", address, ex);
+            throw new ServiceException("地址解析失败,请稍后重试");
+        }
+    }
+}

+ 72 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/WithdrawServiceImpl.java

@@ -17,6 +17,7 @@ import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.main.config.WithdrawConfig;
 import org.dromara.main.domain.CompanyAccountFlow;
 import org.dromara.main.domain.PaymentConfig;
 import org.dromara.main.domain.Withdraw;
@@ -36,6 +37,8 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneId;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -56,6 +59,7 @@ public class WithdrawServiceImpl implements IWithdrawService {
     private final ISysTenantService tenantService;
     private final SysTenantMapper sysTenantMapper;
     private final IPaymentConfigService paymentConfigService;
+    private final WithdrawConfig withdrawConfig;
 
     @Override
     public WithdrawVo queryById(Long id) {
@@ -125,6 +129,14 @@ public class WithdrawServiceImpl implements IWithdrawService {
         insertFlow(company.getId(), 2, amount, 1, availableBalance, availableBalance.subtract(amount), 5, withdraw.getId(), withdraw.getWithdrawNo(), "申请提现:" + withdraw.getWithdrawNo());
         insertFlow(company.getId(), 1, amount, 3, withdrawingBalance, withdrawingBalance.add(amount), 5, withdraw.getId(), withdraw.getWithdrawNo(), "申请提现(冻结):" + withdraw.getWithdrawNo());
 
+        if (withdrawConfig.isAutoAudit() && shouldAutoAudit(company.getId(), amount)) {
+            try {
+                auditAndTransfer(withdraw.getId(), "系统自动审核通过");
+            } catch (Exception ignored) {
+                // 自动审核失败时保留待审核状态,转人工处理
+            }
+        }
+
         return withdraw.getId();
     }
 
@@ -353,6 +365,48 @@ public class WithdrawServiceImpl implements IWithdrawService {
             availableBalance.add(withdraw.getWithdrawAmount()), 7, withdraw.getId(), withdraw.getWithdrawNo(), remark);
     }
 
+    private boolean shouldAutoAudit(Long companyId, BigDecimal amount) {
+        WithdrawConfig.AutoAuditRule rule = withdrawConfig.getAutoAuditRule();
+        if (rule == null) {
+            return false;
+        }
+
+        if (amount.compareTo(defaultAmount(rule.getMaxAmount())) > 0) {
+            return false;
+        }
+
+        DailyLimit dailyLimit = checkDailyLimit(companyId);
+        Integer maxDailyCount = rule.getMaxDailyCount() == null ? 0 : rule.getMaxDailyCount();
+        if (maxDailyCount > 0 && dailyLimit.getCount() >= maxDailyCount) {
+            return false;
+        }
+
+        BigDecimal maxDailyAmount = defaultAmount(rule.getMaxDailyAmount());
+        BigDecimal totalAmount = dailyLimit.getAmount().add(amount);
+        return maxDailyAmount.compareTo(BigDecimal.ZERO) <= 0 || totalAmount.compareTo(maxDailyAmount) <= 0;
+    }
+
+    private DailyLimit checkDailyLimit(Long companyId) {
+        LocalDate today = LocalDate.now();
+        Date startOfDay = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
+        Date endOfDay = Date.from(today.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+        List<Withdraw> todayWithdraws = withdrawMapper.selectList(
+            Wrappers.<Withdraw>lambdaQuery()
+                .eq(Withdraw::getCompanyId, companyId)
+                .ge(Withdraw::getCreateTime, startOfDay)
+                .lt(Withdraw::getCreateTime, endOfDay)
+        );
+
+        int count = todayWithdraws.size();
+        BigDecimal totalAmount = todayWithdraws.stream()
+            .map(Withdraw::getWithdrawAmount)
+            .filter(Objects::nonNull)
+            .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        return new DailyLimit(count, totalAmount);
+    }
+
     private String transferToAlipay(Withdraw withdraw) throws Exception {
         PaymentConfig config = paymentConfigService.getEnabledTransferConfig();
         if (config == null) {
@@ -430,4 +484,22 @@ public class WithdrawServiceImpl implements IWithdrawService {
     private String defaultIfBlank(String value, String defaultValue) {
         return value == null || value.isBlank() ? defaultValue : value;
     }
+
+    private static class DailyLimit {
+        private final int count;
+        private final BigDecimal amount;
+
+        private DailyLimit(int count, BigDecimal amount) {
+            this.count = count;
+            this.amount = amount;
+        }
+
+        public int getCount() {
+            return count;
+        }
+
+        public BigDecimal getAmount() {
+            return amount;
+        }
+    }
 }

+ 4 - 0
ruoyi-modules/ruoyi-main/src/main/resources/mapper/MainAuditMapper.xml

@@ -40,6 +40,8 @@
         <result property="educationRequirement" column="education_requirement" />
         <result property="salaryType"     column="salary_type"     />
         <result property="salaryRange"    column="salary_range"    />
+        <result property="minSalary"      column="min_salary"      />
+        <result property="maxSalary"      column="max_salary"      />
         <result property="recruitNum"     column="recruit_num"     />
         <result property="registrationStartDate" column="registration_start_date" />
         <result property="registrationEndDate" column="registration_end_date" />
@@ -101,6 +103,8 @@
             pa.education_requirement,
             pa.salary_type,
             pa.salary_range,
+            pa.min_salary,
+            pa.max_salary,
             pa.recruit_num,
             pa.registration_start_date,
             pa.registration_end_date,