Gqingci 1 hafta önce
ebeveyn
işleme
0728789b48
23 değiştirilmiş dosya ile 1270 ekleme ve 31 silme
  1. 2 1
      ruoyi-admin/src/main/resources/application-dev.yml
  2. 0 1
      ruoyi-modules/ruoyi-main/pom.xml
  3. 296 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalAuthController.java
  4. 33 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalBannerController.java
  5. 231 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalCheckController.java
  6. 80 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalCompanyController.java
  7. 48 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalOssController.java
  8. 39 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/Banner.java
  9. 38 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainOrder.java
  10. 30 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/BannerBo.java
  11. 39 2
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/MainCompanyApplyBo.java
  12. 38 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/BannerVo.java
  13. 7 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainBackOrderVo.java
  14. 8 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/BannerMapper.java
  15. 7 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/MainOrderMapper.java
  16. 11 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IBannerService.java
  17. 7 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IMainBackOrderService.java
  18. 8 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IMainCompanyApplyService.java
  19. 29 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/BannerServiceImpl.java
  20. 31 16
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/CsMessageServiceImpl.java
  21. 106 11
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/CsSessionServiceImpl.java
  22. 78 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainBackOrderServiceImpl.java
  23. 104 0
      ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainCompanyApplyServiceImpl.java

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

@@ -96,7 +96,8 @@ spring:
 spring.data:
   redis:
     # 地址
-    host: localhost
+    host: 192.168.194.130
+    password: 123456
     # 端口,默认为6379
     port: 6379
     # 数据库索引

+ 0 - 1
ruoyi-modules/ruoyi-main/pom.xml

@@ -3,7 +3,6 @@
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
-    @d:/yp_pro/sj-backend/ruoyi-modules/ruoyi-main/pom.xml:6-10
     <parent>
         <groupId>org.dromara</groupId>
         <artifactId>ruoyi-modules</artifactId>

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

