diff --git a/ry-api/ruoyi-business/pom.xml b/ry-api/ruoyi-business/pom.xml
index 519e7fe..b21ee2e 100644
--- a/ry-api/ruoyi-business/pom.xml
+++ b/ry-api/ruoyi-business/pom.xml
@@ -20,6 +20,10 @@
com.ruoyi
ruoyi-common
+
+ com.ruoyi
+ ruoyi-framework
+
com.ruoyi
ruoyi-system
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizArticleController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizArticleController.java
new file mode 100644
index 0000000..fb30791
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizArticleController.java
@@ -0,0 +1,69 @@
+package com.ruoyi.business.controller;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.business.domain.BizArticle;
+import com.ruoyi.business.service.IBizArticleService;
+import com.ruoyi.common.utils.SecurityUtils;
+
+/**
+ * 平台协议/隐私政策文章 Controller (后台 admin)
+ */
+@RestController
+@RequestMapping("/business/article")
+public class BizArticleController extends BaseController
+{
+ @Autowired
+ private IBizArticleService articleService;
+
+ @PreAuthorize("@ss.hasPermi('business:article:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(BizArticle entity)
+ {
+ startPage();
+ List list = articleService.selectList(entity);
+ return getDataTable(list);
+ }
+
+ @PreAuthorize("@ss.hasPermi('business:article:query')")
+ @GetMapping("/{id}")
+ public AjaxResult getInfo(@PathVariable("id") Long id)
+ {
+ return success(articleService.getById(id));
+ }
+
+ @PreAuthorize("@ss.hasPermi('business:article:add')")
+ @Log(title = "平台协议", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody BizArticle entity)
+ {
+ if (entity.getStatus() == null) entity.setStatus("0");
+ entity.setCreateBy(SecurityUtils.getUsername());
+ entity.setUpdateBy(SecurityUtils.getUsername());
+ return toAjax(articleService.insert(entity));
+ }
+
+ @PreAuthorize("@ss.hasPermi('business:article:edit')")
+ @Log(title = "平台协议", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody BizArticle entity)
+ {
+ entity.setUpdateBy(SecurityUtils.getUsername());
+ return toAjax(articleService.updateByPrimaryKey(entity));
+ }
+
+ @PreAuthorize("@ss.hasPermi('business:article:remove')")
+ @Log(title = "平台协议", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{ids}")
+ public AjaxResult remove(@PathVariable Long[] ids)
+ {
+ return toAjax(articleService.deleteByPrimaryKeys(ids));
+ }
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java
index ff078e3..1bd5579 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java
@@ -2,6 +2,9 @@ package com.ruoyi.business.controller;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -11,9 +14,19 @@ import com.ruoyi.business.domain.BizOrg;
import com.ruoyi.business.dto.SmsValidForm;
import com.ruoyi.business.service.IBizOrgService;
import com.ruoyi.business.service.SysSmsService;
+import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.enums.UserStatus;
+import com.ruoyi.common.utils.ip.IpUtils;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.framework.manager.AsyncManager;
+import com.ruoyi.framework.manager.factory.AsyncFactory;
+import com.ruoyi.framework.web.service.SysPermissionService;
+import com.ruoyi.framework.web.service.TokenService;
+import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.service.ISysUserService;
/**
@@ -27,6 +40,10 @@ import com.ruoyi.system.service.ISysUserService;
* 注: 主账号不建 biz_person
*
* 2026-08-16 重构: 删 biz_user_role_bind, 统一用 sys_user.role_type (见迁移脚本)
+ *
+ * 2026-08-16 新增: 短信验证码登录 (loginMode=sms):
+ * 1. smsSendCode 发送短信 (校验手机号已注册)
+ * 2. smsLogin 校验短信验证码 + 颁发 JWT token (不走 AuthenticationManager, 因为无密码)
*/
@RestController
@RequestMapping("/business/auth")
@@ -44,6 +61,15 @@ public class BizAuthController extends BaseController {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
+ @Autowired
+ private SysUserMapper userMapper;
+
+ @Autowired
+ private TokenService tokenService;
+
+ @Autowired
+ private SysPermissionService permissionService;
+
@PostMapping("/login")
public AjaxResult login(@RequestBody Map body) {
String username = (String) body.get("username");
@@ -52,6 +78,82 @@ public class BizAuthController extends BaseController {
return success().put("roleType", roleType).put("token", "mock-token");
}
+ /**
+ * 登录用 - 发送短信验证码 (手机号必须已注册)
+ */
+ @PostMapping("/smsSendCode")
+ public AjaxResult smsSendCode(@RequestBody Map body) {
+ String phone = body == null ? null : body.get("phone");
+ if (phone == null || !phone.matches("^1\\d{10}$")) {
+ return error("请输入正确的手机号");
+ }
+ // 登录反义: 手机号未注册则不发
+ if (!userService.isPhoneRegistered(phone)) {
+ return error("该手机号未注册, 请先注册账号");
+ }
+ String uuid = smsService.sendCode(phone);
+ return AjaxResult.success("验证码已发送", uuid);
+ }
+
+ /**
+ * 短信验证码登录 (无密码)
+ * 1. 校验手机号 + 短信验证码
+ * 2. 查用户 (按手机号)
+ * 3. 校验用户状态
+ * 4. 构造 LoginUser, 颁发 JWT token (SecurityContext 一并设置, 兼容后续鉴权)
+ * 5. 记录登录信息 + logininfor
+ */
+ @PostMapping("/smsLogin")
+ public AjaxResult smsLogin(@RequestBody Map body) {
+ String phone = body == null ? null : body.get("phone");
+ String code = body == null ? null : body.get("smsCode");
+ String uuid = body == null ? null : body.get("uuid");
+
+ // 1. 基础校验
+ if (phone == null || !phone.matches("^1\\d{10}$")) return error("手机号格式错误");
+ if (code == null || code.isEmpty()) return error("请输入短信验证码");
+
+ // 2. 校验短信验证码
+ SmsValidForm smsForm = new SmsValidForm();
+ smsForm.setPhone(phone);
+ smsForm.setSmsCode(code);
+ smsForm.setUuid(uuid);
+ try {
+ smsService.verifyCode(smsForm);
+ } catch (RuntimeException e) {
+ return error("验证码错误或已过期: " + e.getMessage());
+ }
+
+ // 3. 查用户 (mapper.checkPhoneUnique 返回 SysUser 或 null)
+ SysUser user = userMapper.checkPhoneUnique(phone);
+ if (user == null || user.getUserId() == null) {
+ return error("该手机号未注册");
+ }
+
+ // 4. 校验用户状态
+ if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
+ return error("账号已被删除");
+ }
+ if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+ return error("账号已被停用");
+ }
+
+ // 5. 构造 LoginUser 并设 SecurityContext (兼容后续 spring security 鉴权)
+ LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
+ Authentication authentication = new UsernamePasswordAuthenticationToken(
+ loginUser, null, loginUser.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ // 6. 记录登录信息 + logininfor
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(user.getUserName(), Constants.LOGIN_SUCCESS,
+ "短信验证码登录成功"));
+ userService.updateLoginInfo(user.getUserId(), IpUtils.getIpAddr(), DateUtils.getNowDate());
+
+ // 7. 颁发 token
+ String token = tokenService.createToken(loginUser);
+ return AjaxResult.success("登录成功").put(Constants.TOKEN, token);
+ }
+
@PostMapping("/registerExecutor")
public AjaxResult registerExecutor(@RequestBody Map body) {
String username = (String) body.get("username");
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutorController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutorController.java
index b0a1226..beaa7d4 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutorController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExecutorController.java
@@ -39,8 +39,9 @@ public class BizExecutorController extends BaseController
@RequestParam(required = false, defaultValue = "50") Integer pageSize)
{
StringBuilder sql = new StringBuilder()
+ // 仅返回 MAIN 主账号: 排除 SUB 子账号, 避免项目分配下拉混入下属执行人
.append("SELECT user_id, dept_id, user_name, nick_name, email, phonenumber, role_type, status, create_time ")
- .append("FROM sys_user WHERE del_flag = '0' AND status = '0' AND role_type = ?");
+ .append("FROM sys_user WHERE del_flag = '0' AND status = '0' AND role_type = ? AND account_type = 'MAIN'");
List