@@ -0,0 +1,296 @@
+package org.dromara.main.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.stp.parameter.SaLoginParameter;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.lang.Opt;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.RandomUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import jakarta.validation.constraints.NotBlank;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.constant.SystemConstants;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.domain.dto.PostDTO;
+import org.dromara.common.core.domain.dto.RoleDTO;
+import org.dromara.common.core.domain.model.LoginUser;
+import org.dromara.common.core.domain.model.SmsLoginBody;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.ratelimiter.annotation.RateLimiter;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.main.domain.bo.MainCompanyApplyBo;
+import org.dromara.main.domain.vo.MainCompanyApplyVo;
+import org.dromara.main.service.IMainCompanyApplyService;
+import org.dromara.sms4j.api.SmsBlend;
+import org.dromara.sms4j.api.entity.SmsResponse;
+import org.dromara.sms4j.core.factory.SmsFactory;
+import org.dromara.system.domain.SysUser;
+import org.dromara.system.domain.vo.SysClientVo;
+import org.dromara.system.domain.vo.SysDeptVo;
+import org.dromara.system.domain.vo.SysPostVo;
+import org.dromara.system.domain.vo.SysRoleVo;
+import org.dromara.system.domain.vo.SysTenantVo;
+import org.dromara.system.domain.vo.SysUserVo;
+import org.dromara.system.mapper.SysUserMapper;
+import org.dromara.system.service.ISysClientService;
+import org.dromara.system.service.ISysDeptService;
+import org.dromara.system.service.ISysPermissionService;
+import org.dromara.system.service.ISysPostService;
+import org.dromara.system.service.ISysRoleService;
+import org.dromara.system.service.ISysTenantService;
+import org.springframework.beans.factory.annotation.Value;
+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.RestController;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 门户认证Controller(公开接口,无需登录)
+ *
+ * @author sj
+ */
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/portal/auth")
+public class PortalAuthController extends BaseController {
+
+    private final IMainCompanyApplyService mainCompanyApplyService;
+    private final ISysClientService clientService;
+    private final ISysTenantService tenantService;
+    private final ISysPermissionService permissionService;
+    private final ISysRoleService roleService;
+    private final ISysPostService postService;
+    private final ISysDeptService deptService;
+    private final SysUserMapper userMapper;
+
+    @Value("${spring.profiles.active:}")
+    private String activeProfile;
+
+    /**
+     * 发送短信验证码
+     *
+     * @param phonenumber 手机号
+     */
+    @SaIgnore
+    @RateLimiter(key = "#phonenumber", time = 60, count = 1)
+    @GetMapping("/sms/code")
+    public R<Void> smsCode(@NotBlank(message = "手机号不能为空") String phonenumber) {
+        String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
+        String code = RandomUtil.randomNumbers(4);
+        // 验证码缓存5分钟
+        RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+
+        if ("dev".equalsIgnoreCase(activeProfile)) {
+            log.info("开发环境短信验证码: phone={}, code={}", phonenumber, code);
+            return R.ok();
+        }
+
+        // 发送短信验证码
+        // 验证码模板id 需要在短信服务商配置
+        String templateId = "";
+        LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
+        map.put("code", code);
+
+        try {
+            SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
+            SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
+            if (!smsResponse.isSuccess()) {
+                log.error("验证码短信发送异常 => {}", smsResponse);
+                return R.fail(smsResponse.getData().toString());
+            }
+        } catch (Exception e) {
+            log.error("验证码短信发送异常 => {}", e.getMessage());
+            throw new ServiceException("短信发送失败,请稍后重试");
+        }
+
+        return R.ok();
+    }
+
+    /**
+     * 门户短信登录
+     */
+    @SaIgnore
+    @PostMapping("/smsLogin")
+    public R<Map<String, Object>> smsLogin(@Validated @RequestBody SmsLoginBody loginBody) {
+        ValidatorUtils.validate(loginBody);
+
+        String clientId = loginBody.getClientId();
+        String grantType = loginBody.getGrantType();
+        if (!"sms".equalsIgnoreCase(grantType)) {
+            return R.fail("授权类型错误");
+        }
+
+        SysClientVo client = clientService.queryByClientId(clientId);
+        if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
+            log.info("门户登录客户端异常 clientId={}, grantType={}", clientId, grantType);
+            return R.fail("客户端配置不存在或未开启短信认证");
+        }
+        if (!SystemConstants.NORMAL.equals(client.getStatus())) {
+            return R.fail("客户端已被停用");
+        }
+
+        MainCompanyApplyVo companyApply = queryLatestCompanyApply(loginBody.getPhonenumber());
+        if (companyApply == null) {
+            return R.fail("该手机号未注册企业账号");
+        }
+
+        Integer applyStatus = companyApply.getApplyStatus();
+        Map<String, Object> result = new HashMap<>(4);
+
+        if (!Integer.valueOf(2).equals(applyStatus)) {
+            result.put("auditStatus", Integer.valueOf(3).equals(applyStatus) ? 2 : 0);
+            result.put("company", companyApply);
+            return R.ok(result);
+        }
+
+        try {
+            SysUserVo employee = queryUserByPhone(loginBody.getPhonenumber());
+            if (employee == null) {
+                return R.fail("登录账号不存在,请联系管理员");
+            }
+            if (!SystemConstants.NORMAL.equals(employee.getStatus())) {
+                return R.fail("登录账号已被停用");
+            }
+
+            validateSmsCode(loginBody.getPhonenumber(), loginBody.getSmsCode());
+            LoginUser loginUser = buildLoginUser(employee, client);
+            issueToken(loginUser, client);
+            RedisUtils.deleteObject(GlobalConstants.CAPTCHA_CODE_KEY + loginBody.getPhonenumber());
+
+            SysTenantVo company = null;
+            if (StringUtils.isNotBlank(companyApply.getTenantId())) {
+                company = tenantService.queryByTenantId(companyApply.getTenantId());
+            }
+
+            result.put("auditStatus", 1);
+            result.put("token", StpUtil.getTokenValue());
+            result.put("clientId", client.getClientId());
+            result.put("employee", employee);
+            result.put("company", ObjectUtil.defaultIfNull(company, companyApply));
+            return R.ok(result);
+        } catch (ServiceException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("门户短信登录失败: {}", e.getMessage(), e);
+            throw new ServiceException("登录失败,请稍后重试");
+        }
+    }
+
+    /**
+     * 获取当前登录信息
+     */
+    @GetMapping("/getInfo")
+    public R<Map<String, Object>> getInfo() {
+        Long userId = LoginHelper.getUserId();
+        if (userId == null) {
+            return R.fail("未登录或token已失效");
+        }
+
+        SysUserVo employee = userMapper.selectVoById(userId);
+        if (employee == null) {
+            return R.fail("用户信息不存在");
+        }
+
+        SysTenantVo company = null;
+        if (StringUtils.isNotBlank(employee.getTenantId())) {
+            company = tenantService.queryByTenantId(employee.getTenantId());
+        }
+
+        Map<String, Object> result = new HashMap<>(2);
+        result.put("employee", employee);
+        result.put("company", company);
+        return R.ok(result);
+    }
+
+    /**
+     * 退出登录
+     */
+    @SaIgnore
+    @PostMapping("/logout")
+    public R<Void> logout() {
+        try {
+            if (LoginHelper.isLogin()) {
+                StpUtil.logout();
+            }
+        } catch (Exception e) {
+            log.warn("门户退出登录异常: {}", e.getMessage());
+        }
+        return R.ok();
+    }
+
+    private MainCompanyApplyVo queryLatestCompanyApply(String phonenumber) {
+        MainCompanyApplyBo bo = new MainCompanyApplyBo();
+        bo.setMobile(phonenumber);
+        List<MainCompanyApplyVo> list = mainCompanyApplyService.queryList(bo);
+        return list.isEmpty() ? null : list.get(0);
+    }
+
+    private SysUserVo queryUserByPhone(String phonenumber) {
+        return userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>()
+            .eq(SysUser::getPhonenumber, phonenumber)
+            .last("limit 1"));
+    }
+
+    private void validateSmsCode(String phonenumber, String smsCode) {
+        String cacheKey = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
+        String cachedCode = RedisUtils.getCacheObject(cacheKey);
+        if (StringUtils.isBlank(cachedCode)) {
+            throw new ServiceException("验证码已过期,请重新获取");
+        }
+        if (!StringUtils.equals(cachedCode, smsCode)) {
+            throw new ServiceException("验证码错误");
+        }
+    }
+
+    private LoginUser buildLoginUser(SysUserVo user, SysClientVo client) {
+        LoginUser loginUser = new LoginUser();
+        Long userId = user.getUserId();
+        loginUser.setTenantId(user.getTenantId());
+        loginUser.setUserId(userId);
+        loginUser.setDeptId(user.getDeptId());
+        loginUser.setUsername(user.getUserName());
+        loginUser.setNickname(user.getNickName());
+        loginUser.setUserType(user.getUserType());
+        loginUser.setMenuPermission(permissionService.getMenuPermission(userId));
+        loginUser.setRolePermission(permissionService.getRolePermission(userId));
+        loginUser.setClientKey(client.getClientKey());
+        loginUser.setDeviceType(client.getDeviceType());
+        if (ObjectUtil.isNotNull(user.getDeptId())) {
+            Opt<SysDeptVo> deptOpt = Opt.of(user.getDeptId()).map(deptService::selectDeptById);
+            loginUser.setDeptName(deptOpt.map(SysDeptVo::getDeptName).orElse(StringUtils.EMPTY));
+            loginUser.setDeptCategory(deptOpt.map(SysDeptVo::getDeptCategory).orElse(StringUtils.EMPTY));
+        }
+        List<SysRoleVo> roles = roleService.selectRolesByUserId(userId);
+        List<SysPostVo> posts = postService.selectPostsByUserId(userId);
+        loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
+        loginUser.setPosts(BeanUtil.copyToList(posts, PostDTO.class));
+        return loginUser;
+    }
+
+    private void issueToken(LoginUser loginUser, SysClientVo client) {
+        SaLoginParameter model = new SaLoginParameter();
+        model.setDeviceType(client.getDeviceType());
+        model.setTimeout(client.getTimeout());
+        model.setActiveTimeout(client.getActiveTimeout());
+        model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+        LoginHelper.login(loginUser, model);
+    }
+}

+ 33 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalBannerController.java

@@ -0,0 +1,33 @@
+package org.dromara.main.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.main.domain.bo.BannerBo;
+import org.dromara.main.domain.vo.BannerVo;
+import org.dromara.main.service.IBannerService;
+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;
+
+/**
+ * 门户轮播图接口
+ */
+@SaIgnore
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/portal/banner")
+public class PortalBannerController extends BaseController {
+
+    private final IBannerService bannerService;
+
+    @GetMapping("/list")
+    public R<List<BannerVo>> getBannerList() {
+        BannerBo bo = new BannerBo();
+        bo.setIsEnabled(1L);
+        return R.ok(bannerService.queryList(bo));
+    }
+}

+ 231 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalCheckController.java

@@ -0,0 +1,231 @@
+package org.dromara.main.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.main.domain.bo.MainBackCategoryBo;
+import org.dromara.main.domain.bo.MainBackClauseBo;
+import org.dromara.main.domain.vo.MainBackCategoryVo;
+import org.dromara.main.domain.vo.MainBackClauseVo;
+import org.dromara.main.domain.vo.MainBackOrderVo;
+import org.dromara.main.service.IMainBackCategoryService;
+import org.dromara.main.service.IMainBackClauseService;
+import org.dromara.main.service.IMainBackOrderService;
+import org.dromara.system.domain.vo.SysDictDataVo;
+import org.dromara.system.service.ISysDictTypeService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.math.BigDecimal;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 门户背调公开接口
+ */
+@SaIgnore
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/portal/check")
+public class PortalCheckController extends BaseController {
+
+    private static final Map<String, String> CLAUSE_TYPE_CODE_MAP = Map.of(
+        "身份风险", "1",
+        "职业风险", "2",
+        "能力评估", "3"
+    );
+
+    private final IMainBackCategoryService mainBackCategoryService;
+    private final IMainBackClauseService mainBackClauseService;
+    private final IMainBackOrderService mainBackOrderService;
+    private final ISysDictTypeService dictTypeService;
+
+    @GetMapping("/category/top3")
+    public R<List<CategoryWithClausesVo>> getTop3CategoriesWithClauses() {
+        MainBackCategoryBo categoryBo = new MainBackCategoryBo();
+        categoryBo.setStatus("1");
+
+        List<MainBackCategoryVo> categories = new ArrayList<>(mainBackCategoryService.queryList(categoryBo));
+        categories.sort(Comparator.comparing(MainBackCategoryVo::getId));
+        if (categories.size() > 3) {
+            categories = categories.subList(0, 3);
+        }
+
+        List<CategoryWithClausesVo> result = new ArrayList<>(categories.size());
+        for (MainBackCategoryVo category : categories) {
+            MainBackClauseBo clauseBo = new MainBackClauseBo();
+            clauseBo.setCategoryId(category.getId());
+            clauseBo.setStatus("1");
+            List<MainBackClauseVo> clauses = mainBackClauseService.queryList(clauseBo);
+
+            List<PortalCheckClauseVo> clauseVos = clauses.stream()
+                .map(clause -> toPortalClauseVo(clause, category.getName()))
+                .toList();
+
+            result.add(new CategoryWithClausesVo(toPortalCategoryVo(category), clauseVos));
+        }
+        return R.ok(result);
+    }
+
+    @GetMapping("/clauses/grouped")
+    public R<Map<String, List<PortalCheckClauseVo>>> getAllClausesGrouped() {
+        MainBackClauseBo bo = new MainBackClauseBo();
+        bo.setStatus("1");
+        List<MainBackClauseVo> clauses = mainBackClauseService.queryList(bo);
+
+        MainBackCategoryBo categoryBo = new MainBackCategoryBo();
+        categoryBo.setStatus("1");
+        Map<Long, String> categoryNameMap = new LinkedHashMap<>();
+        for (MainBackCategoryVo category : mainBackCategoryService.queryList(categoryBo)) {
+            categoryNameMap.put(category.getId(), category.getName());
+        }
+
+        Map<String, List<PortalCheckClauseVo>> grouped = new LinkedHashMap<>();
+        for (MainBackClauseVo clause : clauses) {
+            PortalCheckClauseVo clauseVo = toPortalClauseVo(clause, categoryNameMap.get(clause.getCategoryId()));
+            String clauseType = clauseVo.getClauseType();
+            grouped.computeIfAbsent(clauseType == null || clauseType.isBlank() ? "0" : clauseType, key -> new ArrayList<>())
+                .add(clauseVo);
+        }
+        return R.ok(grouped);
+    }
+
+    @GetMapping("/dict/{dictType}")
+    public R<List<SysDictDataVo>> getPublicDict(@PathVariable String dictType) {
+        List<String> allowedTypes = Arrays.asList(
+            "candidate_id_type",
+            "main_clause_type",
+            "check_process_status",
+            "withdraw_account_type",
+            "order_status"
+        );
+        if (!allowedTypes.contains(dictType)) {
+            return R.fail("不允许查询该字典类型");
+        }
+
+        String actualType = "main_clause_type".equals(dictType) ? "sys_clause_type" : dictType;
+        List<SysDictDataVo> data = dictTypeService.selectDictDataByType(actualType);
+        if (data == null) {
+            data = new ArrayList<>();
+        }
+        if ("main_clause_type".equals(dictType)) {
+            data = data.stream().map(this::toPortalClauseTypeDict).toList();
+        }
+        return R.ok(data);
+    }
+
+    @GetMapping("/order/list")
+    public R<List<MainBackOrderVo>> getMyOrders() {
+        String tenantId = LoginHelper.getTenantId();
+        if (tenantId == null || tenantId.isBlank()) {
+            return R.fail("未登录或token已失效");
+        }
+        return R.ok(mainBackOrderService.queryPortalListByTenantId(tenantId));
+    }
+
+    private PortalCheckCategoryVo toPortalCategoryVo(MainBackCategoryVo category) {
+        PortalCheckCategoryVo vo = new PortalCheckCategoryVo();
+        vo.setId(category.getId());
+        vo.setCategoryName(category.getName());
+        vo.setCategoryPrice(category.getPrice());
+        vo.setCreateTime(category.getCreateTime() == null ? null :
+            java.util.Date.from(category.getCreateTime().atZone(ZoneId.systemDefault()).toInstant()));
+        vo.setRemark(category.getRemark());
+        return vo;
+    }
+
+    private PortalCheckClauseVo toPortalClauseVo(MainBackClauseVo clause, String categoryName) {
+        PortalCheckClauseVo vo = new PortalCheckClauseVo();
+        vo.setId(clause.getId());
+        vo.setCategoryId(clause.getCategoryId());
+        vo.setCategoryName(categoryName);
+        vo.setClauseName(clause.getName());
+        vo.setClauseType(normalizeClauseType(clause.getType()));
+        vo.setClauseSetting("0");
+        vo.setClauseContent(clause.getDescription());
+        vo.setClausePrice(clause.getPrice());
+        vo.setIsFree(0);
+        vo.setStatus(parseIntOrDefault(clause.getStatus(), 1));
+        vo.setCreateTime(clause.getCreateTime());
+        vo.setRemark(clause.getRemark());
+        return vo;
+    }
+
+    private String normalizeClauseType(String rawType) {
+        if (rawType == null || rawType.isBlank()) {
+            return "0";
+        }
+        if (rawType.chars().allMatch(Character::isDigit)) {
+            return rawType;
+        }
+        return CLAUSE_TYPE_CODE_MAP.getOrDefault(rawType, rawType);
+    }
+
+    private int parseIntOrDefault(String value, int defaultValue) {
+        if (value == null || value.isBlank()) {
+            return defaultValue;
+        }
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException ignored) {
+            return defaultValue;
+        }
+    }
+
+    private SysDictDataVo toPortalClauseTypeDict(SysDictDataVo source) {
+        SysDictDataVo target = new SysDictDataVo();
+        target.setDictCode(source.getDictCode());
+        target.setDictSort(source.getDictSort());
+        target.setDictLabel(source.getDictLabel());
+        target.setDictValue(normalizeClauseType(source.getDictValue()));
+        target.setDictType("main_clause_type");
+        target.setCssClass(source.getCssClass());
+        target.setListClass(source.getListClass());
+        target.setIsDefault(source.getIsDefault());
+        target.setRemark(source.getRemark());
+        return target;
+    }
+
+    @Data
+    private static class PortalCheckCategoryVo {
+        private Long id;
+        private String categoryName;
+        private BigDecimal categoryPrice;
+        private java.util.Date createTime;
+        private String remark;
+    }
+
+    @Data
+    private static class PortalCheckClauseVo {
+        private Long id;
+        private Long categoryId;
+        private String categoryName;
+        private String clauseName;
+        private String clauseType;
+        private String clauseSetting;
+        private String clauseContent;
+        private BigDecimal clausePrice;
+        private Integer isFree;
+        private Integer status;
+        private java.util.Date createTime;
+        private String remark;
+    }
+
+    @Data
+    @AllArgsConstructor
+    private static class CategoryWithClausesVo {
+        private PortalCheckCategoryVo category;
+        private List<PortalCheckClauseVo> clauses;
+    }
+}

+ 80 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalCompanyController.java

@@ -0,0 +1,80 @@
+package org.dromara.main.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import jakarta.validation.constraints.NotBlank;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.main.domain.bo.MainCompanyApplyBo;
+import org.dromara.main.service.IMainCompanyApplyService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 企业入驻门户Controller(公开接口,无需登录)
+ *
+ * @author sj
+ */
+@SaIgnore
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/portal/company")
+public class PortalCompanyController extends BaseController {
+
+    private final IMainCompanyApplyService mainCompanyApplyService;
+
+    /**
+     * 企业入驻申请
+     */
+    @PostMapping("/apply")
+    public R<Void> apply(@RequestBody @Validated(AddGroup.class) MainCompanyApplyBo bo) {
+        return toAjax(mainCompanyApplyService.applyCompany(bo));
+    }
+
+    /**
+     * 检查企业名称是否已存在(前端实时校验用)
+     *
+     * @param companyName 企业名称
+     * @return true=唯一可用,false=已存在
+     */
+    @GetMapping("/checkName")
+    public R<Boolean> checkCompanyName(@RequestParam @NotBlank(message = "企业名称不能为空") String companyName) {
+        MainCompanyApplyBo bo = new MainCompanyApplyBo();
+        bo.setCompanyName(companyName);
+        boolean unique = mainCompanyApplyService.checkCompanyNameUnique(bo);
+        return R.ok(unique);
+    }
+
+    /**
+     * 检查统一社会信用代码是否已存在(前端实时校验用)
+     *
+     * @param creditCode 统一社会信用代码
+     * @return true=唯一可用,false=已存在
+     */
+    @GetMapping("/checkCreditCode")
+    public R<Boolean> checkCreditCode(@RequestParam @NotBlank(message = "统一社会信用代码不能为空") String creditCode) {
+        MainCompanyApplyBo bo = new MainCompanyApplyBo();
+        bo.setCreditCode(creditCode);
+        boolean unique = mainCompanyApplyService.checkCreditCodeUnique(bo);
+        return R.ok(unique);
+    }
+
+    /**
+     * 检查联系人手机号是否已被使用(前端实时校验用)
+     *
+     * @param contactPhone 联系人手机号
+     * @return true=唯一可用,false=已存在
+     */
+    @GetMapping("/checkPhone")
+    public R<Boolean> checkPhone(@RequestParam @NotBlank(message = "手机号不能为空") String contactPhone) {
+        MainCompanyApplyBo bo = new MainCompanyApplyBo();
+        bo.setMobile(contactPhone);
+        boolean unique = mainCompanyApplyService.checkMobileUnique(bo);
+        return R.ok(unique);
+    }
+}
+

+ 48 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/PortalOssController.java

@@ -0,0 +1,48 @@
+package org.dromara.main.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.web.core.BaseController;
+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.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * 门户文件上传Controller(公开接口,无需登录)
+ *
+ * @author sj
+ */
+@SaIgnore
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/portal/oss")
+public class PortalOssController extends BaseController {
+
+    private final ISysOssService ossService;
+
+    /**
+     * 上传文件(公开接口,无需登录)
+     *
+     * @param file 文件
+     */
+    @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);
+    }
+}

+ 39 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/Banner.java

@@ -0,0 +1,39 @@
+package org.dromara.main.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+import org.dromara.main.domain.vo.BannerVo;
+
+import java.io.Serial;
+
+/**
+ * 首页轮播图对象 main_banner
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = BannerVo.class)
+@TableName("main_banner")
+public class Banner extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id")
+    private Long id;
+
+    private String title;
+
+    private String imageUrl;
+
+    private String linkUrl;
+
+    private Long sortOrder;
+
+    private Long isEnabled;
+
+    private String remark;
+}

+ 38 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainOrder.java

@@ -0,0 +1,38 @@
+package org.dromara.main.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("main_order")
+public class MainOrder extends BaseEntity {
+
+    @TableId(value = "id")
+    private Long id;
+
+    private String orderNo;
+    private Integer orderType;
+    private Integer buyerType;
+    private Long buyerId;
+    private String buyerName;
+    private Long sellerId;
+    private BigDecimal totalAmount;
+    private BigDecimal paidAmount;
+    private BigDecimal refundAmount;
+    private Integer orderStatus;
+    private Integer payStatus;
+    private Long businessId;
+    private Long productId;
+    private Date payTime;
+    private Date completeTime;
+    private Date cancelTime;
+    private String tenantId;
+    private String remark;
+}

+ 30 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/BannerBo.java

@@ -0,0 +1,30 @@
+package org.dromara.main.domain.bo;
+
+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.Banner;
+
+/**
+ * 首页轮播图业务对象
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = Banner.class, reverseConvertGenerate = false)
+public class BannerBo extends BaseEntity {
+
+    private Long id;
+
+    private String title;
+
+    private String imageUrl;
+
+    private String linkUrl;
+
+    private Long sortOrder;
+
+    private Long isEnabled;
+
+    private String remark;
+}

+ 39 - 2
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/bo/MainCompanyApplyBo.java

@@ -1,26 +1,63 @@
 package org.dromara.main.domain.bo;
 
 import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
 import org.dromara.common.mybatis.core.domain.BaseEntity;
 import org.dromara.main.domain.MainCompanyApply;
 
+import java.io.Serial;
+
 @Data
 @EqualsAndHashCode(callSuper = true)
 @AutoMapper(target = MainCompanyApply.class, reverseConvertGenerate = false)
 public class MainCompanyApplyBo extends BaseEntity {
 
+    @Serial
+    private static final long serialVersionUID = 1L;
+
     private Long id;
     private String applyNo;
+
+    @NotBlank(message = "企业名称不能为空", groups = {AddGroup.class, EditGroup.class})
+    @Size(max = 100, message = "企业名称长度不能超过100个字符")
     private String companyName;
+
+    @NotBlank(message = "官方邮箱不能为空", groups = {AddGroup.class, EditGroup.class})
+    @Email(message = "邮箱格式不正确")
+    @Size(max = 100, message = "邮箱长度不能超过100个字符")
     private String officialAccount;
+
+    @NotBlank(message = "办公地址不能为空", groups = {AddGroup.class, EditGroup.class})
+    @Size(max = 200, message = "办公地址长度不能超过200个字符")
     private String officeAddress;
+
+    @NotBlank(message = "统一社会信用代码不能为空", groups = {AddGroup.class, EditGroup.class})
+    @Pattern(regexp = "^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$", message = "统一社会信用代码格式不正确")
     private String creditCode;
-    private Long authLetter;
-    private Long avatar;
+
+    // 门户端会上传 OSS ID 字符串,入库前再转成 Long。
+    private String authLetter;
+    private String avatar;
+
+    @NotBlank(message = "姓氏不能为空", groups = {AddGroup.class, EditGroup.class})
+    @Size(max = 20, message = "姓氏长度不能超过20个字符")
     private String surname;
+
+    @Size(max = 50, message = "名字长度不能超过50个字符")
     private String name;
+
+    @NotBlank(message = "手机号不能为空", groups = {AddGroup.class, EditGroup.class})
+    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
     private String mobile;
     private Integer applyStatus;
+    @NotBlank(message = "验证码不能为空", groups = {AddGroup.class})
+    private String smsCode;
+    private String keyword;
 }

+ 38 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/BannerVo.java

@@ -0,0 +1,38 @@
+package org.dromara.main.domain.vo;
+
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+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.Banner;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 首页轮播图视图对象
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = Banner.class)
+public class BannerVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+
+    private String title;
+
+    private String imageUrl;
+
+    @Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "imageUrl")
+    private String imageUrlUrl;
+
+    private String linkUrl;
+
+    private Long sortOrder;
+
+    private Long isEnabled;
+}

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

@@ -14,14 +14,21 @@ import java.time.LocalDateTime;
 public class MainBackOrderVo implements Serializable {
 
     private Long id;
+    private Long orderId;
     private String orderNo;
     private String tenantId;
+    private Long companyId;
     private String companyName; // 对应前端“下单企业”
     private Long categoryId;
     private String categoryName;
+    private Integer orderType;
+    private String clauseIds;
+    private BigDecimal unitPrice;
     @TableField("total_amount")
     private BigDecimal totalAmount;
     private String status;
+    private Integer orderStatus;
+    private Integer mainOrderStatus;
     private LocalDateTime createTime;
 
     /** 候选人人数 (需在 Service 中统计或 SQL 关联查询) */

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

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

+ 7 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/MainOrderMapper.java

@@ -0,0 +1,7 @@
+package org.dromara.main.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.main.domain.MainOrder;
+
+public interface MainOrderMapper extends BaseMapperPlus<MainOrder, MainOrder> {
+}

+ 11 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IBannerService.java

@@ -0,0 +1,11 @@
+package org.dromara.main.service;
+
+import org.dromara.main.domain.bo.BannerBo;
+import org.dromara.main.domain.vo.BannerVo;
+
+import java.util.List;
+
+public interface IBannerService {
+
+    List<BannerVo> queryList(BannerBo bo);
+}

+ 7 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IMainBackOrderService.java

@@ -5,6 +5,8 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.main.domain.bo.MainBackOrderBo;
 import org.dromara.main.domain.vo.MainBackOrderVo;
 
+import java.util.List;
+
 /**
  * 背调订单Service接口
  */
@@ -14,4 +16,9 @@ public interface IMainBackOrderService {
      * 查询背调订单分页列表
      */
     TableDataInfo<MainBackOrderVo> queryPageList(MainBackOrderBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询门户当前企业的背调订单列表
+     */
+    List<MainBackOrderVo> queryPortalListByTenantId(String tenantId);
 }

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

@@ -21,4 +21,12 @@ public interface IMainCompanyApplyService {
     Boolean updateByBo(MainCompanyApplyBo bo);
 
     Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+
+    boolean checkCompanyNameUnique(MainCompanyApplyBo bo);
+
+    boolean checkCreditCodeUnique(MainCompanyApplyBo bo);
+
+    boolean checkMobileUnique(MainCompanyApplyBo bo);
+
+    Boolean applyCompany(MainCompanyApplyBo bo);
 }

+ 29 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/BannerServiceImpl.java

@@ -0,0 +1,29 @@
+package org.dromara.main.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import org.dromara.main.domain.Banner;
+import org.dromara.main.domain.bo.BannerBo;
+import org.dromara.main.domain.vo.BannerVo;
+import org.dromara.main.mapper.BannerMapper;
+import org.dromara.main.service.IBannerService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+@Service
+public class BannerServiceImpl implements IBannerService {
+
+    private final BannerMapper baseMapper;
+
+    @Override
+    public List<BannerVo> queryList(BannerBo bo) {
+        LambdaQueryWrapper<Banner> lqw = Wrappers.lambdaQuery();
+        lqw.like(bo.getTitle() != null && !bo.getTitle().isBlank(), Banner::getTitle, bo.getTitle());
+        lqw.eq(bo.getIsEnabled() != null, Banner::getIsEnabled, bo.getIsEnabled());
+        lqw.orderByAsc(Banner::getSortOrder).orderByAsc(Banner::getId);
+        return baseMapper.selectVoList(lqw);
+    }
+}

+ 31 - 16
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/CsMessageServiceImpl.java

@@ -12,10 +12,13 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.oss.core.OssClient;
 import org.dromara.common.oss.entity.UploadResult;
 import org.dromara.common.oss.factory.OssFactory;
+import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.main.domain.CsMessage;
+import org.dromara.main.domain.CsSession;
 import org.dromara.main.domain.bo.CsMessageBo;
 import org.dromara.main.domain.vo.CsMessageVo;
 import org.dromara.main.mapper.CsMessageMapper;
+import org.dromara.main.mapper.CsSessionMapper;
 import org.dromara.main.service.ICsMessageService;
 import org.dromara.main.service.ICsSessionService;
 import org.springframework.stereotype.Service;
@@ -30,6 +33,7 @@ import java.time.LocalDateTime;
 public class CsMessageServiceImpl implements ICsMessageService {
 
     private final CsMessageMapper baseMapper;
+    private final CsSessionMapper sessionMapper;
     private final ICsSessionService sessionService;
     private final Converter converter;
 
@@ -52,19 +56,12 @@ public class CsMessageServiceImpl implements ICsMessageService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public CsMessageVo sendTextMessage(CsMessageBo bo) {
+        Long senderId = resolveSenderId(bo.getSenderId());
         CsMessage message = new CsMessage();
         message.setSessionId(bo.getSessionId());
         message.setMsgNo(bo.getMsgNo());
-        message.setSenderType(2);
-        if (bo.getSenderId() != null) {
-            message.setSenderId(bo.getSenderId());
-        } else {
-            // 使用 RuoYi 系统库的 LoginHelper,或者直接从 bo 中带过来
-            // 这里我建议先临时写死为 1L 用于测试,或者是手动加上:
-            message.setSenderId(org.dromara.common.satoken.utils.LoginHelper.getUserId());
-        }
-
-        message.setSenderId(bo.getSenderId());
+        message.setSenderId(senderId);
+        message.setSenderType(resolveSenderType(bo.getSessionId(), senderId));
         message.setMsgType(bo.getMsgType());
         message.setContent(bo.getContent());
         message.setStatus(1);
@@ -82,6 +79,7 @@ public class CsMessageServiceImpl implements ICsMessageService {
     @Transactional(rollbackFor = Exception.class)
     public CsMessageVo sendImageMessage(Long sessionId, String msgNo, Long senderId, MultipartFile file) {
         try {
+            Long actualSenderId = resolveSenderId(senderId);
             OssClient ossClient = OssFactory.instance();
             String fileName = file.getOriginalFilename();
             InputStream inputStream = file.getInputStream();
@@ -97,8 +95,8 @@ public class CsMessageServiceImpl implements ICsMessageService {
             CsMessage message = new CsMessage();
             message.setSessionId(sessionId);
             message.setMsgNo(msgNo);
-            message.setSenderType(2);
-            message.setSenderId(senderId);
+            message.setSenderId(actualSenderId);
+            message.setSenderType(resolveSenderType(sessionId, actualSenderId));
             message.setMsgType("image");
             message.setFileUrl(uploadResult.getUrl());
             message.setFileName(fileName);
@@ -121,6 +119,7 @@ public class CsMessageServiceImpl implements ICsMessageService {
     @Transactional(rollbackFor = Exception.class)
     public CsMessageVo sendFileMessage(Long sessionId, String msgNo, Long senderId, MultipartFile file) {
         try {
+            Long actualSenderId = resolveSenderId(senderId);
             OssClient ossClient = OssFactory.instance();
             String fileName = file.getOriginalFilename();
             InputStream inputStream = file.getInputStream();
@@ -136,8 +135,8 @@ public class CsMessageServiceImpl implements ICsMessageService {
             CsMessage message = new CsMessage();
             message.setSessionId(sessionId);
             message.setMsgNo(msgNo);
-            message.setSenderType(2);
-            message.setSenderId(senderId);
+            message.setSenderId(actualSenderId);
+            message.setSenderType(resolveSenderType(sessionId, actualSenderId));
             message.setMsgType("file");
             message.setFileUrl(uploadResult.getUrl());
             message.setFileName(fileName);
@@ -159,11 +158,12 @@ public class CsMessageServiceImpl implements ICsMessageService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public CsMessageVo sendJobCard(CsMessageBo bo) {
+        Long senderId = resolveSenderId(bo.getSenderId());
         CsMessage message = new CsMessage();
         message.setSessionId(bo.getSessionId());
         message.setMsgNo(bo.getMsgNo());
-        message.setSenderType(2);
-        message.setSenderId(bo.getSenderId());
+        message.setSenderId(senderId);
+        message.setSenderType(resolveSenderType(bo.getSessionId(), senderId));
         message.setMsgType("job_card");
         message.setPayload(JSONUtil.toJsonStr(bo.getPayload()));
         message.setStatus(1);
@@ -196,4 +196,19 @@ public class CsMessageServiceImpl implements ICsMessageService {
         update.setStatus(2);
         return baseMapper.updateById(update) > 0;
     }
+
+    private Long resolveSenderId(Long senderId) {
+        return senderId != null ? senderId : LoginHelper.getUserId();
+    }
+
+    private Integer resolveSenderType(Long sessionId, Long senderId) {
+        if (senderId == null || sessionId == null) {
+            return 1;
+        }
+        CsSession session = sessionMapper.selectById(sessionId);
+        if (session != null && session.getWaiterId() != null && session.getWaiterId().equals(senderId)) {
+            return 2;
+        }
+        return 1;
+    }
 }

+ 106 - 11
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/CsSessionServiceImpl.java

@@ -9,23 +9,36 @@ 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.CsMessage;
+import org.dromara.main.domain.CsSeatConfig;
+import org.dromara.main.domain.CsSeatWaiter;
 import org.dromara.main.domain.CsSession;
-import org.dromara.main.domain.bo.CsMessageBo;
 import org.dromara.main.domain.bo.CsSessionBo;
-import org.dromara.main.domain.vo.CsMessageVo;
 import org.dromara.main.domain.vo.CsSessionVo;
+import org.dromara.main.mapper.CsSeatConfigMapper;
+import org.dromara.main.mapper.CsSeatWaiterMapper;
 import org.dromara.main.mapper.CsSessionMapper;
 import org.dromara.main.service.ICsSessionService;
+import org.dromara.system.domain.SysUser;
+import org.dromara.system.mapper.SysUserMapper;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 @RequiredArgsConstructor
 @Service
-  public class CsSessionServiceImpl implements ICsSessionService {
+public class CsSessionServiceImpl implements ICsSessionService {
 
     private final CsSessionMapper baseMapper;
+    private final CsSeatConfigMapper seatConfigMapper;
+    private final CsSeatWaiterMapper seatWaiterMapper;
+    private final SysUserMapper sysUserMapper;
     private final Converter converter;
 
     @Override
@@ -40,7 +53,7 @@ import java.time.LocalDateTime;
 
         CsSession existSession = baseMapper.selectOne(lqw);
         if (existSession != null) {
-            return converter.convert(existSession, CsSessionVo.class);
+            return enrichSessionVo(converter.convert(existSession, CsSessionVo.class));
         }
 
         CsSession newSession = new CsSession();
@@ -52,9 +65,10 @@ import java.time.LocalDateTime;
         newSession.setStatus(1);
         newSession.setUnreadCount(0);
         newSession.setCreateTime(LocalDateTime.now());
+        assignSeatAndWaiter(newSession);
 
         baseMapper.insert(newSession);
-        return converter.convert(newSession, CsSessionVo.class);
+        return enrichSessionVo(converter.convert(newSession, CsSessionVo.class));
     }
 
     @Override
@@ -62,7 +76,7 @@ import java.time.LocalDateTime;
         LambdaQueryWrapper<CsSession> lqw = buildQueryWrapper(bo);
         Page<CsSession> page = baseMapper.selectPage(pageQuery.build(), lqw);
         Page<CsSessionVo> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
-        voPage.setRecords(converter.convert(page.getRecords(), CsSessionVo.class));
+        voPage.setRecords(enrichSessionVos(converter.convert(page.getRecords(), CsSessionVo.class)));
         return TableDataInfo.build(voPage);
     }
 
@@ -79,7 +93,7 @@ import java.time.LocalDateTime;
     @Override
     public CsSessionVo queryById(Long sessionId) {
         CsSession entity = baseMapper.selectById(sessionId);
-        return converter.convert(entity, CsSessionVo.class);
+        return enrichSessionVo(converter.convert(entity, CsSessionVo.class));
     }
 
     @Override
@@ -117,11 +131,92 @@ import java.time.LocalDateTime;
     }
 
     private String generateSessionNo(Integer sessionType, Long fromUserId) {
-        String prefix = sessionType == 1 ? "MINI" : "MERCHANT";
-        return String.format("SESSION_%s_%d_%s", prefix, fromUserId,
-            IdUtil.fastSimpleUUID().substring(0, 8));
+        return String.format("CS%s%s", sessionType, IdUtil.getSnowflakeNextIdStr());
     }
 
+    private void assignSeatAndWaiter(CsSession session) {
+        String module = session.getSessionType() != null && session.getSessionType() == 2 ? "merchant" : "mini";
+        List<String> moduleCandidates = "merchant".equals(module)
+            ? Arrays.asList("merchant", "all", "官网", "商家")
+            : Arrays.asList("mini", "all", "小程序");
+
+        List<CsSeatConfig> seats = seatConfigMapper.selectList(
+            Wrappers.lambdaQuery(CsSeatConfig.class)
+                .eq(CsSeatConfig::getStatus, 1)
+                .in(CsSeatConfig::getModule, moduleCandidates)
+                .orderByAsc(CsSeatConfig::getId)
+        );
+        if (seats.isEmpty()) {
+            return;
+        }
+
+        CsSeatConfig seat = seats.get(0);
+        session.setSeatId(seat.getId());
+
+        List<CsSeatWaiter> waiters = seatWaiterMapper.selectList(
+            Wrappers.lambdaQuery(CsSeatWaiter.class)
+                .eq(CsSeatWaiter::getSeatId, seat.getId())
+                .orderByAsc(CsSeatWaiter::getId)
+        );
+        if (!waiters.isEmpty()) {
+            session.setWaiterId(waiters.get(0).getUserId());
+        }
+    }
 
+    private List<CsSessionVo> enrichSessionVos(List<CsSessionVo> sessionVos) {
+        if (sessionVos == null || sessionVos.isEmpty()) {
+            return sessionVos;
+        }
+        Map<Long, SysUser> waiterMap = loadWaiterMap(sessionVos);
+        Map<Long, CsSeatConfig> seatMap = loadSeatMap(sessionVos);
+        sessionVos.forEach(vo -> enrichSessionVo(vo, waiterMap, seatMap));
+        return sessionVos;
+    }
+
+    private CsSessionVo enrichSessionVo(CsSessionVo vo) {
+        if (vo == null) {
+            return null;
+        }
+        Map<Long, SysUser> waiterMap = loadWaiterMap(Collections.singletonList(vo));
+        Map<Long, CsSeatConfig> seatMap = loadSeatMap(Collections.singletonList(vo));
+        enrichSessionVo(vo, waiterMap, seatMap);
+        return vo;
+    }
 
+    private void enrichSessionVo(CsSessionVo vo, Map<Long, SysUser> waiterMap, Map<Long, CsSeatConfig> seatMap) {
+        if (vo.getSeatId() != null && seatMap.containsKey(vo.getSeatId())) {
+            vo.setSeatName(seatMap.get(vo.getSeatId()).getSeatName());
+        }
+        if (vo.getWaiterId() != null && waiterMap.containsKey(vo.getWaiterId())) {
+            SysUser waiter = waiterMap.get(vo.getWaiterId());
+            vo.setWaiterName(StringUtils.defaultIfBlank(waiter.getNickName(), waiter.getUserName()));
+            vo.setIsOnline("0".equals(waiter.getStatus()));
+        }
+    }
+
+    private Map<Long, SysUser> loadWaiterMap(List<CsSessionVo> sessionVos) {
+        List<Long> waiterIds = sessionVos.stream()
+            .map(CsSessionVo::getWaiterId)
+            .filter(Objects::nonNull)
+            .distinct()
+            .collect(Collectors.toList());
+        if (waiterIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return sysUserMapper.selectBatchIds(waiterIds).stream()
+            .collect(Collectors.toMap(SysUser::getUserId, Function.identity()));
+    }
+
+    private Map<Long, CsSeatConfig> loadSeatMap(List<CsSessionVo> sessionVos) {
+        List<Long> seatIds = sessionVos.stream()
+            .map(CsSessionVo::getSeatId)
+            .filter(Objects::nonNull)
+            .distinct()
+            .collect(Collectors.toList());
+        if (seatIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return seatConfigMapper.selectBatchIds(seatIds).stream()
+            .collect(Collectors.toMap(CsSeatConfig::getId, Function.identity()));
+    }
 }

+ 78 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainBackOrderServiceImpl.java

@@ -2,25 +2,33 @@ package org.dromara.main.service.impl;
 
 import cn.hutool.core.util.StrUtil;
 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.MapstructUtils;
 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.MainBackOrder;
 import org.dromara.main.domain.MainBackRecord;
+import org.dromara.main.domain.MainOrder;
 import org.dromara.main.domain.bo.MainBackOrderBo;
 import org.dromara.main.domain.vo.MainBackOrderVo;
 import org.dromara.main.mapper.MainBackOrderMapper;
 import org.dromara.main.mapper.MainBackRecordMapper;
+import org.dromara.main.mapper.MainOrderMapper;
 import org.dromara.main.service.IMainBackOrderService;
 import org.dromara.system.domain.bo.SysTenantBo;
 import org.dromara.system.domain.vo.SysTenantVo;
 import org.dromara.system.service.ISysTenantService;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 @RequiredArgsConstructor
 @Service
@@ -28,6 +36,7 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
 
     private final MainBackOrderMapper baseMapper;
     private final MainBackRecordMapper recordMapper;
+    private final MainOrderMapper mainOrderMapper;
     private final ISysTenantService tenantService;
 
     @Override
@@ -78,6 +87,75 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
 
         return result;
     }
+
+    @Override
+    public List<MainBackOrderVo> queryPortalListByTenantId(String tenantId) {
+        LambdaQueryWrapper<MainOrder> orderLqw = Wrappers.lambdaQuery();
+        orderLqw.eq(StrUtil.isNotBlank(tenantId), MainOrder::getTenantId, tenantId)
+            .in(MainOrder::getOrderType, List.of(3, 4))
+            .isNotNull(MainOrder::getBusinessId)
+            .orderByDesc(MainOrder::getCreateTime);
+
+        List<MainOrder> mainOrders = mainOrderMapper.selectList(orderLqw);
+        if (mainOrders.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        List<Long> businessIds = mainOrders.stream()
+            .map(MainOrder::getBusinessId)
+            .distinct()
+            .collect(Collectors.toList());
+
+        Map<Long, MainBackOrder> backOrderMap = new LinkedHashMap<>();
+        for (MainBackOrder backOrder : baseMapper.selectBatchIds(businessIds)) {
+            backOrderMap.put(backOrder.getId(), backOrder);
+        }
+
+        List<MainBackOrderVo> result = new ArrayList<>();
+        for (MainOrder mainOrder : mainOrders) {
+            MainBackOrder backOrder = backOrderMap.get(mainOrder.getBusinessId());
+            if (backOrder == null) {
+                continue;
+            }
+            MainBackOrderVo vo = MapstructUtils.convert(backOrder, MainBackOrderVo.class);
+            fillPortalFields(vo, mainOrder);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    private void fillPortalFields(MainBackOrderVo vo, MainOrder mainOrder) {
+        Long count = recordMapper.selectCount(
+            new LambdaQueryWrapper<MainBackRecord>()
+                .eq(MainBackRecord::getOrderId, vo.getId())
+        );
+        vo.setCandidateCount(count);
+        vo.setOrderId(mainOrder.getId());
+        vo.setOrderNo(mainOrder.getOrderNo());
+        vo.setCompanyId(mainOrder.getBuyerId());
+        vo.setTenantId(mainOrder.getTenantId());
+        vo.setTotalAmount(mainOrder.getTotalAmount());
+        vo.setOrderType(vo.getCategoryId() != null ? 1 : 2);
+        vo.setOrderStatus(parseIntOrDefault(vo.getStatus(), 0));
+        vo.setMainOrderStatus(mainOrder.getOrderStatus());
+
+        if (count != null && count > 0 && vo.getTotalAmount() != null) {
+            vo.setUnitPrice(vo.getTotalAmount().divide(BigDecimal.valueOf(count), 2, java.math.RoundingMode.HALF_UP));
+        } else {
+            vo.setUnitPrice(BigDecimal.ZERO);
+        }
+    }
+
+    private Integer parseIntOrDefault(String value, int defaultValue) {
+        if (StringUtils.isBlank(value)) {
+            return defaultValue;
+        }
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException ignored) {
+            return defaultValue;
+        }
+    }
 }
 
 

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

@@ -1,19 +1,27 @@
 package org.dromara.main.service.impl;
 
+import cn.hutool.core.util.RandomUtil;
 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.constant.GlobalConstants;
+import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
+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.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.vo.MainCompanyApplyVo;
+import org.dromara.main.mapper.MainAuditMapper;
 import org.dromara.main.mapper.MainCompanyApplyMapper;
 import org.dromara.main.service.IMainCompanyApplyService;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.Collection;
 import java.util.List;
@@ -23,6 +31,7 @@ import java.util.List;
 public class MainCompanyApplyServiceImpl implements IMainCompanyApplyService {
 
     private final MainCompanyApplyMapper baseMapper;
+    private final MainAuditMapper mainAuditMapper;
 
     @Override
     public TableDataInfo<MainCompanyApplyVo> queryPageList(MainCompanyApplyBo bo, PageQuery pageQuery) {
@@ -65,6 +74,81 @@ public class MainCompanyApplyServiceImpl implements IMainCompanyApplyService {
         return baseMapper.deleteBatchIds(ids) > 0;
     }
 
+    @Override
+    public boolean checkCompanyNameUnique(MainCompanyApplyBo bo) {
+        LambdaQueryWrapper<MainCompanyApply> lqw = Wrappers.lambdaQuery();
+        lqw.eq(MainCompanyApply::getCompanyName, bo.getCompanyName());
+        lqw.ne(bo.getId() != null, MainCompanyApply::getId, bo.getId());
+        return baseMapper.selectCount(lqw) == 0;
+    }
+
+    @Override
+    public boolean checkCreditCodeUnique(MainCompanyApplyBo bo) {
+        LambdaQueryWrapper<MainCompanyApply> lqw = Wrappers.lambdaQuery();
+        lqw.eq(MainCompanyApply::getCreditCode, bo.getCreditCode());
+        lqw.ne(bo.getId() != null, MainCompanyApply::getId, bo.getId());
+        return baseMapper.selectCount(lqw) == 0;
+    }
+
+    @Override
+    public boolean checkMobileUnique(MainCompanyApplyBo bo) {
+        LambdaQueryWrapper<MainCompanyApply> lqw = Wrappers.lambdaQuery();
+        lqw.eq(MainCompanyApply::getMobile, bo.getMobile());
+        lqw.ne(bo.getId() != null, MainCompanyApply::getId, bo.getId());
+        return baseMapper.selectCount(lqw) == 0;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean applyCompany(MainCompanyApplyBo bo) {
+        String smsKey = GlobalConstants.CAPTCHA_CODE_KEY + bo.getMobile();
+        String cachedCode = RedisUtils.getCacheObject(smsKey);
+        if (StringUtils.isBlank(cachedCode)) {
+            throw new ServiceException("验证码已过期,请重新获取");
+        }
+        if (!StringUtils.equals(cachedCode, bo.getSmsCode())) {
+            throw new ServiceException("验证码错误");
+        }
+        RedisUtils.deleteObject(smsKey);
+
+        if (!checkCompanyNameUnique(bo)) {
+            throw new ServiceException("企业名称已存在");
+        }
+        if (!checkCreditCodeUnique(bo)) {
+            throw new ServiceException("统一社会信用代码已存在");
+        }
+        if (!checkMobileUnique(bo)) {
+            throw new ServiceException("联系人手机号已被使用");
+        }
+
+        MainCompanyApply apply = new MainCompanyApply();
+        apply.setApplyNo(generateApplyNo());
+        apply.setCompanyName(bo.getCompanyName());
+        apply.setOfficialAccount(bo.getOfficialAccount());
+        apply.setOfficeAddress(bo.getOfficeAddress());
+        apply.setCreditCode(bo.getCreditCode());
+        apply.setSurname(bo.getSurname());
+        apply.setName(bo.getName());
+        apply.setMobile(bo.getMobile());
+        apply.setApplyStatus(0);
+        apply.setAuthLetter(parseFirstOssId(bo.getAuthLetter()));
+        apply.setAvatar(parseFirstOssId(bo.getAvatar()));
+
+        validEntityBeforeSave(apply);
+        boolean insertResult = baseMapper.insert(apply) > 0;
+        if (insertResult) {
+            MainAudit audit = new MainAudit();
+            audit.setAuditType(1);
+            audit.setTargetId(apply.getId());
+            audit.setAuditResult(0);
+            mainAuditMapper.insert(audit);
+
+            apply.setAuditId(audit.getId());
+            baseMapper.updateById(apply);
+        }
+        return insertResult;
+    }
+
     private LambdaQueryWrapper<MainCompanyApply> buildQueryWrapper(MainCompanyApplyBo bo) {
         LambdaQueryWrapper<MainCompanyApply> lqw = Wrappers.lambdaQuery();
         lqw.like(ObjectUtil.isNotNull(bo.getApplyNo()), MainCompanyApply::getApplyNo, bo.getApplyNo());
@@ -78,4 +162,24 @@ public class MainCompanyApplyServiceImpl implements IMainCompanyApplyService {
 
     private void validEntityBeforeSave(MainCompanyApply entity) {
     }
+
+    private String generateApplyNo() {
+        String dateStr = cn.hutool.core.date.DateUtil.format(cn.hutool.core.date.DateUtil.date(), "yyyyMMdd");
+        return "AP" + dateStr + RandomUtil.randomNumbers(6);
+    }
+
+    private Long parseFirstOssId(String rawValue) {
+        if (StringUtils.isBlank(rawValue)) {
+            return null;
+        }
+        String first = rawValue.split(",")[0].trim();
+        if (StringUtils.isBlank(first)) {
+            return null;
+        }
+        try {
+            return Long.valueOf(first);
+        } catch (NumberFormatException ex) {
+            throw new ServiceException("上传文件标识格式错误");
+        }
+    }
 }