Binary file not shown.
@@ -0,0 +1,147 @@
|
|||||||
|
package com.bidding.supplier.security.openapi;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supplier account API codec delivered to the customer for payload decryption
|
||||||
|
* and BCrypt password verification.
|
||||||
|
*/
|
||||||
|
public class BiddingSupplierAccountApiCodec
|
||||||
|
{
|
||||||
|
private static final String PROTOCOL = "BSA";
|
||||||
|
private static final String VERSION = "v1";
|
||||||
|
private static final int IV_LENGTH = 12;
|
||||||
|
private static final int GCM_TAG_BITS = 128;
|
||||||
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||||
|
private static final BCryptPasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();
|
||||||
|
|
||||||
|
private final String keyId;
|
||||||
|
private final String prefix;
|
||||||
|
private final byte[] aesKey;
|
||||||
|
|
||||||
|
public BiddingSupplierAccountApiCodec(String keyId, String aesKeyBase64)
|
||||||
|
{
|
||||||
|
if (keyId == null || !keyId.matches("^[A-Za-z0-9_-]{1,32}$"))
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Invalid supplier account API key id");
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.aesKey = Base64.getDecoder().decode(aesKeyBase64 == null ? "" : aesKeyBase64.trim());
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException exception)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Invalid supplier account API AES key", exception);
|
||||||
|
}
|
||||||
|
if (aesKey.length != 32)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Supplier account API AES key must be 32 bytes");
|
||||||
|
}
|
||||||
|
this.keyId = keyId;
|
||||||
|
this.prefix = PROTOCOL + "." + VERSION + "." + keyId + ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String decryptPayload(String payload)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
String[] parts = parsePayload(payload);
|
||||||
|
byte[] iv = Base64.getUrlDecoder().decode(parts[3]);
|
||||||
|
byte[] ciphertextAndTag = Base64.getUrlDecoder().decode(parts[4]);
|
||||||
|
if (iv.length != IV_LENGTH || ciphertextAndTag.length <= GCM_TAG_BITS / 8)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Invalid supplier account API payload");
|
||||||
|
}
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey, "AES"),
|
||||||
|
new GCMParameterSpec(GCM_TAG_BITS, iv));
|
||||||
|
return new String(cipher.doFinal(ciphertextAndTag), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException exception)
|
||||||
|
{
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Unable to decrypt supplier account API payload", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T decryptPayload(String payload, Class<T> targetType)
|
||||||
|
{
|
||||||
|
if (targetType == null)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Target type is required");
|
||||||
|
}
|
||||||
|
return JSON.parseObject(decryptPayload(payload), targetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a BCrypt hash for a new or changed customer-platform password.
|
||||||
|
*/
|
||||||
|
public String encodePassword(String rawPassword)
|
||||||
|
{
|
||||||
|
if (rawPassword == null)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Raw password is required");
|
||||||
|
}
|
||||||
|
return PASSWORD_ENCODER.encode(rawPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matchesPassword(String rawPassword, String bcryptPasswordHash)
|
||||||
|
{
|
||||||
|
if (rawPassword == null || bcryptPasswordHash == null || bcryptPasswordHash.isBlank())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return PASSWORD_ENCODER.matches(rawPassword, bcryptPasswordHash);
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String encryptPayload(Object payload)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] iv = new byte[IV_LENGTH];
|
||||||
|
SECURE_RANDOM.nextBytes(iv);
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey, "AES"),
|
||||||
|
new GCMParameterSpec(GCM_TAG_BITS, iv));
|
||||||
|
byte[] ciphertextAndTag = cipher.doFinal(JSON.toJSONBytes(payload));
|
||||||
|
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
|
||||||
|
return prefix + encoder.encodeToString(iv) + "." + encoder.encodeToString(ciphertextAndTag);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
throw new IllegalStateException("Unable to encrypt supplier account API payload", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] parsePayload(String payload)
|
||||||
|
{
|
||||||
|
if (payload == null || !payload.startsWith(prefix))
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Invalid supplier account API payload");
|
||||||
|
}
|
||||||
|
String[] parts = payload.split("\\.", 5);
|
||||||
|
if (parts.length != 5 || !PROTOCOL.equals(parts[0]) || !VERSION.equals(parts[1])
|
||||||
|
|| !keyId.equals(parts[2]))
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Invalid supplier account API payload");
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -23,6 +23,9 @@ import com.ruoyi.framework.web.service.SysPermissionService;
|
|||||||
import com.ruoyi.framework.web.service.TokenService;
|
import com.ruoyi.framework.web.service.TokenService;
|
||||||
import com.ruoyi.system.service.ISysConfigService;
|
import com.ruoyi.system.service.ISysConfigService;
|
||||||
import com.ruoyi.system.service.ISysMenuService;
|
import com.ruoyi.system.service.ISysMenuService;
|
||||||
|
import com.ruoyi.system.service.ISysUserService;
|
||||||
|
import com.ruoyi.business.domain.BizExpert;
|
||||||
|
import com.ruoyi.business.service.IBizExpertService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录验证
|
* 登录验证
|
||||||
@@ -47,6 +50,12 @@ public class SysLoginController
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ISysConfigService configService;
|
private ISysConfigService configService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ISysUserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IBizExpertService expertService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录方法
|
* 登录方法
|
||||||
*
|
*
|
||||||
@@ -56,6 +65,11 @@ public class SysLoginController
|
|||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public AjaxResult login(@RequestBody LoginBody loginBody)
|
public AjaxResult login(@RequestBody LoginBody loginBody)
|
||||||
{
|
{
|
||||||
|
// 待审核专家拦截: role_type=doctor 且 biz_expert.audit_status='1' → 禁止登录
|
||||||
|
if (isPendingExpert(loginBody.getUsername()))
|
||||||
|
{
|
||||||
|
return AjaxResult.error("您的信息正在审核中");
|
||||||
|
}
|
||||||
AjaxResult ajax = AjaxResult.success();
|
AjaxResult ajax = AjaxResult.success();
|
||||||
// 生成令牌
|
// 生成令牌
|
||||||
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
|
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
|
||||||
@@ -64,6 +78,29 @@ public class SysLoginController
|
|||||||
return ajax;
|
return ajax;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断用户是否为待审核专家 (role_type=doctor 且 biz_expert.audit_status='1')
|
||||||
|
*/
|
||||||
|
private boolean isPendingExpert(String username)
|
||||||
|
{
|
||||||
|
if (username == null || username.isEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SysUser probe = userService.selectUserByUserName(username);
|
||||||
|
// 邮箱登录兜底 (用户名查不到且含 @ 时按邮箱反查)
|
||||||
|
if (probe == null && username.contains("@"))
|
||||||
|
{
|
||||||
|
probe = userService.selectUserByEmail(username);
|
||||||
|
}
|
||||||
|
if (probe == null || probe.getUserId() == null || !"doctor".equals(probe.getRoleType()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BizExpert expert = expertService.getByUserId(probe.getUserId());
|
||||||
|
return expert != null && "1".equals(expert.getAuditStatus());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户信息
|
* 获取用户信息
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ ruoyi:
|
|||||||
# 阿里云短信配置 (hwt-code ali.sms 模式)
|
# 阿里云短信配置 (hwt-code ali.sms 模式)
|
||||||
# dev/prod 区分走代码: phone 以 "10" 开头视为 dev 测试 (固定码 1234), 其它走 aliyun 真发
|
# dev/prod 区分走代码: phone 以 "10" 开头视为 dev 测试 (固定码 1234), 其它走 aliyun 真发
|
||||||
sms:
|
sms:
|
||||||
accessKeyId: LTAI5t7S88DmdTxHPzPtJTwG
|
accessKeyId: LTAI5tAgeAUviVdPSsxYCkPY
|
||||||
accessKeySecret: UIkwjMpmlYjgX5IMLjPj8FQNPthdlR
|
accessKeySecret: 556LX6mXnJIPZgf0vFXcl4mKCzzgKq
|
||||||
signName: 北京仙仁掌医学科技发展
|
signName: 北京整合医学学会
|
||||||
template: SMS_321560247
|
template: SMS_291440833
|
||||||
esignTemplate: SMS_492460505
|
esignTemplate: SMS_512040098
|
||||||
esignBaseUrl: https://risingdoctor.com/hg
|
esignBaseUrl: https://hegui.bahim.org.cn
|
||||||
endpoint: dysmsapi.aliyuncs.com
|
endpoint: dysmsapi.aliyuncs.com
|
||||||
regionId: cn-hangzhou
|
regionId: cn-hangzhou
|
||||||
# 发票 OCR (本地 Java 识别, PaddleOCR ONNX Runtime, 替代原 ry-ocr Python 微服务)
|
# 发票 OCR (本地 Java 识别, PaddleOCR ONNX Runtime, 替代原 ry-ocr Python 微服务)
|
||||||
|
|||||||
+17
-2
@@ -14,8 +14,10 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import com.ruoyi.business.domain.BizOrg;
|
import com.ruoyi.business.domain.BizOrg;
|
||||||
import com.ruoyi.business.domain.BizPerson;
|
import com.ruoyi.business.domain.BizPerson;
|
||||||
|
import com.ruoyi.business.domain.BizExpert;
|
||||||
import com.ruoyi.business.dto.SmsValidForm;
|
import com.ruoyi.business.dto.SmsValidForm;
|
||||||
import com.ruoyi.business.mapper.BizPersonMapper;
|
import com.ruoyi.business.mapper.BizPersonMapper;
|
||||||
|
import com.ruoyi.business.service.IBizExpertService;
|
||||||
import com.ruoyi.business.service.IBizOrgService;
|
import com.ruoyi.business.service.IBizOrgService;
|
||||||
import com.ruoyi.business.service.SysSmsService;
|
import com.ruoyi.business.service.SysSmsService;
|
||||||
import com.ruoyi.common.utils.id.SnowflakeId;
|
import com.ruoyi.common.utils.id.SnowflakeId;
|
||||||
@@ -64,6 +66,9 @@ public class BizAuthController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IBizOrgService bizOrgService;
|
private IBizOrgService bizOrgService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IBizExpertService expertService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BizPersonMapper bizPersonMapper;
|
private BizPersonMapper bizPersonMapper;
|
||||||
|
|
||||||
@@ -165,6 +170,14 @@ public class BizAuthController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4.6 待审核专家拦截: role_type=doctor 且 biz_expert.audit_status='1' → 禁止登录
|
||||||
|
if ("doctor".equals(user.getRoleType())) {
|
||||||
|
BizExpert expert = expertService.getByUserId(user.getUserId());
|
||||||
|
if (expert != null && "1".equals(expert.getAuditStatus())) {
|
||||||
|
return error("您的信息正在审核中");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 5. 构造 LoginUser 并设 SecurityContext (兼容后续 spring security 鉴权)
|
// 5. 构造 LoginUser 并设 SecurityContext (兼容后续 spring security 鉴权)
|
||||||
LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
|
LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
|
||||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||||
@@ -288,6 +301,7 @@ public class BizAuthController extends BaseController {
|
|||||||
public AjaxResult registerSponsor(@RequestBody Map<String, Object> body) {
|
public AjaxResult registerSponsor(@RequestBody Map<String, Object> body) {
|
||||||
String username = (String) body.get("username");
|
String username = (String) body.get("username");
|
||||||
String orgIdStr = body.get("orgId") == null ? null : body.get("orgId").toString();
|
String orgIdStr = body.get("orgId") == null ? null : body.get("orgId").toString();
|
||||||
|
String realName = (String) body.get("realName");
|
||||||
String phone = (String) body.get("phone");
|
String phone = (String) body.get("phone");
|
||||||
String code = (String) body.get("smsCode");
|
String code = (String) body.get("smsCode");
|
||||||
String password = (String) body.get("password");
|
String password = (String) body.get("password");
|
||||||
@@ -298,6 +312,7 @@ public class BizAuthController extends BaseController {
|
|||||||
if (username == null || username.length() < 4 || username.length() > 20) return error("用户名长度 4-20 位");
|
if (username == null || username.length() < 4 || username.length() > 20) return error("用户名长度 4-20 位");
|
||||||
if (!username.matches("^[A-Za-z0-9_]+$")) return error("用户名只能包含字母/数字/下划线");
|
if (!username.matches("^[A-Za-z0-9_]+$")) return error("用户名只能包含字母/数字/下划线");
|
||||||
if (orgIdStr == null || orgIdStr.isEmpty()) return error("请选择企业");
|
if (orgIdStr == null || orgIdStr.isEmpty()) return error("请选择企业");
|
||||||
|
if (realName == null || realName.trim().isEmpty()) return error("请输入联系人姓名");
|
||||||
if (phone == null || !phone.matches("^1\\d{10}$")) return error("手机号格式错误");
|
if (phone == null || !phone.matches("^1\\d{10}$")) return error("手机号格式错误");
|
||||||
if (code == null || code.isEmpty()) return error("请输入短信验证码");
|
if (code == null || code.isEmpty()) return error("请输入短信验证码");
|
||||||
if (password == null || password.length() < 6 || password.length() > 20) return error("密码长度 6-20 位");
|
if (password == null || password.length() < 6 || password.length() > 20) return error("密码长度 6-20 位");
|
||||||
@@ -340,7 +355,7 @@ public class BizAuthController extends BaseController {
|
|||||||
// role_type 显式写 sponsor, 避免被 sys_user.role_type DB DEFAULT 'executor' 覆盖
|
// role_type 显式写 sponsor, 避免被 sys_user.role_type DB DEFAULT 'executor' 覆盖
|
||||||
SysUser user = new SysUser();
|
SysUser user = new SysUser();
|
||||||
user.setUserName(username);
|
user.setUserName(username);
|
||||||
user.setNickName(org.getOrgName()); // 昵称用所选企业名
|
user.setNickName(realName); // 昵称用联系人姓名
|
||||||
user.setPhonenumber(phone);
|
user.setPhonenumber(phone);
|
||||||
user.setPassword(passwordEncoder.encode(password));
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
user.setStatus("0");
|
user.setStatus("0");
|
||||||
@@ -353,7 +368,7 @@ public class BizAuthController extends BaseController {
|
|||||||
// 6. 写 biz_person (关联到所选企业, 不再新建 biz_org)
|
// 6. 写 biz_person (关联到所选企业, 不再新建 biz_org)
|
||||||
BizPerson self = new BizPerson();
|
BizPerson self = new BizPerson();
|
||||||
SnowflakeId.injectIfEmpty(self, "personId");
|
SnowflakeId.injectIfEmpty(self, "personId");
|
||||||
self.setName(username); // 表单无姓名字段, 用登录用户名占位
|
self.setName(realName);
|
||||||
self.setPhone(phone);
|
self.setPhone(phone);
|
||||||
self.setOrgId(orgId);
|
self.setOrgId(orgId);
|
||||||
self.setDepartment("待分配");
|
self.setDepartment("待分配");
|
||||||
|
|||||||
+1
-1
@@ -68,7 +68,7 @@ public class BizExecutionIntentController extends BaseController
|
|||||||
SysUser u = sysUserMapper.selectUserById(uid);
|
SysUser u = sysUserMapper.selectUserById(uid);
|
||||||
if (u != null) {
|
if (u != null) {
|
||||||
if (bizExecutionIntent.getName() == null || bizExecutionIntent.getName().isEmpty())
|
if (bizExecutionIntent.getName() == null || bizExecutionIntent.getName().isEmpty())
|
||||||
bizExecutionIntent.setName(u.getUserName());
|
bizExecutionIntent.setName(u.getNickName());
|
||||||
if (bizExecutionIntent.getPhone() == null || bizExecutionIntent.getPhone().isEmpty())
|
if (bizExecutionIntent.getPhone() == null || bizExecutionIntent.getPhone().isEmpty())
|
||||||
bizExecutionIntent.setPhone(u.getPhonenumber());
|
bizExecutionIntent.setPhone(u.getPhonenumber());
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -94,8 +94,8 @@ public class BizMeetingAttendeeController extends BaseController {
|
|||||||
@PostMapping
|
@PostMapping
|
||||||
public AjaxResult add(@RequestBody BizMeetingAttendee body) {
|
public AjaxResult add(@RequestBody BizMeetingAttendee body) {
|
||||||
Long attendeeId = attendeeService.insertByPhoneWithProfile(body);
|
Long attendeeId = attendeeService.insertByPhoneWithProfile(body);
|
||||||
// 人员变化 → 会议费用待重算
|
// 人员变化 → 立即重算劳务费 (不碰会务费/不置统计中)
|
||||||
bizMeetingService.markFeeCalcPending(body.getMeetingId());
|
bizMeetingService.recomputeLaborFee(body.getMeetingId());
|
||||||
// 邀请参会已改为手动, 不再自动推"会议邀请", 由前端"邀请参会"按钮触发
|
// 邀请参会已改为手动, 不再自动推"会议邀请", 由前端"邀请参会"按钮触发
|
||||||
return success(attendeeId);
|
return success(attendeeId);
|
||||||
}
|
}
|
||||||
@@ -113,9 +113,9 @@ public class BizMeetingAttendeeController extends BaseController {
|
|||||||
BizMeetingAttendee before = attendeeService.selectById(body.getId());
|
BizMeetingAttendee before = attendeeService.selectById(body.getId());
|
||||||
body.setUpdateBy(SecurityUtils.getUsername());
|
body.setUpdateBy(SecurityUtils.getUsername());
|
||||||
int rows = attendeeService.updateProfile(body);
|
int rows = attendeeService.updateProfile(body);
|
||||||
// 人员变化 → 会议费用待重算
|
// 人员变化 → 立即重算劳务费 (不碰会务费/不置统计中)
|
||||||
if (before != null) {
|
if (before != null) {
|
||||||
bizMeetingService.markFeeCalcPending(before.getMeetingId());
|
bizMeetingService.recomputeLaborFee(before.getMeetingId());
|
||||||
}
|
}
|
||||||
return toAjax(rows);
|
return toAjax(rows);
|
||||||
}
|
}
|
||||||
@@ -129,9 +129,9 @@ public class BizMeetingAttendeeController extends BaseController {
|
|||||||
public AjaxResult remove(@PathVariable("id") Long id) {
|
public AjaxResult remove(@PathVariable("id") Long id) {
|
||||||
BizMeetingAttendee before = attendeeService.selectById(id);
|
BizMeetingAttendee before = attendeeService.selectById(id);
|
||||||
int rows = attendeeService.deleteByPrimaryKey(id);
|
int rows = attendeeService.deleteByPrimaryKey(id);
|
||||||
// 人员变化 → 会议费用待重算
|
// 人员变化 → 立即重算劳务费 (不碰会务费/不置统计中)
|
||||||
if (before != null) {
|
if (before != null) {
|
||||||
bizMeetingService.markFeeCalcPending(before.getMeetingId());
|
bizMeetingService.recomputeLaborFee(before.getMeetingId());
|
||||||
}
|
}
|
||||||
return toAjax(rows);
|
return toAjax(rows);
|
||||||
}
|
}
|
||||||
@@ -194,9 +194,9 @@ public class BizMeetingAttendeeController extends BaseController {
|
|||||||
@RequestParam("meetingId") Long meetingId) throws Exception {
|
@RequestParam("meetingId") Long meetingId) throws Exception {
|
||||||
// 解析 + 入库 (邀请参会已改为手动, 不再自动推"会议邀请", 由前端"邀请参会"按钮触发)
|
// 解析 + 入库 (邀请参会已改为手动, 不再自动推"会议邀请", 由前端"邀请参会"按钮触发)
|
||||||
ImportResult result = attendeeService.importFromExcel(file, meetingId, SecurityUtils.getUsername());
|
ImportResult result = attendeeService.importFromExcel(file, meetingId, SecurityUtils.getUsername());
|
||||||
// 人员变化 → 会议费用待重算 (有成功导入才需重算, 但幂等, 直接标记)
|
// 人员变化 → 立即重算劳务费 (有成功导入才需重算)
|
||||||
if (result != null && result.getOkNum() > 0) {
|
if (result != null && result.getOkNum() > 0) {
|
||||||
bizMeetingService.markFeeCalcPending(meetingId);
|
bizMeetingService.recomputeLaborFee(meetingId);
|
||||||
}
|
}
|
||||||
return success(result);
|
return success(result);
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-1
@@ -99,6 +99,12 @@ public class BizMeetingController extends BaseController {
|
|||||||
} else {
|
} else {
|
||||||
bizMeeting.getParams().put("executorUserId", uid);
|
bizMeeting.getParams().put("executorUserId", uid);
|
||||||
}
|
}
|
||||||
|
// assignedSessions 聚合用 (公司维度): MAIN 用自己, SUB 反查主账号 parent_user_id; 与过滤参数分离, 避免改变 SUB 可见会议范围
|
||||||
|
Long aggUid = uid;
|
||||||
|
if (current != null && "SUB".equals(current.getAccountType()) && current.getParentUserId() != null) {
|
||||||
|
aggUid = current.getParentUserId();
|
||||||
|
}
|
||||||
|
bizMeeting.getParams().put("assignedExecutorUserId", aggUid);
|
||||||
}
|
}
|
||||||
// 合规人员(manager) 数据权限: 只看"本人创建的项目"下的会议 (project_id ∈ create_user_id = 自己的项目)
|
// 合规人员(manager) 数据权限: 只看"本人创建的项目"下的会议 (project_id ∈ create_user_id = 自己的项目)
|
||||||
else if ("manager".equals(roleType)) {
|
else if ("manager".equals(roleType)) {
|
||||||
@@ -111,7 +117,19 @@ public class BizMeetingController extends BaseController {
|
|||||||
|
|
||||||
@GetMapping("/{meetingId}")
|
@GetMapping("/{meetingId}")
|
||||||
public AjaxResult getInfo(@PathVariable("meetingId") Long meetingId) {
|
public AjaxResult getInfo(@PathVariable("meetingId") Long meetingId) {
|
||||||
return success(bizMeetingService.getById(meetingId));
|
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||||
|
// executor 详情: "总期数/期数分母" = 分配给本执行方的场次, 而非项目总场次 (total_periods 对执行方不可见)
|
||||||
|
if (m != null && m.getProjectId() != null
|
||||||
|
&& "executor".equals(SecurityUtils.getLoginUser().getUser().getRoleType())) {
|
||||||
|
Long uid = SecurityUtils.getUserId();
|
||||||
|
Long aggUid = uid;
|
||||||
|
SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
|
||||||
|
if (current != null && "SUB".equals(current.getAccountType()) && current.getParentUserId() != null) {
|
||||||
|
aggUid = current.getParentUserId();
|
||||||
|
}
|
||||||
|
m.setAssignedSessions((long) bizProjectService.countAssignedSessions(m.getProjectId(), aggUid));
|
||||||
|
}
|
||||||
|
return success(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Log(title = "会议", businessType = BusinessType.INSERT)
|
@Log(title = "会议", businessType = BusinessType.INSERT)
|
||||||
@@ -183,6 +201,32 @@ public class BizMeetingController extends BaseController {
|
|||||||
@Log(title = "会议", businessType = BusinessType.UPDATE)
|
@Log(title = "会议", businessType = BusinessType.UPDATE)
|
||||||
@PutMapping
|
@PutMapping
|
||||||
public AjaxResult edit(@RequestBody BizMeeting bizMeeting) {
|
public AjaxResult edit(@RequestBody BizMeeting bizMeeting) {
|
||||||
|
// executor 修改会议同样校验 (与 add 一致, 只是期数冲突检测排除自身): 期数不得超过分配给本公司的场次 + 相同期数不重复.
|
||||||
|
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||||
|
if ("executor".equals(roleType)) {
|
||||||
|
Long uid = SecurityUtils.getUserId();
|
||||||
|
Long projectId = bizMeeting.getProjectId();
|
||||||
|
if (projectId == null) {
|
||||||
|
throw new ServiceException("修改会议必须指定 projectId");
|
||||||
|
}
|
||||||
|
Long aggUid = uid;
|
||||||
|
SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
|
||||||
|
if (current != null && "SUB".equals(current.getAccountType()) && current.getParentUserId() != null) {
|
||||||
|
aggUid = current.getParentUserId();
|
||||||
|
}
|
||||||
|
Long executionUnitId = bizOrgService.selectOrgIdByUserId(aggUid);
|
||||||
|
int assigned = bizProjectService.countAssignedSessions(projectId, aggUid);
|
||||||
|
Long periodNo = bizMeeting.getPeriodNo();
|
||||||
|
if (periodNo != null && periodNo > assigned) {
|
||||||
|
throw new ServiceException("期数不能超过分配给本公司的场次 (共 " + assigned + " 场)");
|
||||||
|
}
|
||||||
|
if (periodNo != null) {
|
||||||
|
int dup = bizMeetingService.countByProjectIdExecutionUnitPeriodExclude(projectId, executionUnitId, periodNo, bizMeeting.getMeetingId());
|
||||||
|
if (dup > 0) {
|
||||||
|
throw new ServiceException("本机构已创建第 " + periodNo + " 期会议, 请勿重复");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
bizMeeting.setUpdateBy(SecurityUtils.getUsername());
|
bizMeeting.setUpdateBy(SecurityUtils.getUsername());
|
||||||
bizMeeting.setUpdateTime(new Date());
|
bizMeeting.setUpdateTime(new Date());
|
||||||
// 修改会议时项目形式也从项目继承 (与 add 一致, 避免 project_form 残留为空)
|
// 修改会议时项目形式也从项目继承 (与 add 一致, 避免 project_form 残留为空)
|
||||||
@@ -534,6 +578,9 @@ public class BizMeetingController extends BaseController {
|
|||||||
|
|
||||||
m.setIsSettled(1);
|
m.setIsSettled(1);
|
||||||
m.setSettleTime(new Date());
|
m.setSettleTime(new Date());
|
||||||
|
// 结算即终态: 直接落完结标记, 省去二次「完结」动作 (auto-finish)
|
||||||
|
m.setIsFinished(1);
|
||||||
|
m.setFinishTime(new Date());
|
||||||
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
||||||
bizMeetingService.updateByPrimaryKey(m);
|
bizMeetingService.updateByPrimaryKey(m);
|
||||||
appendAuditLog(m, "SETTLE", "APPROVED", "会议结算", null);
|
appendAuditLog(m, "SETTLE", "APPROVED", "会议结算", null);
|
||||||
|
|||||||
+50
-16
@@ -1,5 +1,6 @@
|
|||||||
package com.ruoyi.business.controller;
|
package com.ruoyi.business.controller;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -10,7 +11,9 @@ import com.ruoyi.common.core.domain.AjaxResult;
|
|||||||
import com.ruoyi.common.enums.BusinessType;
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
import com.ruoyi.common.utils.file.FileUtils;
|
||||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||||
|
import com.ruoyi.business.oss.OssZipService;
|
||||||
import com.ruoyi.business.service.IBizMeetingMaterialService;
|
import com.ruoyi.business.service.IBizMeetingMaterialService;
|
||||||
import com.ruoyi.business.service.IBizMeetingService;
|
import com.ruoyi.business.service.IBizMeetingService;
|
||||||
|
|
||||||
@@ -29,6 +32,8 @@ public class BizMeetingMaterialController extends BaseController {
|
|||||||
private IBizMeetingMaterialService bizMeetingMaterialService;
|
private IBizMeetingMaterialService bizMeetingMaterialService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IBizMeetingService bizMeetingService;
|
private IBizMeetingService bizMeetingService;
|
||||||
|
@Autowired
|
||||||
|
private OssZipService ossZipService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查该会议的所有材料记录
|
* 查该会议的所有材料记录
|
||||||
@@ -56,9 +61,14 @@ public class BizMeetingMaterialController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 先判断材料是否实际变化 (必须在 replaceByMeetingId 之前, 后者会先删旧记录).
|
||||||
|
// 会务材料没变时不置"统计中", 避免无谓重算 + 前端"统计中"闪烁.
|
||||||
|
boolean changed = bizMeetingMaterialService.isMaterialSetChanged(meetingId, list);
|
||||||
List<BizMeetingMaterial> saved = bizMeetingMaterialService.replaceByMeetingId(meetingId, list);
|
List<BizMeetingMaterial> saved = bizMeetingMaterialService.replaceByMeetingId(meetingId, list);
|
||||||
// 材料变化 → 会议费用待重算 (FeeCalcScheduler 汇总回写)
|
if (changed) {
|
||||||
bizMeetingService.markFeeCalcPending(meetingId);
|
// 材料变化 → 立即重算会议费用 (发票未 OCR 完则回滚 0 走调度器兜底)
|
||||||
|
bizMeetingService.recomputeMeetingFee(meetingId);
|
||||||
|
}
|
||||||
return success(saved);
|
return success(saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,58 +102,79 @@ public class BizMeetingMaterialController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 会务下载: 把该会议所有"会务"材料 (SERVICE + SERVICE_VOUCHER) 在 OSS 端打 zip, 返回下载 URL.
|
* 会务下载: 把该会议所有"会务"材料 (SERVICE + SERVICE_VOUCHER) 在 OSS 端打 zip, 后端代理改名下发.
|
||||||
* 仅 admin/manager 可触发 (前端 manager/meetings 列表操作栏按钮).
|
* 仅 admin/manager 可触发 (前端 manager/meetings 列表操作栏按钮).
|
||||||
|
* 文件名: 项目编号_项目名称_第N期_会务.zip
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{meetingId}/downloadZip")
|
@GetMapping("/{meetingId}/downloadZip")
|
||||||
public AjaxResult downloadZip(@PathVariable("meetingId") Long meetingId) {
|
public void downloadZip(@PathVariable("meetingId") Long meetingId, HttpServletResponse response) throws Exception {
|
||||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||||
throw new ServiceException("只有管理员或合规经理可下载会务材料");
|
throw new ServiceException("只有管理员或合规经理可下载会务材料");
|
||||||
}
|
}
|
||||||
// 注意: 不能用 success(String), 否则 URL 会被塞进 msg 字段 (BaseController.success(String) 重载陷阱)
|
String url = bizMeetingMaterialService.buildServiceZipUrl(meetingId);
|
||||||
return AjaxResult.success("操作成功", bizMeetingMaterialService.buildServiceZipUrl(meetingId));
|
streamZip(url, bizMeetingMaterialService.serviceZipFilename(meetingId), response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 劳务下载: 把该会议所有"劳务"材料 (LABOR + LABOR_VOUCHER) + 参会人信息在 OSS 端打 zip, 返回下载 URL.
|
* 劳务下载: 把该会议所有"劳务"材料 (LABOR + LABOR_VOUCHER) + 参会人信息在 OSS 端打 zip, 后端代理改名下发.
|
||||||
* 仅 admin/manager 可触发 (前端 manager/meetings 列表操作栏按钮).
|
* 仅 admin/manager 可触发 (前端 manager/meetings 列表操作栏按钮).
|
||||||
|
* 文件名: 项目编号_项目名称_第N期_劳务.zip
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{meetingId}/downloadLaborZip")
|
@GetMapping("/{meetingId}/downloadLaborZip")
|
||||||
public AjaxResult downloadLaborZip(@PathVariable("meetingId") Long meetingId) {
|
public void downloadLaborZip(@PathVariable("meetingId") Long meetingId, HttpServletResponse response) throws Exception {
|
||||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||||
throw new ServiceException("只有管理员或合规经理可下载劳务材料");
|
throw new ServiceException("只有管理员或合规经理可下载劳务材料");
|
||||||
}
|
}
|
||||||
return AjaxResult.success("操作成功", bizMeetingMaterialService.buildLaborZipUrl(meetingId));
|
String url = bizMeetingMaterialService.buildLaborZipUrl(meetingId);
|
||||||
|
streamZip(url, bizMeetingMaterialService.laborZipFilename(meetingId), response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量会务下载: 多会议会务材料合并打一个 zip, 返回下载 URL.
|
* 批量会务下载: 多会议会务材料合并打一个 zip, 后端代理改名下发.
|
||||||
* body: { "meetingIds": [1,2,3] }, 仅 admin/manager.
|
* body: { "meetingIds": [1,2,3] }, 仅 admin/manager.
|
||||||
|
* 文件名: 会务_下载时间.zip
|
||||||
*/
|
*/
|
||||||
@PostMapping("/batchDownloadZip")
|
@PostMapping("/batchDownloadZip")
|
||||||
public AjaxResult batchDownloadZip(@RequestBody(required = false) BatchDownloadBody body) {
|
public void batchDownloadZip(@RequestBody(required = false) BatchDownloadBody body, HttpServletResponse response) throws Exception {
|
||||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||||
throw new ServiceException("只有管理员或合规经理可下载会务材料");
|
throw new ServiceException("只有管理员或合规经理可下载会务材料");
|
||||||
}
|
}
|
||||||
List<Long> meetingIds = body == null ? null : body.getMeetingIds();
|
List<Long> meetingIds = body == null ? null : body.getMeetingIds();
|
||||||
return AjaxResult.success("操作成功", bizMeetingMaterialService.buildBatchServiceZipUrl(meetingIds));
|
String url = bizMeetingMaterialService.buildBatchServiceZipUrl(meetingIds);
|
||||||
|
streamZip(url, bizMeetingMaterialService.batchServiceZipFilename(), response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量劳务下载: 多会议劳务材料(含参会人信息)合并打一个 zip, 返回下载 URL.
|
* 批量劳务下载: 多会议劳务材料(含参会人信息)合并打一个 zip, 后端代理改名下发.
|
||||||
* body: { "meetingIds": [1,2,3] }, 仅 admin/manager.
|
* body: { "meetingIds": [1,2,3] }, 仅 admin/manager.
|
||||||
|
* 文件名: 劳务_下载时间.zip
|
||||||
*/
|
*/
|
||||||
@PostMapping("/batchDownloadLaborZip")
|
@PostMapping("/batchDownloadLaborZip")
|
||||||
public AjaxResult batchDownloadLaborZip(@RequestBody(required = false) BatchDownloadBody body) {
|
public void batchDownloadLaborZip(@RequestBody(required = false) BatchDownloadBody body, HttpServletResponse response) throws Exception {
|
||||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||||
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
if (!"admin".equals(roleType) && !"manager".equals(roleType)) {
|
||||||
throw new ServiceException("只有管理员或合规经理可下载劳务材料");
|
throw new ServiceException("只有管理员或合规经理可下载劳务材料");
|
||||||
}
|
}
|
||||||
List<Long> meetingIds = body == null ? null : body.getMeetingIds();
|
List<Long> meetingIds = body == null ? null : body.getMeetingIds();
|
||||||
return AjaxResult.success("操作成功", bizMeetingMaterialService.buildBatchLaborZipUrl(meetingIds));
|
String url = bizMeetingMaterialService.buildBatchLaborZipUrl(meetingIds);
|
||||||
|
streamZip(url, bizMeetingMaterialService.batchLaborZipFilename(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 FC 打包返回的签名 URL 拉取 zip 字节流, 以自定义文件名写回 response (Content-Disposition 走 UTF-8 编码) */
|
||||||
|
private void streamZip(String url, String filename, HttpServletResponse response) throws Exception {
|
||||||
|
response.setContentType("application/zip");
|
||||||
|
FileUtils.setAttachmentResponseHeader(response, filename);
|
||||||
|
try (InputStream in = ossZipService.openZipStream(url)) {
|
||||||
|
byte[] buf = new byte[8192];
|
||||||
|
int n;
|
||||||
|
while ((n = in.read(buf)) > 0) {
|
||||||
|
response.getOutputStream().write(buf, 0, n);
|
||||||
|
}
|
||||||
|
response.getOutputStream().flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,7 +198,10 @@ public class BizMeetingMaterialController extends BaseController {
|
|||||||
public AjaxResult uploadServiceMaterials(@RequestParam("file") MultipartFile file,
|
public AjaxResult uploadServiceMaterials(@RequestParam("file") MultipartFile file,
|
||||||
@RequestParam("meetingId") Long meetingId) throws Exception {
|
@RequestParam("meetingId") Long meetingId) throws Exception {
|
||||||
int updated = bizMeetingMaterialService.uploadServiceMaterials(file, meetingId);
|
int updated = bizMeetingMaterialService.uploadServiceMaterials(file, meetingId);
|
||||||
bizMeetingService.markFeeCalcPending(meetingId);
|
// 仅当真有材料被替换 (内容变化, updated>0) 才立即重算会务费; 全量未变不置"统计中"
|
||||||
|
if (updated > 0) {
|
||||||
|
bizMeetingService.recomputeMeetingFee(meetingId);
|
||||||
|
}
|
||||||
return success(updated);
|
return success(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
package com.ruoyi.business.controller;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import com.ruoyi.business.domain.BizMeeting;
|
||||||
|
import com.ruoyi.business.service.IBizMeetingService;
|
||||||
|
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.utils.SecurityUtils;
|
||||||
|
import com.ruoyi.system.mapper.SysUserMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会议阶段统计端点 — 独立 Controller (仅 sponsor/Home KPI 用).
|
||||||
|
*
|
||||||
|
* <p>独立成 Controller 的原因: BizMeetingController 有 `GET /{meetingId}` 详情端点,
|
||||||
|
* Spring 6.x 会把同 Controller 内的字面量 `/stageStats` 路由到 `/{meetingId}` 上
|
||||||
|
* (报 "参数类型不匹配"). 拆到独立 Controller 后两条路径不在同一个映射表竞争, 零冲突.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/business/meeting")
|
||||||
|
public class BizMeetingStageStatsController extends BaseController
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private IBizMeetingService bizMeetingService;
|
||||||
|
@Autowired
|
||||||
|
private SysUserMapper sysUserMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 current_stage 分组计数, 只做 sponsor 数据权限隔离 (MAIN=本支持单位全部, SUB=自己负责的项目).
|
||||||
|
* 返回 { stage: cnt, ... }, 前端据此算: 未执行=NOT_STARTED, 已执行=排除 NOT_STARTED/IN_PROGRESS 的其余 stage (含 FROZEN).
|
||||||
|
* 其他角色的统计端点单独实现, 此端点非 sponsor 调用返回空 map (不泄露全量数据).
|
||||||
|
*/
|
||||||
|
@GetMapping("/stageStats")
|
||||||
|
public AjaxResult stageStats() {
|
||||||
|
Map<String, Long> result = new HashMap<>();
|
||||||
|
if (!"sponsor".equals(SecurityUtils.getLoginUser().getUser().getRoleType())) {
|
||||||
|
return success(result);
|
||||||
|
}
|
||||||
|
BizMeeting q = new BizMeeting();
|
||||||
|
Long uid = SecurityUtils.getUserId();
|
||||||
|
SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
|
||||||
|
if (current != null && "SUB".equals(current.getAccountType())) {
|
||||||
|
q.getParams().put("monitorUserId", uid);
|
||||||
|
} else {
|
||||||
|
q.getParams().put("sponsorAdminUserId", uid);
|
||||||
|
}
|
||||||
|
for (Map<String, Object> r : bizMeetingService.selectStageStats(q)) {
|
||||||
|
// resultType=map 的 key 大小写随 JDBC 驱动, 做大小写不敏感匹配, 避免"统计恒 0"
|
||||||
|
String stage = null;
|
||||||
|
long cnt = 0L;
|
||||||
|
for (Map.Entry<String, Object> e : r.entrySet()) {
|
||||||
|
String k = e.getKey();
|
||||||
|
if (k == null) continue;
|
||||||
|
if (k.equalsIgnoreCase("currentStage") || k.equalsIgnoreCase("stage")) {
|
||||||
|
stage = e.getValue() == null ? null : String.valueOf(e.getValue());
|
||||||
|
} else if (k.equalsIgnoreCase("cnt") || k.equalsIgnoreCase("count")) {
|
||||||
|
cnt = e.getValue() == null ? 0L : ((Number) e.getValue()).longValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stage != null) {
|
||||||
|
result.put(stage, cnt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-6
@@ -40,20 +40,34 @@ public class BizMessageController extends BaseController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前登录用户的最近消息 (含未读统计)
|
* 当前登录用户的最近消息 (含未读统计)
|
||||||
* GET /business/message/my?limit=50
|
* 两种模式:
|
||||||
* 返回 {rows: [...], unread: 12}
|
* - 分页: GET /business/message/my?pageNum=1&pageSize=20 (返回真实 total)
|
||||||
* unread 用 countUnread 跟 limit 解耦, SSE 推的也是真值
|
* - 快捷: GET /business/message/my?limit=50 (嵌入式列表, total=rows.size)
|
||||||
|
* 返回 {rows: [...], unread: 12, total: n}
|
||||||
|
* unread 用 countUnread 跟分页解耦, SSE 推的也是真值
|
||||||
*/
|
*/
|
||||||
@GetMapping("/my")
|
@GetMapping("/my")
|
||||||
public AjaxResult my(@RequestParam(value = "limit", required = false, defaultValue = "50") Integer limit)
|
public AjaxResult my(@RequestParam(value = "pageNum", required = false) Integer pageNum,
|
||||||
|
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit)
|
||||||
{
|
{
|
||||||
Long uid = SecurityUtils.getUserId();
|
Long uid = SecurityUtils.getUserId();
|
||||||
List<BizMessage> rows = bizMessageService.selectMyRecent(uid, limit);
|
List<BizMessage> rows;
|
||||||
|
int total;
|
||||||
|
if (pageNum != null && pageSize != null && pageNum > 0 && pageSize > 0) {
|
||||||
|
int offset = (pageNum - 1) * pageSize;
|
||||||
|
rows = bizMessageService.selectMyPage(uid, offset, pageSize);
|
||||||
|
total = bizMessageService.countMy(uid);
|
||||||
|
} else {
|
||||||
|
int lim = (limit == null || limit <= 0) ? 50 : limit;
|
||||||
|
rows = bizMessageService.selectMyRecent(uid, lim);
|
||||||
|
total = rows.size();
|
||||||
|
}
|
||||||
int unread = bizMessageService.countUnread(uid);
|
int unread = bizMessageService.countUnread(uid);
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
data.put("rows", rows);
|
data.put("rows", rows);
|
||||||
data.put("unread", unread);
|
data.put("unread", unread);
|
||||||
data.put("total", rows.size());
|
data.put("total", total);
|
||||||
return success(data);
|
return success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-4
@@ -15,6 +15,7 @@ import com.ruoyi.common.utils.poi.ExcelUtil;
|
|||||||
import com.ruoyi.business.domain.BizPerson;
|
import com.ruoyi.business.domain.BizPerson;
|
||||||
import com.ruoyi.business.domain.BizPersonExecutorImportVO;
|
import com.ruoyi.business.domain.BizPersonExecutorImportVO;
|
||||||
import com.ruoyi.business.domain.BizPersonImportVO;
|
import com.ruoyi.business.domain.BizPersonImportVO;
|
||||||
|
import com.ruoyi.business.mapper.BizOrgMapper;
|
||||||
import com.ruoyi.business.service.IBizPersonService;
|
import com.ruoyi.business.service.IBizPersonService;
|
||||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
@@ -33,6 +34,9 @@ public class BizPersonController extends BaseController
|
|||||||
@Autowired
|
@Autowired
|
||||||
private SysUserMapper sysUserMapper;
|
private SysUserMapper sysUserMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BizOrgMapper bizOrgMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 业务角色白名单: 这些 roleType 的 MAIN 账号调 /list 会自动按"本机构子账号"隔离
|
* 业务角色白名单: 这些 roleType 的 MAIN 账号调 /list 会自动按"本机构子账号"隔离
|
||||||
* (sys_user.parent_user_id → sys_user.user_id → biz_person.user_id 链路过滤)
|
* (sys_user.parent_user_id → sys_user.user_id → biz_person.user_id 链路过滤)
|
||||||
@@ -69,8 +73,9 @@ public class BizPersonController extends BaseController
|
|||||||
@GetMapping("/sponsorList")
|
@GetMapping("/sponsorList")
|
||||||
public TableDataInfo sponsorList(BizPerson bizPerson)
|
public TableDataInfo sponsorList(BizPerson bizPerson)
|
||||||
{
|
{
|
||||||
Long mainUid = getUserId();
|
// 严格按 org_id 圈本机构所有用户 (MAIN + SUB), 而非 parent_user_id 归属
|
||||||
bizPerson.getParams().put("sponsorOwnerUid", mainUid);
|
Long orgId = bizOrgMapper.selectOrgIdByUserId(getUserId());
|
||||||
|
bizPerson.getParams().put("sponsorOrgId", orgId);
|
||||||
startPage();
|
startPage();
|
||||||
List<BizPerson> list = bizPersonService.selectSponsorList(bizPerson);
|
List<BizPerson> list = bizPersonService.selectSponsorList(bizPerson);
|
||||||
return getDataTable(list);
|
return getDataTable(list);
|
||||||
@@ -82,8 +87,9 @@ public class BizPersonController extends BaseController
|
|||||||
@GetMapping("/executorList")
|
@GetMapping("/executorList")
|
||||||
public TableDataInfo executorList(BizPerson bizPerson)
|
public TableDataInfo executorList(BizPerson bizPerson)
|
||||||
{
|
{
|
||||||
Long mainUid = getUserId();
|
// 严格按 org_id 圈本机构所有用户 (MAIN + SUB), 而非 parent_user_id 归属
|
||||||
bizPerson.getParams().put("executorOwnerUid", mainUid);
|
Long orgId = bizOrgMapper.selectOrgIdByUserId(getUserId());
|
||||||
|
bizPerson.getParams().put("executorOrgId", orgId);
|
||||||
startPage();
|
startPage();
|
||||||
List<BizPerson> list = bizPersonService.selectExecutorList(bizPerson);
|
List<BizPerson> list = bizPersonService.selectExecutorList(bizPerson);
|
||||||
return getDataTable(list);
|
return getDataTable(list);
|
||||||
@@ -114,6 +120,17 @@ public class BizPersonController extends BaseController
|
|||||||
bizPerson.setUpdateBy(getUsername());
|
bizPerson.setUpdateBy(getUsername());
|
||||||
return toAjax(bizPersonService.updateByPrimaryKey(bizPerson));
|
return toAjax(bizPersonService.updateByPrimaryKey(bizPerson));
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 个人资料编辑 (sponsor/executor 账号管理页): 只改当前登录人自己的姓名/手机号
|
||||||
|
* 按 user_id 反查 personId, 同步写 biz_person.name + sys_user.nick_name (单一可信源)
|
||||||
|
*/
|
||||||
|
@Log(title = "个人资料", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/profile")
|
||||||
|
public AjaxResult updateProfile(@RequestBody BizPerson bizPerson)
|
||||||
|
{
|
||||||
|
bizPerson.setUserId(getUserId());
|
||||||
|
return toAjax(bizPersonService.updateProfileByUserId(bizPerson));
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 更换机构管理员 (admin/sponsor-people 管理员 switch)
|
* 更换机构管理员 (admin/sponsor-people 管理员 switch)
|
||||||
* body: { personId } — 把该人员晋升为机构 MAIN, 原管理员降为 SUB
|
* body: { personId } — 把该人员晋升为机构 MAIN, 原管理员降为 SUB
|
||||||
|
|||||||
+54
-15
@@ -252,7 +252,15 @@ public class BizProjectController extends BaseController
|
|||||||
@GetMapping("/{projectId}/assigns")
|
@GetMapping("/{projectId}/assigns")
|
||||||
public AjaxResult getAssigns(@PathVariable("projectId") Long projectId)
|
public AjaxResult getAssigns(@PathVariable("projectId") Long projectId)
|
||||||
{
|
{
|
||||||
return success(bizProjectAssignService.selectByProjectId(projectId));
|
List<BizProjectAssign> list = bizProjectAssignService.selectByProjectId(projectId);
|
||||||
|
// 执行方只看本单位 (严格隔离, 不依赖前端): 过滤掉其它执行单位
|
||||||
|
if ("executor".equals(SecurityUtils.getLoginUser().getUser().getRoleType())) {
|
||||||
|
Long myOrgId = bizOrgService.selectOrgIdByUserId(SecurityUtils.getUserId());
|
||||||
|
if (myOrgId != null) {
|
||||||
|
list.removeIf(a -> a.getExecutionUnitId() == null || !a.getExecutionUnitId().equals(myOrgId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Log(title = "项目执行方分配", businessType = BusinessType.INSERT)
|
@Log(title = "项目执行方分配", businessType = BusinessType.INSERT)
|
||||||
@@ -350,30 +358,57 @@ public class BizProjectController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用评分 upsert (任何角色: sponsor / executor / compliance)
|
* 评分 upsert
|
||||||
* POST /business/project/rate
|
* POST /business/project/rate
|
||||||
* 不做可见性校验 — 评分公开
|
* 评分公开可读; 写入仅限 admin/manager → 'manager', sponsor → 'sponsor'
|
||||||
* 评分写入 biz_project_rating 后, 同步回写 biz_project.manager_score
|
* raterRole 由后端按登录人 role_type 派生, 不信任前端 (杜绝医生/执行方伪造评分)
|
||||||
* = 该项目所有 compliance 角色评分的 4 维度总分之平均 (decimal(3,1) 1 位小数)
|
* 评分写入 biz_project_rating 后, 同步回写聚合分:
|
||||||
|
* manager → biz_project.manager_score
|
||||||
|
* sponsor → biz_project.sponsor_score
|
||||||
|
* 聚合口径 = 该角色所有评分记录 4 个维度值的平均 (decimal(3,1) 1 位小数), 多人评分取平均而非最后一次覆盖
|
||||||
*/
|
*/
|
||||||
@Log(title = "项目评分", businessType = BusinessType.UPDATE)
|
@Log(title = "项目评分", businessType = BusinessType.UPDATE)
|
||||||
@PostMapping("/rate")
|
@PostMapping("/rate")
|
||||||
public AjaxResult rate(@RequestBody BizProjectRating body)
|
public AjaxResult rate(@RequestBody BizProjectRating body)
|
||||||
{
|
{
|
||||||
if (body.getProjectId() == null || body.getRaterRole() == null) {
|
if (body.getProjectId() == null) {
|
||||||
return error("projectId / raterRole 必填");
|
return error("projectId 必填");
|
||||||
}
|
}
|
||||||
// 评分人以当前登录用户为准, 不信任前端传入的 raterId
|
// 评分人以当前登录用户为准, 不信任前端传入的 raterId
|
||||||
body.setRaterId(SecurityUtils.getUserId());
|
body.setRaterId(SecurityUtils.getUserId());
|
||||||
body.setCreateBy(SecurityUtils.getUsername());
|
body.setCreateBy(SecurityUtils.getUsername());
|
||||||
body.setUpdateBy(SecurityUtils.getUsername());
|
body.setUpdateBy(SecurityUtils.getUsername());
|
||||||
|
|
||||||
|
// raterRole 由登录人真实角色派生 (role_type 单一可信源), 杜绝医生/执行方伪造 manager/sponsor 评分
|
||||||
|
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||||
|
String raterRole;
|
||||||
|
if ("admin".equals(roleType) || "manager".equals(roleType)) {
|
||||||
|
raterRole = "manager";
|
||||||
|
}
|
||||||
|
else if ("sponsor".equals(roleType)) {
|
||||||
|
raterRole = "sponsor";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return error("当前角色不可评分");
|
||||||
|
}
|
||||||
|
body.setRaterRole(raterRole);
|
||||||
|
|
||||||
int rows = bizProjectRatingService.upsertRating(body);
|
int rows = bizProjectRatingService.upsertRating(body);
|
||||||
|
|
||||||
// 同步回写 biz_project.manager_score (仅 compliance 角色聚合)
|
// 写明细后重算聚合分 (聚合分只由后端从明细算, 前端不再手写单值)
|
||||||
if ("compliance".equals(body.getRaterRole())) {
|
recomputeScore(body.getProjectId(), body.getRaterRole());
|
||||||
|
return toAjax(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 biz_project_rating 实时重算某项目某角色的聚合分 (4 维度平均, 覆盖该角色所有评分人)
|
||||||
|
* 聚合 = 该角色所有评分记录 4 维度值之和 / (记录数 × 4)
|
||||||
|
*/
|
||||||
|
private void recomputeScore(Long projectId, String raterRole)
|
||||||
|
{
|
||||||
BizProjectRating q = new BizProjectRating();
|
BizProjectRating q = new BizProjectRating();
|
||||||
q.setProjectId(body.getProjectId());
|
q.setProjectId(projectId);
|
||||||
q.setRaterRole("compliance");
|
q.setRaterRole(raterRole);
|
||||||
List<BizProjectRating> all = bizProjectRatingService.selectList(q);
|
List<BizProjectRating> all = bizProjectRatingService.selectList(q);
|
||||||
BigDecimal sum = BigDecimal.ZERO;
|
BigDecimal sum = BigDecimal.ZERO;
|
||||||
int cnt = 0;
|
int cnt = 0;
|
||||||
@@ -382,12 +417,16 @@ public class BizProjectController extends BaseController
|
|||||||
+ safeLong(r.getCooperationScore()) + safeLong(r.getComplianceScore());
|
+ safeLong(r.getCooperationScore()) + safeLong(r.getComplianceScore());
|
||||||
if (s > 0) { sum = sum.add(BigDecimal.valueOf(s)); cnt++; }
|
if (s > 0) { sum = sum.add(BigDecimal.valueOf(s)); cnt++; }
|
||||||
}
|
}
|
||||||
|
if (cnt == 0) return; // 无有效评分, 不动聚合分
|
||||||
|
BigDecimal avg = sum.divide(BigDecimal.valueOf(cnt * 4L), 1, RoundingMode.HALF_UP);
|
||||||
BizProject p = new BizProject();
|
BizProject p = new BizProject();
|
||||||
p.setProjectId(body.getProjectId());
|
p.setProjectId(projectId);
|
||||||
p.setManagerScore(cnt == 0 ? null : sum.divide(BigDecimal.valueOf(cnt), 1, RoundingMode.HALF_UP));
|
if ("sponsor".equals(raterRole)) {
|
||||||
bizProjectService.updateByPrimaryKey(p); // mapper `<if test="managerScore != null">` 仅更新这一列
|
p.setSponsorScore(avg);
|
||||||
|
} else {
|
||||||
|
p.setManagerScore(avg);
|
||||||
}
|
}
|
||||||
return toAjax(rows);
|
bizProjectService.updateByPrimaryKey(p); // mapper `<if test="...Score != null">` 仅更新对应列
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long safeLong(Long v) { return v == null ? 0 : v; }
|
private static long safeLong(Long v) { return v == null ? 0 : v; }
|
||||||
|
|||||||
+4
-1
@@ -17,7 +17,7 @@ import com.ruoyi.business.service.IBizProjectPlanService;
|
|||||||
*
|
*
|
||||||
* 角色权限:
|
* 角色权限:
|
||||||
* - 投稿角色 (doctor/executor/sponsor): 只看自己投的稿 (submitter_id = 当前用户); 新建/编辑强制 submitter_id 写自己, status 默认 '0'
|
* - 投稿角色 (doctor/executor/sponsor): 只看自己投的稿 (submitter_id = 当前用户); 新建/编辑强制 submitter_id 写自己, status 默认 '0'
|
||||||
* - 管理角色 (admin/manager): 全部可见, 不强制 submitter (用于审核/结算)
|
* - 管理角色 (admin/manager): 排除未提交草稿 (status='0'), 不强制 submitter (用于审核/结算)
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/business/projectPlan")
|
@RequestMapping("/business/projectPlan")
|
||||||
@@ -32,6 +32,9 @@ public class BizProjectPlanController extends BaseController
|
|||||||
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||||
if (isSubmitterRole(roleType)) {
|
if (isSubmitterRole(roleType)) {
|
||||||
BizProjectPlan.setSubmitterId(SecurityUtils.getUserId());
|
BizProjectPlan.setSubmitterId(SecurityUtils.getUserId());
|
||||||
|
} else {
|
||||||
|
// 管理角色 (admin/manager): 未提交的草稿 (status='0') 不进列表, 只审已提交的稿
|
||||||
|
BizProjectPlan.getParams().put("excludeDraft", true);
|
||||||
}
|
}
|
||||||
startPage();
|
startPage();
|
||||||
List<BizProjectPlan> list = BizProjectPlanService.selectList(BizProjectPlan);
|
List<BizProjectPlan> list = BizProjectPlanService.selectList(BizProjectPlan);
|
||||||
|
|||||||
-2
@@ -232,7 +232,6 @@ public class BizPublicityIntentController extends BaseController {
|
|||||||
v.setPosition(e.getPosition());
|
v.setPosition(e.getPosition());
|
||||||
v.setPhone(e.getPhone());
|
v.setPhone(e.getPhone());
|
||||||
v.setUserStatus(e.getUserId() != null ? "存在" : "不存在");
|
v.setUserStatus(e.getUserId() != null ? "存在" : "不存在");
|
||||||
v.setIntentStatus(e.getIntentStatus() == null || e.getIntentStatus().isEmpty() ? "待审核" : e.getIntentStatus());
|
|
||||||
v.setCreateTime(e.getCreateTime());
|
v.setCreateTime(e.getCreateTime());
|
||||||
exportList.add(v);
|
exportList.add(v);
|
||||||
}
|
}
|
||||||
@@ -259,7 +258,6 @@ public class BizPublicityIntentController extends BaseController {
|
|||||||
v.setPosition(e.getPosition());
|
v.setPosition(e.getPosition());
|
||||||
v.setPhone(e.getPhone());
|
v.setPhone(e.getPhone());
|
||||||
v.setUserStatus(e.getUserId() != null ? "存在" : "不存在");
|
v.setUserStatus(e.getUserId() != null ? "存在" : "不存在");
|
||||||
v.setIntentStatus(e.getIntentStatus() == null || e.getIntentStatus().isEmpty() ? "待审核" : e.getIntentStatus());
|
|
||||||
v.setCreateTime(e.getCreateTime());
|
v.setCreateTime(e.getCreateTime());
|
||||||
exportList.add(v);
|
exportList.add(v);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ public class BizExpert extends BaseEntity {
|
|||||||
private String bankCard;
|
private String bankCard;
|
||||||
/** 银行名称 */
|
/** 银行名称 */
|
||||||
private String bankName;
|
private String bankName;
|
||||||
|
/** 开户行 (支行) */
|
||||||
|
private String bankBranch;
|
||||||
/** 开户行省/市 (1-3 段, / 分隔, 直辖市省=市) */
|
/** 开户行省/市 (1-3 段, / 分隔, 直辖市省=市) */
|
||||||
private String bankRegion;
|
private String bankRegion;
|
||||||
/** 开户行地址 */
|
/** 开户行地址 */
|
||||||
@@ -120,6 +122,8 @@ public class BizExpert extends BaseEntity {
|
|||||||
public void setBankCard(String bankCard) { this.bankCard = bankCard; }
|
public void setBankCard(String bankCard) { this.bankCard = bankCard; }
|
||||||
public String getBankName() { return bankName; }
|
public String getBankName() { return bankName; }
|
||||||
public void setBankName(String bankName) { this.bankName = bankName; }
|
public void setBankName(String bankName) { this.bankName = bankName; }
|
||||||
|
public String getBankBranch() { return bankBranch; }
|
||||||
|
public void setBankBranch(String bankBranch) { this.bankBranch = bankBranch; }
|
||||||
public String getBankRegion() { return bankRegion; }
|
public String getBankRegion() { return bankRegion; }
|
||||||
public void setBankRegion(String bankRegion) { this.bankRegion = bankRegion; }
|
public void setBankRegion(String bankRegion) { this.bankRegion = bankRegion; }
|
||||||
public String getBankAddress() { return bankAddress; }
|
public String getBankAddress() { return bankAddress; }
|
||||||
|
|||||||
@@ -118,10 +118,14 @@ public class BizMeeting extends BaseEntity {
|
|||||||
private String laborSigned;
|
private String laborSigned;
|
||||||
/** 参会人 user_id (非持久化字段, 仅用于 mapper WHERE 过滤; controller 注入, 配合 biz_meeting_attendee 中间表) */
|
/** 参会人 user_id (非持久化字段, 仅用于 mapper WHERE 过滤; controller 注入, 配合 biz_meeting_attendee 中间表) */
|
||||||
private transient Long userId;
|
private transient Long userId;
|
||||||
|
/** 会议列表筛选: current_stage NOT IN (非持久化, 逗号分隔; 前端 sponsor Home "已执行会议"跳转传 'NOT_STARTED,IN_PROGRESS') */
|
||||||
|
private transient String currentStageNotIn;
|
||||||
/** 当前登录医生/专家在本会议的参会人记录 id (非持久化, mapper 子查询填充; 用于 /doctor/meetings 签署劳务链接) */
|
/** 当前登录医生/专家在本会议的参会人记录 id (非持久化, mapper 子查询填充; 用于 /doctor/meetings 签署劳务链接) */
|
||||||
private transient Long attendeeId;
|
private transient Long attendeeId;
|
||||||
/** 当前登录医生/专家在本会议的已签劳务 PDF URL (非持久化, mapper 子查询填充; null=未签) */
|
/** 当前登录医生/专家在本会议的已签劳务 PDF URL (非持久化, mapper 子查询填充; null=未签) */
|
||||||
private transient String attendeeLaborProtocol;
|
private transient String attendeeLaborProtocol;
|
||||||
|
/** 分配给本执行方的场次 (非持久化; executor 会议详情用, controller 按登录执行方反查 biz_project_assign.sessions 之和, 作为"期数"分母/总期数) */
|
||||||
|
private transient Long assignedSessions;
|
||||||
/** 新增/编辑会议时传入, 自动批量写入 biz_meeting_attendee 中间表 (非持久化) */
|
/** 新增/编辑会议时传入, 自动批量写入 biz_meeting_attendee 中间表 (非持久化) */
|
||||||
private Long[] attendeeUserIds;
|
private Long[] attendeeUserIds;
|
||||||
/** 劳务费用 = 参会人应发金额 (fee_pre_tax) 合计 (后台定时任务汇总回写) */
|
/** 劳务费用 = 参会人应发金额 (fee_pre_tax) 合计 (后台定时任务汇总回写) */
|
||||||
@@ -223,10 +227,14 @@ public class BizMeeting extends BaseEntity {
|
|||||||
public void setSubmitDeadline(Date submitDeadline) { this.submitDeadline = submitDeadline; }
|
public void setSubmitDeadline(Date submitDeadline) { this.submitDeadline = submitDeadline; }
|
||||||
public Long getUserId() { return userId; }
|
public Long getUserId() { return userId; }
|
||||||
public void setUserId(Long userId) { this.userId = userId; }
|
public void setUserId(Long userId) { this.userId = userId; }
|
||||||
|
public String getCurrentStageNotIn() { return currentStageNotIn; }
|
||||||
|
public void setCurrentStageNotIn(String currentStageNotIn) { this.currentStageNotIn = currentStageNotIn; }
|
||||||
public Long getAttendeeId() { return attendeeId; }
|
public Long getAttendeeId() { return attendeeId; }
|
||||||
public void setAttendeeId(Long attendeeId) { this.attendeeId = attendeeId; }
|
public void setAttendeeId(Long attendeeId) { this.attendeeId = attendeeId; }
|
||||||
public String getAttendeeLaborProtocol() { return attendeeLaborProtocol; }
|
public String getAttendeeLaborProtocol() { return attendeeLaborProtocol; }
|
||||||
public void setAttendeeLaborProtocol(String attendeeLaborProtocol) { this.attendeeLaborProtocol = attendeeLaborProtocol; }
|
public void setAttendeeLaborProtocol(String attendeeLaborProtocol) { this.attendeeLaborProtocol = attendeeLaborProtocol; }
|
||||||
|
public Long getAssignedSessions() { return assignedSessions; }
|
||||||
|
public void setAssignedSessions(Long assignedSessions) { this.assignedSessions = assignedSessions; }
|
||||||
public Integer getIsDeleted() { return isDeleted; }
|
public Integer getIsDeleted() { return isDeleted; }
|
||||||
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
||||||
public Long[] getAttendeeUserIds() { return attendeeUserIds; }
|
public Long[] getAttendeeUserIds() { return attendeeUserIds; }
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ public class BizMeetingAttendee extends BaseEntity {
|
|||||||
private transient Date endTime;
|
private transient Date endTime;
|
||||||
private transient String projectName;
|
private transient String projectName;
|
||||||
private transient String projectNo;
|
private transient String projectNo;
|
||||||
|
/** 该参会人是否已报名该项目 (biz_execution_intent 存在 user_id + project_no): 1已报名 0未报名; 会议详情参会人列表用, 未报名姓名标红 */
|
||||||
|
private transient Integer hasIntent;
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
public void setId(Long id) { this.id = id; }
|
public void setId(Long id) { this.id = id; }
|
||||||
@@ -131,6 +133,8 @@ public class BizMeetingAttendee extends BaseEntity {
|
|||||||
public void setProjectName(String projectName) { this.projectName = projectName; }
|
public void setProjectName(String projectName) { this.projectName = projectName; }
|
||||||
public String getProjectNo() { return projectNo; }
|
public String getProjectNo() { return projectNo; }
|
||||||
public void setProjectNo(String projectNo) { this.projectNo = projectNo; }
|
public void setProjectNo(String projectNo) { this.projectNo = projectNo; }
|
||||||
|
public Integer getHasIntent() { return hasIntent; }
|
||||||
|
public void setHasIntent(Integer hasIntent) { this.hasIntent = hasIntent; }
|
||||||
/** 软删除标记 0否1是 (admin 删除会议时级联置1, 查询需 WHERE is_deleted=0) */
|
/** 软删除标记 0否1是 (admin 删除会议时级联置1, 查询需 WHERE is_deleted=0) */
|
||||||
private Integer isDeleted;
|
private Integer isDeleted;
|
||||||
public Integer getIsDeleted() { return isDeleted; }
|
public Integer getIsDeleted() { return isDeleted; }
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ public class BizPerson extends BaseEntity {
|
|||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
/** 关联系统用户ID */
|
/** 关联系统用户ID */
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
/** 是否供应商同步过来的用户 (1=是, 0=否) */
|
||||||
|
private Integer isSynced;
|
||||||
/** 启停状态 '0'/'1' (前端 toggle 用, BizPersonServiceImpl 同步到 sys_user.status) - 非持久化字段 */
|
/** 启停状态 '0'/'1' (前端 toggle 用, BizPersonServiceImpl 同步到 sys_user.status) - 非持久化字段 */
|
||||||
@com.fasterxml.jackson.annotation.JsonProperty("status")
|
@com.fasterxml.jackson.annotation.JsonProperty("status")
|
||||||
private transient String status;
|
private transient String status;
|
||||||
@@ -94,6 +96,8 @@ public class BizPerson extends BaseEntity {
|
|||||||
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
|
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
|
||||||
public Long getUserId() { return userId; }
|
public Long getUserId() { return userId; }
|
||||||
public void setUserId(Long userId) { this.userId = userId; }
|
public void setUserId(Long userId) { this.userId = userId; }
|
||||||
|
public Integer getIsSynced() { return isSynced; }
|
||||||
|
public void setIsSynced(Integer isSynced) { this.isSynced = isSynced; }
|
||||||
public String getStatus() { return status; }
|
public String getStatus() { return status; }
|
||||||
public void setStatus(String status) { this.status = status; }
|
public void setStatus(String status) { this.status = status; }
|
||||||
public String getUnitType() { return unitType; }
|
public String getUnitType() { return unitType; }
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ public class BizProjectPlan extends BaseEntity {
|
|||||||
@Excel(name = "update_time")
|
@Excel(name = "update_time")
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
|
/** 提交时间 (状态→待审核 '1' 时写入, 列表按此倒序) */
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date submitTime;
|
||||||
/** 审核意见 */
|
/** 审核意见 */
|
||||||
private String auditOpinion;
|
private String auditOpinion;
|
||||||
/** 审核人 */
|
/** 审核人 */
|
||||||
@@ -115,6 +118,8 @@ public class BizProjectPlan extends BaseEntity {
|
|||||||
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
|
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
|
||||||
public Date getUpdateTime() { return updateTime; }
|
public Date getUpdateTime() { return updateTime; }
|
||||||
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
|
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
|
||||||
|
public Date getSubmitTime() { return submitTime; }
|
||||||
|
public void setSubmitTime(Date submitTime) { this.submitTime = submitTime; }
|
||||||
public String getAuditOpinion() { return auditOpinion; }
|
public String getAuditOpinion() { return auditOpinion; }
|
||||||
public void setAuditOpinion(String auditOpinion) { this.auditOpinion = auditOpinion; }
|
public void setAuditOpinion(String auditOpinion) { this.auditOpinion = auditOpinion; }
|
||||||
public String getAuditBy() { return auditBy; }
|
public String getAuditBy() { return auditBy; }
|
||||||
|
|||||||
+1
-6
@@ -38,10 +38,7 @@ public class BizPublicityExecutionIntentExportVo {
|
|||||||
@Excel(name = "账号状态", sort = 8)
|
@Excel(name = "账号状态", sort = 8)
|
||||||
private String userStatus;
|
private String userStatus;
|
||||||
|
|
||||||
@Excel(name = "审核状态", sort = 9)
|
@Excel(name = "创建时间", sort = 9, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||||
private String intentStatus;
|
|
||||||
|
|
||||||
@Excel(name = "创建时间", sort = 10, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@@ -61,8 +58,6 @@ public class BizPublicityExecutionIntentExportVo {
|
|||||||
public void setPhone(String phone) { this.phone = phone; }
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
public String getUserStatus() { return userStatus; }
|
public String getUserStatus() { return userStatus; }
|
||||||
public void setUserStatus(String userStatus) { this.userStatus = userStatus; }
|
public void setUserStatus(String userStatus) { this.userStatus = userStatus; }
|
||||||
public String getIntentStatus() { return intentStatus; }
|
|
||||||
public void setIntentStatus(String intentStatus) { this.intentStatus = intentStatus; }
|
|
||||||
public Date getCreateTime() { return createTime; }
|
public Date getCreateTime() { return createTime; }
|
||||||
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-6
@@ -38,10 +38,7 @@ public class BizPublicitySupportIntentExportVo {
|
|||||||
@Excel(name = "账号状态", sort = 8)
|
@Excel(name = "账号状态", sort = 8)
|
||||||
private String userStatus;
|
private String userStatus;
|
||||||
|
|
||||||
@Excel(name = "审核状态", sort = 9)
|
@Excel(name = "创建时间", sort = 9, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||||
private String intentStatus;
|
|
||||||
|
|
||||||
@Excel(name = "创建时间", sort = 10, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@@ -61,8 +58,6 @@ public class BizPublicitySupportIntentExportVo {
|
|||||||
public void setPhone(String phone) { this.phone = phone; }
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
public String getUserStatus() { return userStatus; }
|
public String getUserStatus() { return userStatus; }
|
||||||
public void setUserStatus(String userStatus) { this.userStatus = userStatus; }
|
public void setUserStatus(String userStatus) { this.userStatus = userStatus; }
|
||||||
public String getIntentStatus() { return intentStatus; }
|
|
||||||
public void setIntentStatus(String intentStatus) { this.intentStatus = intentStatus; }
|
|
||||||
public Date getCreateTime() { return createTime; }
|
public Date getCreateTime() { return createTime; }
|
||||||
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-3
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.business.mapper;
|
package com.ruoyi.business.mapper;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import com.ruoyi.business.domain.BizMeeting;
|
import com.ruoyi.business.domain.BizMeeting;
|
||||||
|
|
||||||
@@ -11,6 +12,11 @@ public interface BizMeetingMapper
|
|||||||
{
|
{
|
||||||
BizMeeting selectByPrimaryKey(Long meetingId);
|
BizMeeting selectByPrimaryKey(Long meetingId);
|
||||||
List<BizMeeting> selectList(BizMeeting entity);
|
List<BizMeeting> selectList(BizMeeting entity);
|
||||||
|
/**
|
||||||
|
* 会议阶段统计 (仅 sponsor/Home KPI 用): 按 current_stage 分组计数, 只套 sponsor 数据权限 (不参与分页).
|
||||||
|
* 返回 [{currentStage: 'RUNNING', cnt: 10}, ...]. 其他角色的统计另行实现, 不在此复用多角色过滤.
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> selectStageStats(BizMeeting entity);
|
||||||
int insert(BizMeeting entity);
|
int insert(BizMeeting entity);
|
||||||
int updateByPrimaryKey(BizMeeting entity);
|
int updateByPrimaryKey(BizMeeting entity);
|
||||||
int deleteByPrimaryKey(Long meetingId);
|
int deleteByPrimaryKey(Long meetingId);
|
||||||
@@ -25,8 +31,15 @@ public interface BizMeetingMapper
|
|||||||
int countByProjectIdAndExecutionUnit(@Param("projectId") Long projectId, @Param("executionUnitId") Long executionUnitId);
|
int countByProjectIdAndExecutionUnit(@Param("projectId") Long projectId, @Param("executionUnitId") Long executionUnitId);
|
||||||
/** 建会校验用 (执行方隔离): 统计某项目下、某执行方、某期数的未软删会议数 (相同期数冲突检测) */
|
/** 建会校验用 (执行方隔离): 统计某项目下、某执行方、某期数的未软删会议数 (相同期数冲突检测) */
|
||||||
int countByProjectIdExecutionUnitPeriod(@Param("projectId") Long projectId, @Param("executionUnitId") Long executionUnitId, @Param("periodNo") Long periodNo);
|
int countByProjectIdExecutionUnitPeriod(@Param("projectId") Long projectId, @Param("executionUnitId") Long executionUnitId, @Param("periodNo") Long periodNo);
|
||||||
|
/** 修改校验用 (执行方隔离): 同上, 但排除指定 meetingId (修改自身会议不触发相同期数误报) */
|
||||||
|
int countByProjectIdExecutionUnitPeriodExclude(@Param("projectId") Long projectId, @Param("executionUnitId") Long executionUnitId, @Param("periodNo") Long periodNo, @Param("excludeMeetingId") Long excludeMeetingId);
|
||||||
/**
|
/**
|
||||||
* 自动流转: start_time 已过 且 material 未提交 (NOT_SUBMITTED) 且未执行的会议 → 置 is_executed=1 并转 RUNNING.
|
* 自动流转: start_time 已过 且 end_time 未到 且 material 未提交 (NOT_SUBMITTED) 且未执行的会议 → 置 current_stage = IN_PROGRESS (执行中).
|
||||||
|
* <p>由 MeetingStageScheduler 每分钟触发. 只写 current_stage 缓存, 不动 is_executed 事实.
|
||||||
|
*/
|
||||||
|
int markInProgress();
|
||||||
|
/**
|
||||||
|
* 自动流转: end_time 已过 且 material 未提交 (NOT_SUBMITTED) 且未执行的会议 → 置 is_executed=1 并转 RUNNING (已执行).
|
||||||
* <p>由 MeetingStageScheduler 每分钟触发. 事实 + current_stage 缓存一起写.
|
* <p>由 MeetingStageScheduler 每分钟触发. 事实 + current_stage 缓存一起写.
|
||||||
*/
|
*/
|
||||||
int markExecuted();
|
int markExecuted();
|
||||||
@@ -40,9 +53,10 @@ public interface BizMeetingMapper
|
|||||||
*/
|
*/
|
||||||
List<Long> selectPendingFeeCalcIds();
|
List<Long> selectPendingFeeCalcIds();
|
||||||
/**
|
/**
|
||||||
* 置未汇总 (人员/材料变化触发, 幂等).
|
* 费用重算状态机用: 直接置 fee_calc_status = #{status} (-1 计算中 / 0 待算兜底).
|
||||||
|
* <p>成功(1) 由 {@link #updateFeeSummary} 一并写入, 不单独置.
|
||||||
*/
|
*/
|
||||||
int markFeeCalcPending(Long meetingId);
|
int updateFeeCalcStatus(@Param("meetingId") Long meetingId, @Param("status") int status);
|
||||||
/**
|
/**
|
||||||
* 汇总回写 labor_fee/meeting_fee/total_fee 并置 fee_calc_status=1.
|
* 汇总回写 labor_fee/meeting_fee/total_fee 并置 fee_calc_status=1.
|
||||||
*/
|
*/
|
||||||
@@ -50,4 +64,11 @@ public interface BizMeetingMapper
|
|||||||
@Param("laborFee") BigDecimal laborFee,
|
@Param("laborFee") BigDecimal laborFee,
|
||||||
@Param("meetingFee") BigDecimal meetingFee,
|
@Param("meetingFee") BigDecimal meetingFee,
|
||||||
@Param("totalFee") BigDecimal totalFee);
|
@Param("totalFee") BigDecimal totalFee);
|
||||||
|
/**
|
||||||
|
* 同步重算劳务费 (参会人增删改后立即调用): 只回写 labor_fee + total_fee,
|
||||||
|
* 不动 meeting_fee / fee_calc_status (参会人变化不影响会务费, 也不触发整体重算).
|
||||||
|
*/
|
||||||
|
int updateLaborFee(@Param("meetingId") Long meetingId,
|
||||||
|
@Param("laborFee") BigDecimal laborFee,
|
||||||
|
@Param("totalFee") BigDecimal totalFee);
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,9 @@ public interface BizMessageMapper
|
|||||||
/** 收件人未读总数 (SSE 推送用, 跟 limit 解耦) */
|
/** 收件人未读总数 (SSE 推送用, 跟 limit 解耦) */
|
||||||
int countUnread(Long receiverUserId);
|
int countUnread(Long receiverUserId);
|
||||||
|
|
||||||
|
/** 收件人的全部消息总数 (分页 total 用, 跟未读解耦) */
|
||||||
|
int countMy(Long receiverUserId);
|
||||||
|
|
||||||
int insert(BizMessage entity);
|
int insert(BizMessage entity);
|
||||||
|
|
||||||
int updateByPrimaryKey(BizMessage entity);
|
int updateByPrimaryKey(BizMessage entity);
|
||||||
|
|||||||
@@ -31,4 +31,6 @@ public interface BizOrgMapper {
|
|||||||
/** user_id → org_id 单一可信源反查: COALESCE(biz_org.user_id, biz_person.user_id)
|
/** user_id → org_id 单一可信源反查: COALESCE(biz_org.user_id, biz_person.user_id)
|
||||||
* MAIN 主账号走 biz_org.user_id; SUB 子账号走 biz_person.org_id; 都没有返回 null */
|
* MAIN 主账号走 biz_org.user_id; SUB 子账号走 biz_person.org_id; 都没有返回 null */
|
||||||
Long selectOrgIdByUserId(Long userId);
|
Long selectOrgIdByUserId(Long userId);
|
||||||
|
/** 单位名称查重: 同 orgType 下 org_name 精确匹配的条数 (新增单位前判重) */
|
||||||
|
int countByOrgName(BizOrg entity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ public interface BizPersonMapper
|
|||||||
List<BizPerson> selectSponsorList(BizPerson entity);
|
List<BizPerson> selectSponsorList(BizPerson entity);
|
||||||
/** executor 专属: 同 sponsor, SQL 硬编码 unit_type='executor' (前端绕不开) */
|
/** executor 专属: 同 sponsor, SQL 硬编码 unit_type='executor' (前端绕不开) */
|
||||||
List<BizPerson> selectExecutorList(BizPerson entity);
|
List<BizPerson> selectExecutorList(BizPerson entity);
|
||||||
|
/** 按 user_id 反查人员档案 (个人资料编辑: 拿 personId 再走 updateByPrimaryKey) */
|
||||||
|
BizPerson selectByUserId(Long userId);
|
||||||
|
/** 按 org_id 反查该机构主账号 user_id (sys_user.account_type='MAIN' + biz_person.org_id 绑定), 不走 biz_org.user_id (可能脏) */
|
||||||
|
Long selectMainUserIdByOrgId(Long orgId);
|
||||||
int insert(BizPerson entity);
|
int insert(BizPerson entity);
|
||||||
int updateByPrimaryKey(BizPerson entity);
|
int updateByPrimaryKey(BizPerson entity);
|
||||||
int deleteByPrimaryKey(String personId);
|
int deleteByPrimaryKey(String personId);
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package com.ruoyi.business.oss;
|
package com.ruoyi.business.oss;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLConnection;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -199,4 +203,18 @@ public class OssZipService
|
|||||||
}
|
}
|
||||||
return location.replaceFirst("^http://", "https://");
|
return location.replaceFirst("^http://", "https://");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载已打包好的 zip 字节流 (从 FC 返回的签名 URL 拉取).
|
||||||
|
* 后端代理改名下发用: 先拿到 zip 字节流, 再以自定义文件名写回 response (见 BizMeetingMaterialController),
|
||||||
|
* 绕开 FC 固定命名 output_1-xxx.zip 的问题.
|
||||||
|
*/
|
||||||
|
public InputStream openZipStream(String signedUrl) throws IOException
|
||||||
|
{
|
||||||
|
URI uri = URI.create(signedUrl);
|
||||||
|
URLConnection conn = uri.toURL().openConnection();
|
||||||
|
conn.setConnectTimeout(10000);
|
||||||
|
conn.setReadTimeout(120000);
|
||||||
|
return conn.getInputStream();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-79
@@ -1,29 +1,25 @@
|
|||||||
package com.ruoyi.business.scheduler;
|
package com.ruoyi.business.scheduler;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import com.ruoyi.business.domain.BizMeetingMaterial;
|
|
||||||
import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
|
|
||||||
import com.ruoyi.business.mapper.BizMeetingMapper;
|
import com.ruoyi.business.mapper.BizMeetingMapper;
|
||||||
import com.ruoyi.business.mapper.BizMeetingMaterialMapper;
|
import com.ruoyi.business.service.IBizMeetingService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 会议费用汇总调度器 (每分钟一次, 多线程无锁).
|
* 会议费用汇总调度器 (每分钟一次, 多线程无锁) — 兜底通道.
|
||||||
* <p>
|
* <p>
|
||||||
* 两态设计 (不用抢占锁): 会议 fee_calc_status (0未汇总/1已汇总) + 材料 fee_status (0未计算/1已计算).
|
* 主通道: 材料保存/上传、发票 OCR 完成后由 {@link IBizMeetingService#recomputeMeetingFee} 立即同步重算,
|
||||||
|
* 状态机 -1(计算中) → 1(成功) / 0(失败回滚). 本调度器只兜底扫描 fee_calc_status=0 的会议重试,
|
||||||
|
* 覆盖"立即算时发票还没 OCR 完 / 算错"留下的 0 态.
|
||||||
* <pre>
|
* <pre>
|
||||||
* 每分钟: 查 fee_calc_status=0 的会议
|
* 每分钟: 查 fee_calc_status=0 的会议 → 逐个交给 recomputeMeetingFee 重算
|
||||||
* → 任一材料 fee_status=0 (发票还没 OCR 完) → 跳过, 等下轮
|
|
||||||
* → 全部 fee_status=1 → SUM 汇总 labor_fee/meeting_fee/total_fee → fee_calc_status=1
|
|
||||||
* </pre>
|
* </pre>
|
||||||
* 为什么不需要锁: 汇总前已检查"材料全算完", 天然防半成品; 汇总纯 SUM 幂等, 并发重复无副作用;
|
* 为什么不需要锁: 立即算/兜底都是纯 SUM 幂等, 置 1 后不再被扫到, 天然去重; -1 期间也不会被扫 (=0 才扫).
|
||||||
* 置 1 后不再被扫到, 天然去重. 重算 = 只对 material + attendee 重新求和, 不碰 OCR.
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@@ -32,15 +28,13 @@ public class FeeCalcScheduler
|
|||||||
@Autowired
|
@Autowired
|
||||||
private BizMeetingMapper meetingMapper;
|
private BizMeetingMapper meetingMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private BizMeetingMaterialMapper materialMapper;
|
private IBizMeetingService bizMeetingService;
|
||||||
@Autowired
|
|
||||||
private BizMeetingAttendeeMapper attendeeMapper;
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("feeCalcExecutor")
|
@Qualifier("feeCalcExecutor")
|
||||||
private ExecutorService feeCalcExecutor;
|
private ExecutorService feeCalcExecutor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每分钟: 扫描 fee_calc_status=0 的会议, 并行汇总.
|
* 每分钟: 扫描 fee_calc_status=0 的会议, 并行兜底重算.
|
||||||
*/
|
*/
|
||||||
@Scheduled(fixedRate = 60_000, initialDelay = 60_000)
|
@Scheduled(fixedRate = 60_000, initialDelay = 60_000)
|
||||||
public void calcFees()
|
public void calcFees()
|
||||||
@@ -52,11 +46,11 @@ public class FeeCalcScheduler
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info("[FeeCalcScheduler] 待汇总会议 {} 个", ids.size());
|
log.info("[FeeCalcScheduler] 待兜底汇总会议 {} 个", ids.size());
|
||||||
for (Long meetingId : ids)
|
for (Long meetingId : ids)
|
||||||
{
|
{
|
||||||
if (meetingId == null) continue;
|
if (meetingId == null) continue;
|
||||||
feeCalcExecutor.submit(() -> processMeeting(meetingId));
|
feeCalcExecutor.submit(() -> bizMeetingService.recomputeMeetingFee(meetingId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -64,66 +58,4 @@ public class FeeCalcScheduler
|
|||||||
log.warn("[FeeCalcScheduler] 扫描异常 (跳过, 下分钟再试)", e);
|
log.warn("[FeeCalcScheduler] 扫描异常 (跳过, 下分钟再试)", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processMeeting(Long meetingId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
List<BizMeetingMaterial> mats = materialMapper.selectByMeetingId(meetingId);
|
|
||||||
// 任一材料 fee_status=0 (发票待 OCR) → 跳过
|
|
||||||
if (mats != null)
|
|
||||||
{
|
|
||||||
for (BizMeetingMaterial m : mats)
|
|
||||||
{
|
|
||||||
if (m.getFeeStatus() != null && m.getFeeStatus() == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BigDecimal laborFee = attendeeMapper.sumFeePreTaxByMeetingId(meetingId);
|
|
||||||
if (laborFee == null) laborFee = BigDecimal.ZERO;
|
|
||||||
BigDecimal meetingFee = computeMeetingFee(mats);
|
|
||||||
BigDecimal totalFee = laborFee.add(meetingFee);
|
|
||||||
|
|
||||||
meetingMapper.updateFeeSummary(meetingId, laborFee, meetingFee, totalFee);
|
|
||||||
log.info("[FeeCalcScheduler] 汇总完成 meetingId={} labor={} meeting={} total={}",
|
|
||||||
meetingId, laborFee, meetingFee, totalFee);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
log.warn("[FeeCalcScheduler] 汇总失败 meetingId={} err={}", meetingId, e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 会务费口径: 有总发票 (M_INVOICE 金额>0) 则只用总发票; 否则各 SUB 子类 (M_ 开头) 金额之和,
|
|
||||||
* 排除 M_INVOICE / M_SETTLEMENT (两者是汇总单据, 非子类发票).
|
|
||||||
*/
|
|
||||||
private BigDecimal computeMeetingFee(List<BizMeetingMaterial> mats)
|
|
||||||
{
|
|
||||||
if (mats == null || mats.isEmpty()) return BigDecimal.ZERO;
|
|
||||||
BigDecimal main = null;
|
|
||||||
for (BizMeetingMaterial m : mats)
|
|
||||||
{
|
|
||||||
if ("M_INVOICE".equals(m.getSubType()))
|
|
||||||
{
|
|
||||||
main = m.getAmount();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (main != null && main.compareTo(BigDecimal.ZERO) > 0)
|
|
||||||
{
|
|
||||||
return main;
|
|
||||||
}
|
|
||||||
BigDecimal sum = BigDecimal.ZERO;
|
|
||||||
for (BizMeetingMaterial m : mats)
|
|
||||||
{
|
|
||||||
if (m.getSubType() == null || !m.getSubType().startsWith("M_")) continue;
|
|
||||||
if ("M_INVOICE".equals(m.getSubType()) || "M_SETTLEMENT".equals(m.getSubType())) continue;
|
|
||||||
if (m.getAmount() != null) sum = sum.add(m.getAmount());
|
|
||||||
}
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-6
@@ -11,10 +11,11 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
* <p>
|
* <p>
|
||||||
* 状态机已改为「事实 + 推导」模型 (见 {@code StageDeriver}): biz_meeting 存事实
|
* 状态机已改为「事实 + 推导」模型 (见 {@code StageDeriver}): biz_meeting 存事实
|
||||||
* (is_executed / is_frozen / 劳务·会务两轨 audit_stage / 审核时间 …),
|
* (is_executed / is_frozen / 劳务·会务两轨 audit_stage / 审核时间 …),
|
||||||
* 各角色看到的阶段名称由推导得出. 本调度器只负责「时间驱动」的两类事实落地:
|
* 各角色看到的阶段名称由推导得出. 本调度器只负责「时间驱动」的三类事实落地:
|
||||||
* <pre>
|
* <pre>
|
||||||
* 1) start_time 到 → is_executed=1 (执行中)
|
* 1) start_time 到 → current_stage = IN_PROGRESS (执行中)
|
||||||
* 2) submit_deadline 到 且 任一轨未提交/已退回 → is_frozen=1 (冻结)
|
* 2) end_time 到 → is_executed=1 (已执行)
|
||||||
|
* 3) submit_deadline 到 且 任一轨未提交/已退回 → is_frozen=1 (冻结)
|
||||||
* </pre>
|
* </pre>
|
||||||
* 其余阶段流转由执行方提交 / 审核动作触发 (BizMeetingController), 不在此调度器范围.
|
* 其余阶段流转由执行方提交 / 审核动作触发 (BizMeetingController), 不在此调度器范围.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -28,7 +29,27 @@ public class MeetingStageScheduler
|
|||||||
private BizMeetingMapper meetingMapper;
|
private BizMeetingMapper meetingMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每分钟: start_time 已过 且 劳务·会务两轨均未提交 且未执行 → 置执行中.
|
* 每分钟: start_time 已过 且 end_time 未到 且 劳务·会务两轨均未提交 且未执行 → 置执行中 (IN_PROGRESS).
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
||||||
|
public void markInProgress()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int affected = meetingMapper.markInProgress();
|
||||||
|
if (affected > 0)
|
||||||
|
{
|
||||||
|
log.info("[MeetingStageScheduler] 自动置执行中: 本次更新 {} 行", affected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.warn("[MeetingStageScheduler] 置执行中异常 (跳过, 下分钟再试)", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每分钟: end_time 已过 且 劳务·会务两轨均未提交 且未执行 → 置已执行 (is_executed=1).
|
||||||
*/
|
*/
|
||||||
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
||||||
public void markExecuted()
|
public void markExecuted()
|
||||||
@@ -38,12 +59,12 @@ public class MeetingStageScheduler
|
|||||||
int affected = meetingMapper.markExecuted();
|
int affected = meetingMapper.markExecuted();
|
||||||
if (affected > 0)
|
if (affected > 0)
|
||||||
{
|
{
|
||||||
log.info("[MeetingStageScheduler] 自动置执行中: 本次更新 {} 行", affected);
|
log.info("[MeetingStageScheduler] 自动置已执行: 本次更新 {} 行", affected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
log.warn("[MeetingStageScheduler] 置执行中异常 (跳过, 下分钟再试)", e);
|
log.warn("[MeetingStageScheduler] 置已执行异常 (跳过, 下分钟再试)", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-4
@@ -3,27 +3,31 @@ package com.ruoyi.business.scheduler;
|
|||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ruoyi.business.supplier.SupplierAccount;
|
||||||
import com.ruoyi.business.supplier.SupplierAccountApiCodec;
|
import com.ruoyi.business.supplier.SupplierAccountApiCodec;
|
||||||
|
import com.ruoyi.business.supplier.SupplierAccountSyncService;
|
||||||
import com.ruoyi.common.utils.http.HttpUtils;
|
import com.ruoyi.common.utils.http.HttpUtils;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 供应商账号数据拉取调度器: 每分钟拉取最近 5 分钟更新的账号, 解密后打印.
|
* 供应商账号数据拉取调度器: 每分钟拉取最近 N 分钟更新的账号, 解密后同步到执行方侧.
|
||||||
* <p>
|
* <p>
|
||||||
* 数据源: {@code GET /supplier-api/bidding/supplier/openapi/accounts}
|
* 数据源: {@code GET /supplier-api/bidding/supplier/openapi/accounts}
|
||||||
* 入参 lastUpdatedTime(最后更新时间) / pageNum / pageSize, 按更新时间倒序返回.
|
* 入参 lastUpdatedTime(最后更新时间) / pageNum / pageSize, 按更新时间倒序返回.
|
||||||
* 返回 data 字段为 AES-256-GCM 加密串, 用 {@link SupplierAccountApiCodec} 解密.
|
* 返回 data 字段为 AES-256-GCM 加密串, 用 {@link SupplierAccountApiCodec} 解密.
|
||||||
* <p>
|
* <p>
|
||||||
* 说明: 只打印不落库 (后续需要持久化时再扩展).
|
* 解密后按邮箱 upsert 到 sys_user / biz_org / biz_person (见 {@link SupplierAccountSyncService}).
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@@ -44,6 +48,9 @@ public class SupplierAccountPullScheduler
|
|||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SupplierAccountSyncService supplierAccountSyncService;
|
||||||
|
|
||||||
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
||||||
public void pullAccounts()
|
public void pullAccounts()
|
||||||
{
|
{
|
||||||
@@ -87,8 +94,14 @@ public class SupplierAccountPullScheduler
|
|||||||
int size = rows.isArray() ? rows.size() : 0;
|
int size = rows.isArray() ? rows.size() : 0;
|
||||||
fetched += size;
|
fetched += size;
|
||||||
|
|
||||||
// 只打印即可: 整页明文 JSON 打出来 (供观察/后续落库)
|
// 同步到执行方 sys_user / biz_org / biz_person (按邮箱 upsert)
|
||||||
log.info("[SupplierAccountPull] page={} total={} 本页={} 明文: {}", pageNum, total, size, plain);
|
if (size > 0)
|
||||||
|
{
|
||||||
|
List<SupplierAccount> accounts =
|
||||||
|
objectMapper.convertValue(rows, new TypeReference<List<SupplierAccount>>() {});
|
||||||
|
supplierAccountSyncService.sync(accounts);
|
||||||
|
}
|
||||||
|
log.info("[SupplierAccountPull] page={} total={} 本页={} 同步完成", pageNum, total, size);
|
||||||
|
|
||||||
if (size == 0 || fetched >= total)
|
if (size == 0 || fetched >= total)
|
||||||
{
|
{
|
||||||
|
|||||||
+21
@@ -28,6 +28,15 @@ public interface IBizMeetingMaterialService {
|
|||||||
*/
|
*/
|
||||||
List<BizMeetingMaterial> replaceByMeetingId(Long meetingId, List<BizMeetingMaterial> list);
|
List<BizMeetingMaterial> replaceByMeetingId(Long meetingId, List<BizMeetingMaterial> list);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断保存的材料集合相对库里是否有实际变化 (增/删 subType, 或同 subType 的 ossUrl 变化).
|
||||||
|
* <p>
|
||||||
|
* 用于"保存"按钮: 会务材料没变时不应把会议费用置"统计中" (fee_calc_status=0),
|
||||||
|
* 避免无谓的汇总重算与前端"统计中"闪烁.
|
||||||
|
* 必须在 {@link #replaceByMeetingId} 之前调用 (后者会先删旧记录).
|
||||||
|
*/
|
||||||
|
boolean isMaterialSetChanged(Long meetingId, List<BizMeetingMaterial> list);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单条更新 amount (OCR 识别为发票后回写).
|
* 单条更新 amount (OCR 识别为发票后回写).
|
||||||
* 不动其他字段, 不抛异常 (失败仅 log).
|
* 不动其他字段, 不抛异常 (失败仅 log).
|
||||||
@@ -81,6 +90,18 @@ public interface IBizMeetingMaterialService {
|
|||||||
*/
|
*/
|
||||||
String buildBatchLaborZipUrl(List<Long> meetingIds);
|
String buildBatchLaborZipUrl(List<Long> meetingIds);
|
||||||
|
|
||||||
|
/** 单个会务下载文件名 (项目编号_项目名称_第N期_会务.zip) */
|
||||||
|
String serviceZipFilename(Long meetingId);
|
||||||
|
|
||||||
|
/** 单个劳务下载文件名 (项目编号_项目名称_第N期_劳务.zip) */
|
||||||
|
String laborZipFilename(Long meetingId);
|
||||||
|
|
||||||
|
/** 批量会务下载文件名 (会务_下载时间.zip) */
|
||||||
|
String batchServiceZipFilename();
|
||||||
|
|
||||||
|
/** 批量劳务下载文件名 (劳务_下载时间.zip) */
|
||||||
|
String batchLaborZipFilename();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成"会务材料"空目录模板 zip (会务材料 tab "打包上传" 的"下载目录"按钮).
|
* 生成"会务材料"空目录模板 zip (会务材料 tab "打包上传" 的"下载目录"按钮).
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
+21
-2
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.business.service;
|
package com.ruoyi.business.service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import com.ruoyi.business.domain.BizMeeting;
|
import com.ruoyi.business.domain.BizMeeting;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,6 +11,11 @@ public interface IBizMeetingService
|
|||||||
{
|
{
|
||||||
BizMeeting getById(Long meetingId);
|
BizMeeting getById(Long meetingId);
|
||||||
List<BizMeeting> selectList(BizMeeting entity);
|
List<BizMeeting> selectList(BizMeeting entity);
|
||||||
|
/**
|
||||||
|
* 会议阶段统计 (仅 sponsor/Home KPI 用): 按 current_stage 分组计数, 只套 sponsor 数据权限.
|
||||||
|
* 返回 [{currentStage: 'RUNNING', cnt: 10}, ...]. 其他角色的统计另行实现.
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> selectStageStats(BizMeeting entity);
|
||||||
int insert(BizMeeting entity);
|
int insert(BizMeeting entity);
|
||||||
int updateByPrimaryKey(BizMeeting entity);
|
int updateByPrimaryKey(BizMeeting entity);
|
||||||
int deleteByPrimaryKey(Long meetingId);
|
int deleteByPrimaryKey(Long meetingId);
|
||||||
@@ -28,6 +34,19 @@ public interface IBizMeetingService
|
|||||||
int countByProjectIdAndExecutionUnit(Long projectId, Long executionUnitId);
|
int countByProjectIdAndExecutionUnit(Long projectId, Long executionUnitId);
|
||||||
/** 建会校验用 (执行方隔离): 统计某项目下、某执行方、某期数的未软删会议数 (相同期数冲突检测) */
|
/** 建会校验用 (执行方隔离): 统计某项目下、某执行方、某期数的未软删会议数 (相同期数冲突检测) */
|
||||||
int countByProjectIdExecutionUnitPeriod(Long projectId, Long executionUnitId, Long periodNo);
|
int countByProjectIdExecutionUnitPeriod(Long projectId, Long executionUnitId, Long periodNo);
|
||||||
/** 标记会议费用待重算 (人员/材料变化触发, 幂等; 由 FeeCalcScheduler 汇总回写) */
|
/** 修改校验用 (执行方隔离): 同上, 但排除指定 meetingId (修改自身会议不触发相同期数误报) */
|
||||||
void markFeeCalcPending(Long meetingId);
|
int countByProjectIdExecutionUnitPeriodExclude(Long projectId, Long executionUnitId, Long periodNo, Long excludeMeetingId);
|
||||||
|
/**
|
||||||
|
* 立即重算会议费用 (会务材料保存/上传、发票 OCR 完成后同步触发, 不再等 FeeCalcScheduler 每分钟扫).
|
||||||
|
* <p>状态机: -1(计算中) → 算成功 1 / 材料发票仍未 OCR 完或异常 回滚 0 走 FeeCalcScheduler 兜底.
|
||||||
|
* 全程一个事务, 失败不落 -1 残留.
|
||||||
|
*/
|
||||||
|
void recomputeMeetingFee(Long meetingId);
|
||||||
|
/**
|
||||||
|
* 同步重算劳务费 (参会人增删改后立即调用).
|
||||||
|
* <p>labor_fee = 参会人应发金额之和, 纯 DB 求和不依赖 OCR, 可当场算;
|
||||||
|
* total_fee = labor_fee + 当前 meeting_fee. 不动 meeting_fee / fee_calc_status
|
||||||
|
* (参会人变化不影响会务费, 也不触发整体重算, 避免"统计中"等待 60s 调度器).
|
||||||
|
*/
|
||||||
|
void recomputeLaborFee(Long meetingId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ public interface IBizMessageService
|
|||||||
/** 收件人的最近消息 (含未读) */
|
/** 收件人的最近消息 (含未读) */
|
||||||
List<BizMessage> selectMyRecent(Long receiverUserId, Integer limit);
|
List<BizMessage> selectMyRecent(Long receiverUserId, Integer limit);
|
||||||
|
|
||||||
|
/** 收件人的分页消息 (offset/limit, 未读在前) */
|
||||||
|
List<BizMessage> selectMyPage(Long receiverUserId, int offset, int pageSize);
|
||||||
|
|
||||||
|
/** 收件人的全部消息总数 (分页 total) */
|
||||||
|
int countMy(Long receiverUserId);
|
||||||
|
|
||||||
/** 收件人未读总数 (SSE 推送用) */
|
/** 收件人未读总数 (SSE 推送用) */
|
||||||
int countUnread(Long receiverUserId);
|
int countUnread(Long receiverUserId);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ public interface IBizPersonService
|
|||||||
List<BizPerson> selectExecutorList(BizPerson entity);
|
List<BizPerson> selectExecutorList(BizPerson entity);
|
||||||
SysUser insert(BizPerson entity, Long mainUserId);
|
SysUser insert(BizPerson entity, Long mainUserId);
|
||||||
int updateByPrimaryKey(BizPerson entity);
|
int updateByPrimaryKey(BizPerson entity);
|
||||||
|
/** 个人资料编辑: 按 user_id 反查 personId 后走 updateByPrimaryKey (同步 nick_name/phonenumber/email) */
|
||||||
|
int updateProfileByUserId(BizPerson entity);
|
||||||
int deleteByPrimaryKey(String personId);
|
int deleteByPrimaryKey(String personId);
|
||||||
int deleteByPrimaryKeys(String[] personId);
|
int deleteByPrimaryKeys(String[] personId);
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.ruoyi.business.service;
|
package com.ruoyi.business.service;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import com.ruoyi.business.domain.BizMeeting;
|
import com.ruoyi.business.domain.BizMeeting;
|
||||||
|
|
||||||
@@ -50,6 +52,8 @@ public class StageDeriver
|
|||||||
* 10 值物理阶段 (NOT_STARTED/RUNNING/AWAITING_COMPLIANCE/AWAITING_SUPERVISION/
|
* 10 值物理阶段 (NOT_STARTED/RUNNING/AWAITING_COMPLIANCE/AWAITING_SUPERVISION/
|
||||||
* SUPERVISION_APPROVED/RECTIFYING/AWAITING_SETTLEMENT/SETTLED/FINISHED/FROZEN), 由事实推导.
|
* SUPERVISION_APPROVED/RECTIFYING/AWAITING_SETTLEMENT/SETTLED/FINISHED/FROZEN), 由事实推导.
|
||||||
* 两轨取最小进度 (流程最靠前的一轨决定会议物理态), 用于 current_stage 缓存 (列表筛选按物理态精确匹配).
|
* 两轨取最小进度 (流程最靠前的一轨决定会议物理态), 用于 current_stage 缓存 (列表筛选按物理态精确匹配).
|
||||||
|
* <p>注意: IN_PROGRESS(执行中) 是时间窗口态, 由 MeetingStageScheduler 在 start_time 到点直接写 current_stage,
|
||||||
|
* 不在此处由事实推导 (本函数只在已执行后的材料动作里被调, 那时早已越过该窗口).
|
||||||
*/
|
*/
|
||||||
public String derivePhysicalStage(BizMeeting m)
|
public String derivePhysicalStage(BizMeeting m)
|
||||||
{
|
{
|
||||||
@@ -85,7 +89,7 @@ public class StageDeriver
|
|||||||
if (t(m.getIsSettled())) return "已结算";
|
if (t(m.getIsSettled())) return "已结算";
|
||||||
|
|
||||||
int chosen = chooseState(role, laborState(m), serviceState(m));
|
int chosen = chooseState(role, laborState(m), serviceState(m));
|
||||||
return render(role, chosen, t(m.getIsExecuted()));
|
return render(role, chosen, executionPhase(m));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按角色优先级选代表轨 (两轨中优先级更高的那轨). */
|
/** 按角色优先级选代表轨 (两轨中优先级更高的那轨). */
|
||||||
@@ -130,15 +134,16 @@ public class StageDeriver
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 单轨措辞 (代表轨状态 + 角色 → 展示名). */
|
/** 单轨措辞 (代表轨状态 + 角色 → 展示名). */
|
||||||
private static String render(String role, int s, boolean executed)
|
private static String render(String role, int s, int phase)
|
||||||
{
|
{
|
||||||
switch (s)
|
switch (s)
|
||||||
{
|
{
|
||||||
case 0: // R 退回
|
case 0: // R 退回
|
||||||
return "executor".equals(role) ? "已退回" : "待整改";
|
return "executor".equals(role) ? "已退回" : "待整改";
|
||||||
case 1: // N 未提交
|
case 1: // N 未提交 (时间驱动三态: 未执行 → 执行中 → 已执行, 所有角色统一)
|
||||||
if (!executed) return "未执行";
|
if (phase == 0) return "未执行";
|
||||||
return "executor".equals(role) ? "执行中" : "已执行未传材料";
|
if (phase == 1) return "执行中";
|
||||||
|
return "已执行";
|
||||||
case 2: // C0 合规审中
|
case 2: // C0 合规审中
|
||||||
if ("sponsor".equals(role)) return "已执行未传材料"; // 只读
|
if ("sponsor".equals(role)) return "已执行未传材料"; // 只读
|
||||||
return "待审核"; // executor / manager / admin
|
return "待审核"; // executor / manager / admin
|
||||||
@@ -149,4 +154,19 @@ public class StageDeriver
|
|||||||
return "待结算";
|
return "待结算";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行进度三态: 0=未执行 (now < start_time), 1=执行中 (start_time ≤ now < end_time),
|
||||||
|
* 2=已执行 (now ≥ end_time 或 is_executed=1). 仅用于材料未提交 (N) 的展示措辞.
|
||||||
|
* <p>is_executed=1 是 scheduler 在 end_time 到点落库的「已执行」事实, 优先采信;
|
||||||
|
* 未落库前用 start_time/end_time 现场判断.
|
||||||
|
*/
|
||||||
|
private static int executionPhase(BizMeeting m)
|
||||||
|
{
|
||||||
|
if (t(m.getIsExecuted())) return 2;
|
||||||
|
Date now = new Date();
|
||||||
|
if (m.getEndTime() != null && !m.getEndTime().after(now)) return 2;
|
||||||
|
if (m.getStartTime() != null && !m.getStartTime().after(now)) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-3
@@ -108,10 +108,23 @@ public class BizAdminUserServiceImpl implements IBizAdminUserService {
|
|||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** admin / manager: 纯 sys_user */
|
/** admin: 纯 sys_user; manager: sys_user + biz_person (unit_type='manager', 让 biz_project.create_user_name 反查得到姓名) */
|
||||||
private SysUser insertPlain(AdminUserCreateBody b, String role, String encPwd) {
|
private SysUser insertPlain(AdminUserCreateBody b, String role, String encPwd) {
|
||||||
SysUser u = baseUser(b, role, encPwd);
|
SysUser u = baseUser(b, role, encPwd);
|
||||||
sysUserService.insertUser(u);
|
sysUserService.insertUser(u);
|
||||||
|
if ("manager".equals(role)) {
|
||||||
|
BizPerson p = new BizPerson();
|
||||||
|
SnowflakeId.injectIfEmpty(p, "personId");
|
||||||
|
p.setName(u.getNickName() == null || u.getNickName().isEmpty() ? "用户" : u.getNickName());
|
||||||
|
p.setPhone(b.getPhonenumber() == null ? "" : b.getPhonenumber());
|
||||||
|
p.setDepartment("合规部");
|
||||||
|
p.setPosition("合规员");
|
||||||
|
p.setUnitType("manager");
|
||||||
|
p.setUserId(u.getUserId());
|
||||||
|
p.setCreateBy(SecurityUtils.getUsername());
|
||||||
|
p.setUpdateBy(SecurityUtils.getUsername());
|
||||||
|
bizPersonMapper.insert(p);
|
||||||
|
}
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +183,7 @@ public class BizAdminUserServiceImpl implements IBizAdminUserService {
|
|||||||
|
|
||||||
BizPerson self = new BizPerson();
|
BizPerson self = new BizPerson();
|
||||||
SnowflakeId.injectIfEmpty(self, "personId");
|
SnowflakeId.injectIfEmpty(self, "personId");
|
||||||
self.setName(contactName);
|
self.setName(u.getNickName());
|
||||||
self.setPhone(contactPhone);
|
self.setPhone(contactPhone);
|
||||||
self.setOrgId(org.getOrgId());
|
self.setOrgId(org.getOrgId());
|
||||||
self.setDepartment("管理部");
|
self.setDepartment("管理部");
|
||||||
@@ -195,13 +208,18 @@ public class BizAdminUserServiceImpl implements IBizAdminUserService {
|
|||||||
if (!role.equals(org.getOrgType())) {
|
if (!role.equals(org.getOrgType())) {
|
||||||
throw new ServiceException("所选单位类型与角色不匹配");
|
throw new ServiceException("所选单位类型与角色不匹配");
|
||||||
}
|
}
|
||||||
|
// 主账号 user_id: 按 biz_person.org_id + sys_user.account_type='MAIN' 反查, 不取 biz_org.user_id (可能脏/过期)
|
||||||
|
Long mainUserId = bizPersonMapper.selectMainUserIdByOrgId(b.getOrgId());
|
||||||
|
if (mainUserId == null) {
|
||||||
|
throw new ServiceException("该单位暂无主账号,请先创建主账号");
|
||||||
|
}
|
||||||
if (b.getNickName() == null || b.getNickName().isEmpty()) {
|
if (b.getNickName() == null || b.getNickName().isEmpty()) {
|
||||||
throw new ServiceException("姓名不能为空");
|
throw new ServiceException("姓名不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
SysUser u = baseUser(b, role, encPwd);
|
SysUser u = baseUser(b, role, encPwd);
|
||||||
u.setAccountType("SUB");
|
u.setAccountType("SUB");
|
||||||
u.setParentUserId(org.getUserId()); // 主账号 user_id, 可为 null (单位暂无主账号时留空待分配)
|
u.setParentUserId(mainUserId); // 主账号 user_id (按 account_type='MAIN' 反查)
|
||||||
sysUserService.insertUser(u);
|
sysUserService.insertUser(u);
|
||||||
|
|
||||||
BizPerson p = new BizPerson();
|
BizPerson p = new BizPerson();
|
||||||
|
|||||||
+29
-3
@@ -121,14 +121,21 @@ public class BizExpertServiceImpl implements IBizExpertService
|
|||||||
@Override
|
@Override
|
||||||
public int updateByPrimaryKey(BizExpert entity)
|
public int updateByPrimaryKey(BizExpert entity)
|
||||||
{
|
{
|
||||||
|
BizExpert existed = null;
|
||||||
String oldAuditStatus = null;
|
String oldAuditStatus = null;
|
||||||
if (entity.getExpertId() != null) {
|
if (entity.getExpertId() != null) {
|
||||||
BizExpert existed = bizExpertMapper.selectByPrimaryKey(entity.getExpertId());
|
existed = bizExpertMapper.selectByPrimaryKey(entity.getExpertId());
|
||||||
if (existed != null) {
|
if (existed != null) {
|
||||||
oldAuditStatus = existed.getAuditStatus();
|
oldAuditStatus = existed.getAuditStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int n = bizExpertMapper.updateByPrimaryKey(entity);
|
int n = bizExpertMapper.updateByPrimaryKey(entity);
|
||||||
|
// 姓名变更 → 同步 sys_user.nick_name (单一可信源 nick_name, biz_expert.name 作镜像)
|
||||||
|
if (n > 0 && entity.getName() != null && !entity.getName().isEmpty()) {
|
||||||
|
Long uid = entity.getUserId() != null ? entity.getUserId()
|
||||||
|
: (existed != null ? existed.getUserId() : null);
|
||||||
|
syncNickName(uid, entity.getName());
|
||||||
|
}
|
||||||
if (n > 0 && entity.getAuditStatus() != null
|
if (n > 0 && entity.getAuditStatus() != null
|
||||||
&& !entity.getAuditStatus().equals(oldAuditStatus)) {
|
&& !entity.getAuditStatus().equals(oldAuditStatus)) {
|
||||||
// 重新读一次拿 userId (entity 可能只传了 expertId+auditStatus)
|
// 重新读一次拿 userId (entity 可能只传了 expertId+auditStatus)
|
||||||
@@ -174,12 +181,31 @@ public class BizExpertServiceImpl implements IBizExpertService
|
|||||||
@Override
|
@Override
|
||||||
public int updateProfileByUserId(BizExpert entity)
|
public int updateProfileByUserId(BizExpert entity)
|
||||||
{
|
{
|
||||||
|
int n;
|
||||||
BizExpert existed = bizExpertMapper.selectByUserId(entity.getUserId());
|
BizExpert existed = bizExpertMapper.selectByUserId(entity.getUserId());
|
||||||
if (existed == null) {
|
if (existed == null) {
|
||||||
return bizExpertMapper.insertWithUserId(entity);
|
n = bizExpertMapper.insertWithUserId(entity);
|
||||||
|
} else {
|
||||||
|
n = bizExpertMapper.updateByUserId(entity);
|
||||||
}
|
}
|
||||||
return bizExpertMapper.updateByUserId(entity);
|
// 姓名变更 → 同步 sys_user.nick_name
|
||||||
|
if (n > 0 && entity.getUserId() != null
|
||||||
|
&& entity.getName() != null && !entity.getName().isEmpty()) {
|
||||||
|
syncNickName(entity.getUserId(), entity.getName());
|
||||||
}
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步 sys_user.nick_name (单一可信源), userId/name 为空时静默跳过 */
|
||||||
|
private void syncNickName(Long userId, String name) {
|
||||||
|
if (userId == null || name == null || name.isEmpty()) return;
|
||||||
|
SysUser u = new SysUser();
|
||||||
|
u.setUserId(userId);
|
||||||
|
u.setNickName(name);
|
||||||
|
u.setUpdateBy(SecurityUtils.getUsername());
|
||||||
|
sysUserService.updateUser(u);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int deleteByPrimaryKey(Long expertId)
|
public int deleteByPrimaryKey(Long expertId)
|
||||||
{ return bizExpertMapper.deleteByPrimaryKey(expertId); }
|
{ return bizExpertMapper.deleteByPrimaryKey(expertId); }
|
||||||
|
|||||||
+141
-7
@@ -25,11 +25,13 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ruoyi.business.domain.BizExpert;
|
||||||
import com.ruoyi.business.domain.BizMeeting;
|
import com.ruoyi.business.domain.BizMeeting;
|
||||||
import com.ruoyi.business.domain.BizMeetingAttendee;
|
import com.ruoyi.business.domain.BizMeetingAttendee;
|
||||||
import com.ruoyi.business.domain.BizProject;
|
import com.ruoyi.business.domain.BizProject;
|
||||||
import com.ruoyi.business.domain.dto.ImportResult;
|
import com.ruoyi.business.domain.dto.ImportResult;
|
||||||
import com.ruoyi.business.domain.vo.BizMeetingAttendeeImportVo;
|
import com.ruoyi.business.domain.vo.BizMeetingAttendeeImportVo;
|
||||||
|
import com.ruoyi.business.mapper.BizExpertMapper;
|
||||||
import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
|
import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
|
||||||
import com.ruoyi.business.mapper.BizMeetingMapper;
|
import com.ruoyi.business.mapper.BizMeetingMapper;
|
||||||
import com.ruoyi.business.mapper.BizProjectMapper;
|
import com.ruoyi.business.mapper.BizProjectMapper;
|
||||||
@@ -61,6 +63,8 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
|||||||
private SysUserMapper sysUserMapper;
|
private SysUserMapper sysUserMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private ISysUserService sysUserService;
|
private ISysUserService sysUserService;
|
||||||
|
@Autowired
|
||||||
|
private BizExpertMapper bizExpertMapper;
|
||||||
|
|
||||||
/** Jackson (Spring Boot 自带), 解析 biz_project.role_labor JSON 数组 [{role, customName, amount}] */
|
/** Jackson (Spring Boot 自带), 解析 biz_project.role_labor JSON 数组 [{role, customName, amount}] */
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
@@ -116,6 +120,10 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
|||||||
}
|
}
|
||||||
phone = phone.trim();
|
phone = phone.trim();
|
||||||
|
|
||||||
|
// 校验实发金额不超所选角色劳务金额合计 (与手动新增弹窗 MeetingDetail.validateFee 一致;
|
||||||
|
// 无角色/无金额/项目无 role_labor 时跳过)
|
||||||
|
validateFeeAgainstRole(loadRoleLabor(body.getMeetingId()), body.getLaborForm(), body.getFee());
|
||||||
|
|
||||||
// 1. 按 phone 查 sys_user (单条 IN 查, selectByPhoneList 接受 List<String>)
|
// 1. 按 phone 查 sys_user (单条 IN 查, selectByPhoneList 接受 List<String>)
|
||||||
List<SysUser> hits = sysUserMapper.selectByPhoneList(Collections.singletonList(phone));
|
List<SysUser> hits = sysUserMapper.selectByPhoneList(Collections.singletonList(phone));
|
||||||
Long userId;
|
Long userId;
|
||||||
@@ -152,6 +160,9 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
|||||||
throw new ServiceException("该手机号参会人已在会议中, 无需重复添加");
|
throw new ServiceException("该手机号参会人已在会议中, 无需重复添加");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3.5 确保专家档案存在 (不存在则新建, 存在则补齐空字段), 让参会人在专家库可见
|
||||||
|
ensureExpertProfile(body, userId);
|
||||||
|
|
||||||
// 4. 写完整档案行 (attendee.id 用雪花 ID, 不走 DB 自增)
|
// 4. 写完整档案行 (attendee.id 用雪花 ID, 不走 DB 自增)
|
||||||
body.setUserId(userId);
|
body.setUserId(userId);
|
||||||
body.setCreateBy(SecurityUtils.getUsername());
|
body.setCreateBy(SecurityUtils.getUsername());
|
||||||
@@ -162,8 +173,71 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
|||||||
return newId;
|
return newId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保参会人对应的 biz_expert 记录存在且字段尽量完整:
|
||||||
|
* - 不存在 → 新建 (status='Y' 正常, auditStatus='2' 通过, 与 importExpert 约定一致)
|
||||||
|
* - 存在但部分字段为空 → 只用参会人档案里的非空值补齐空字段, 不覆盖已有值
|
||||||
|
*/
|
||||||
|
private void ensureExpertProfile(BizMeetingAttendee body, Long userId) {
|
||||||
|
if (userId == null) return;
|
||||||
|
BizExpert existed = bizExpertMapper.selectByUserId(userId);
|
||||||
|
if (existed == null) {
|
||||||
|
BizExpert e = new BizExpert();
|
||||||
|
e.setExpertId(IdGenerator.generateId());
|
||||||
|
e.setUserId(userId);
|
||||||
|
e.setName(isBlank(body.getName()) ? body.getPhone() : body.getName());
|
||||||
|
e.setPhone(body.getPhone());
|
||||||
|
e.setWorkUnit(body.getWorkUnit());
|
||||||
|
e.setDepartment(body.getDepartment());
|
||||||
|
e.setTitle(body.getTitle());
|
||||||
|
e.setIdCard(body.getIdCard());
|
||||||
|
e.setBankCard(body.getBankCard());
|
||||||
|
e.setBankName(body.getBankName());
|
||||||
|
e.setBankBranch(body.getBankBranch());
|
||||||
|
e.setBankRegion(body.getBankRegion());
|
||||||
|
e.setBankAddress(body.getBankAddress());
|
||||||
|
e.setIdCardAttachments(body.getIdCardAttachments());
|
||||||
|
e.setStatus("Y");
|
||||||
|
e.setAuditStatus("2");
|
||||||
|
e.setAuditBy(SecurityUtils.getUsername());
|
||||||
|
e.setAuditTime(new Date());
|
||||||
|
e.setCreateBy(SecurityUtils.getUsername());
|
||||||
|
e.setCreateTime(new Date());
|
||||||
|
bizExpertMapper.insert(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 补充: 只填 biz_expert 里为空的字段, 已有值不覆盖
|
||||||
|
BizExpert u = new BizExpert();
|
||||||
|
u.setUserId(userId);
|
||||||
|
boolean changed = false;
|
||||||
|
if (isBlank(existed.getName()) && !isBlank(body.getName())) { u.setName(body.getName()); changed = true; }
|
||||||
|
if (isBlank(existed.getPhone()) && !isBlank(body.getPhone())) { u.setPhone(body.getPhone()); changed = true; }
|
||||||
|
if (isBlank(existed.getWorkUnit()) && !isBlank(body.getWorkUnit())) { u.setWorkUnit(body.getWorkUnit()); changed = true; }
|
||||||
|
if (isBlank(existed.getDepartment()) && !isBlank(body.getDepartment())) { u.setDepartment(body.getDepartment()); changed = true; }
|
||||||
|
if (isBlank(existed.getTitle()) && !isBlank(body.getTitle())) { u.setTitle(body.getTitle()); changed = true; }
|
||||||
|
if (isBlank(existed.getIdCard()) && !isBlank(body.getIdCard())) { u.setIdCard(body.getIdCard()); changed = true; }
|
||||||
|
if (isBlank(existed.getBankCard()) && !isBlank(body.getBankCard())) { u.setBankCard(body.getBankCard()); changed = true; }
|
||||||
|
if (isBlank(existed.getBankName()) && !isBlank(body.getBankName())) { u.setBankName(body.getBankName()); changed = true; }
|
||||||
|
if (isBlank(existed.getBankBranch()) && !isBlank(body.getBankBranch())) { u.setBankBranch(body.getBankBranch()); changed = true; }
|
||||||
|
if (isBlank(existed.getBankRegion()) && !isBlank(body.getBankRegion())) { u.setBankRegion(body.getBankRegion()); changed = true; }
|
||||||
|
if (isBlank(existed.getBankAddress()) && !isBlank(body.getBankAddress())) { u.setBankAddress(body.getBankAddress()); changed = true; }
|
||||||
|
if (isBlank(existed.getIdCardAttachments()) && !isBlank(body.getIdCardAttachments())) { u.setIdCardAttachments(body.getIdCardAttachments()); changed = true; }
|
||||||
|
if (changed) {
|
||||||
|
bizExpertMapper.updateByUserId(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBlank(String s) { return s == null || s.trim().isEmpty(); }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int updateProfile(BizMeetingAttendee entity) {
|
public int updateProfile(BizMeetingAttendee entity) {
|
||||||
|
Long meetingId = entity.getMeetingId();
|
||||||
|
if (meetingId == null) {
|
||||||
|
BizMeetingAttendee old = mapper.selectById(entity.getId());
|
||||||
|
if (old != null) meetingId = old.getMeetingId();
|
||||||
|
}
|
||||||
|
// 校验实发金额不超所选角色劳务金额合计 (与手动新增弹窗 MeetingDetail.validateFee 一致)
|
||||||
|
validateFeeAgainstRole(loadRoleLabor(meetingId), entity.getLaborForm(), entity.getFee());
|
||||||
return mapper.updateProfile(entity);
|
return mapper.updateProfile(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,18 +378,36 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在项目角色劳务 JSON 数组 [{role, customName, amount}] 里按角色名匹配劳务金额.
|
* 解析某会议的 project.role_labor JSON 数组, 失败/无数据返回 null.
|
||||||
|
* 供"实发金额 ≤ 角色设置金额"校验复用 (单条 add/edit 各自加载).
|
||||||
|
*/
|
||||||
|
private JsonNode loadRoleLabor(Long meetingId) {
|
||||||
|
if (meetingId == null) return null;
|
||||||
|
try {
|
||||||
|
BizMeeting meeting = meetingMapper.selectByPrimaryKey(meetingId);
|
||||||
|
if (meeting == null || meeting.getProjectId() == null) return null;
|
||||||
|
BizProject project = projectMapper.selectByPrimaryKey(meeting.getProjectId());
|
||||||
|
if (project == null || project.getRoleLabor() == null || project.getRoleLabor().trim().isEmpty()) return null;
|
||||||
|
return objectMapper.readTree(project.getRoleLabor());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[attendee] 解析项目角色劳务失败 meetingId={}", meetingId, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在项目角色劳务 JSON 数组 [{role, customName, amount}] 里按单个角色名匹配劳务金额.
|
||||||
* 匹配规则与前端 ProjectRoleSelect 一致: role === '其他' 时用 customName 作 label, 否则用 role.
|
* 匹配规则与前端 ProjectRoleSelect 一致: role === '其他' 时用 customName 作 label, 否则用 role.
|
||||||
*
|
*
|
||||||
* @param nodes 已解析的 role_labor JSON (可为 null/非数组)
|
* @param nodes 已解析的 role_labor JSON (可为 null/非数组)
|
||||||
* @param laborForm 参会人填的角色名 (可为 null/空)
|
* @param roleLabel 单个角色名 (可为 null/空)
|
||||||
* @return 匹配到的 amount (BigDecimal); 没匹配到或 amount 非法 → null
|
* @return 匹配到的 amount (BigDecimal); 没匹配到或 amount 非法 → null
|
||||||
*/
|
*/
|
||||||
private BigDecimal findRoleAmount(JsonNode nodes, String laborForm) {
|
private BigDecimal matchRoleAmount(JsonNode nodes, String roleLabel) {
|
||||||
if (nodes == null || !nodes.isArray() || laborForm == null || laborForm.trim().isEmpty()) {
|
if (nodes == null || !nodes.isArray() || roleLabel == null || roleLabel.trim().isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String target = laborForm.trim();
|
String target = roleLabel.trim();
|
||||||
for (JsonNode n : nodes) {
|
for (JsonNode n : nodes) {
|
||||||
if (n == null || n.isNull()) continue;
|
if (n == null || n.isNull()) continue;
|
||||||
String role = n.path("role").asText("");
|
String role = n.path("role").asText("");
|
||||||
@@ -334,6 +426,47 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在项目角色劳务 JSON 里按角色名匹配劳务金额 (导入自动带出金额用, 单个角色).
|
||||||
|
* 保留原 findRoleAmount 语义: 单个 laborForm 精确匹配.
|
||||||
|
*/
|
||||||
|
private BigDecimal findRoleAmount(JsonNode nodes, String laborForm) {
|
||||||
|
return matchRoleAmount(nodes, laborForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参会人角色 (laborForm, 可能逗号分隔多选) 对应的角色劳务金额合计.
|
||||||
|
* 与前端 MeetingDetail.roleAmountSum 一致: 按逗号拆分逐个匹配求和.
|
||||||
|
*
|
||||||
|
* @return 金额合计; nodes 为空/非数组 → null (无 role_labor 数据, 无法比较)
|
||||||
|
*/
|
||||||
|
private BigDecimal sumRoleAmount(JsonNode nodes, String laborForm) {
|
||||||
|
if (nodes == null || !nodes.isArray() || laborForm == null || laborForm.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BigDecimal sum = BigDecimal.ZERO;
|
||||||
|
for (String item : laborForm.split(",")) {
|
||||||
|
BigDecimal a = matchRoleAmount(nodes, item.trim());
|
||||||
|
if (a != null) sum = sum.add(a);
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验实发金额(fee)不超所选角色劳务金额合计 (与前端 MeetingDetail.validateFee 一致).
|
||||||
|
* laborForm 为空 / fee 为空 / 项目无 role_labor 数据时跳过 (无"设置金额"可比较).
|
||||||
|
*/
|
||||||
|
private void validateFeeAgainstRole(JsonNode roleLaborNodes, String laborForm, BigDecimal fee) {
|
||||||
|
if (laborForm == null || laborForm.trim().isEmpty()) return;
|
||||||
|
if (fee == null) return;
|
||||||
|
BigDecimal sum = sumRoleAmount(roleLaborNodes, laborForm);
|
||||||
|
if (sum == null) return;
|
||||||
|
if (fee.compareTo(sum) > 0) {
|
||||||
|
throw new ServiceException("实发金额不能超过角色金额 "
|
||||||
|
+ sum.setScale(2, RoundingMode.HALF_UP).toPlainString() + " 元");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量导入参会人 (Excel → biz_meeting_attendee).
|
* 批量导入参会人 (Excel → biz_meeting_attendee).
|
||||||
*
|
*
|
||||||
@@ -467,11 +600,12 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
|
|||||||
}
|
}
|
||||||
String meetingName = m != null ? m.getMeetingName() : null;
|
String meetingName = m != null ? m.getMeetingName() : null;
|
||||||
Date startTime = m != null ? m.getStartTime() : null;
|
Date startTime = m != null ? m.getStartTime() : null;
|
||||||
|
Date endTime = m != null ? m.getEndTime() : null;
|
||||||
|
|
||||||
// 1. 短信 (电子签模板: name=参会人姓名, date=会议日期, link=签署链接)
|
// 1. 短信 (邀请签署模板: name=参会人姓名, time=会议时间, attendeeId=参会人id)
|
||||||
String phone = a.getPhone();
|
String phone = a.getPhone();
|
||||||
if (phone != null && !phone.trim().isEmpty()) {
|
if (phone != null && !phone.trim().isEmpty()) {
|
||||||
aliyunSmsSender.sendEsign(phone.trim(), a.getName(), startTime, aliyunSmsSender.esignLink(id));
|
aliyunSmsSender.sendEsign(phone.trim(), a.getName(), startTime, endTime, id);
|
||||||
}
|
}
|
||||||
// 2. 站内信 (劳务协议待签署 + 签署链接, 与短信同一链接)
|
// 2. 站内信 (劳务协议待签署 + 签署链接, 与短信同一链接)
|
||||||
bizNotifyService.esignPushed(a.getUserId(), id, mid, meetingName, aliyunSmsSender.esignLink(id));
|
bizNotifyService.esignPushed(a.getUserId(), id, mid, meetingName, aliyunSmsSender.esignLink(id));
|
||||||
|
|||||||
+141
-22
@@ -5,12 +5,14 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.net.URI;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -202,6 +204,45 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMaterialSetChanged(Long meetingId, List<BizMeetingMaterial> list) {
|
||||||
|
// 只关心"影响会务费"的服务类发票 (M_*): 劳务材料/凭证/现场照片等不影响会务费, 直接忽略.
|
||||||
|
// 会务费口径 (见 FeeCalcScheduler.computeMeetingFee): 有总发票 M_INVOICE 就用它, 否则各 M_* 子发票之和.
|
||||||
|
List<BizMeetingMaterial> oldList = bizMeetingMaterialMapper.selectByMeetingId(meetingId);
|
||||||
|
Map<String, String> oldUrl = new HashMap<>();
|
||||||
|
if (oldList != null) {
|
||||||
|
for (BizMeetingMaterial o : oldList) {
|
||||||
|
if (isFeeRelevant(o.getSubType())) oldUrl.put(o.getSubType(), o.getOssUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, String> newUrl = new HashMap<>();
|
||||||
|
if (list != null) {
|
||||||
|
for (BizMeetingMaterial m : list) {
|
||||||
|
if (isFeeRelevant(m.getSubType())) newUrl.put(m.getSubType(), m.getOssUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean hasInvoice = oldUrl.containsKey("M_INVOICE") || newUrl.containsKey("M_INVOICE");
|
||||||
|
if (hasInvoice) {
|
||||||
|
// 有总发票: 会务费只看总发票, 其余子发票被覆盖 → 仅总发票变化才算变
|
||||||
|
return !Objects.equals(oldUrl.get("M_INVOICE"), newUrl.get("M_INVOICE"));
|
||||||
|
}
|
||||||
|
// 无总发票: 任一 M_* 子发票 (排除 M_INVOICE / M_SETTLEMENT 两个汇总单据) 增删或换 URL 都算变
|
||||||
|
Set<String> subs = new HashSet<>();
|
||||||
|
subs.addAll(oldUrl.keySet());
|
||||||
|
subs.addAll(newUrl.keySet());
|
||||||
|
subs.remove("M_INVOICE");
|
||||||
|
subs.remove("M_SETTLEMENT");
|
||||||
|
for (String sub : subs) {
|
||||||
|
if (!Objects.equals(oldUrl.get(sub), newUrl.get(sub))) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否影响会务费: 仅服务类发票 M_* (劳务 L_*、凭证 LV/SV、结算单等都不算) */
|
||||||
|
private static boolean isFeeRelevant(String subType) {
|
||||||
|
return subType != null && subType.startsWith("M_");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int updateAmount(Long materialId, BigDecimal amount) {
|
public int updateAmount(Long materialId, BigDecimal amount) {
|
||||||
if (materialId == null || amount == null) return 0;
|
if (materialId == null || amount == null) return 0;
|
||||||
@@ -217,7 +258,7 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
|||||||
/**
|
/**
|
||||||
* 扫码拍照回传: ry-h5 手机端拍照直传 OSS 后回传 URL, 按 (meetingId, subType) upsert 单行.
|
* 扫码拍照回传: ry-h5 手机端拍照直传 OSS 后回传 URL, 按 (meetingId, subType) upsert 单行.
|
||||||
* 公开端点 (匿名) — 白名单 subType + 会议存在校验兜底.
|
* 公开端点 (匿名) — 白名单 subType + 会议存在校验兜底.
|
||||||
* 照片类 NON_OCR, 不触发 OCR, 不影响会议费用, 故不 markFeeCalcPending.
|
* 照片类 NON_OCR, 不触发 OCR, 不影响会议费用, 故不触发费用重算.
|
||||||
* extraOssUrl: 签到表(L_SIGN_IN)拍照时额外生成的高斯模糊版 URL, sponsor 只看这个; 其他 subType 传空.
|
* extraOssUrl: 签到表(L_SIGN_IN)拍照时额外生成的高斯模糊版 URL, sponsor 只看这个; 其他 subType 传空.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -309,9 +350,10 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
|||||||
{
|
{
|
||||||
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
||||||
if (meeting == null) throw new ServiceException("会议不存在");
|
if (meeting == null) throw new ServiceException("会议不存在");
|
||||||
String prefix = "download/huiwu/" + meetingId + "/";
|
String name = singleZipName(meeting, "会务");
|
||||||
|
String prefix = "download/huiwu/" + name + "/";
|
||||||
ossZipService.clearPrefix(prefix);
|
ossZipService.clearPrefix(prefix);
|
||||||
int copied = stageServiceMaterials(meetingId, prefix, projectFolderName(meeting));
|
int copied = stageServiceMaterials(meetingId, prefix, name);
|
||||||
if (copied == 0) throw new ServiceException("该会议暂无可下载的会务材料");
|
if (copied == 0) throw new ServiceException("该会议暂无可下载的会务材料");
|
||||||
return ossZipService.zipDownload(prefix);
|
return ossZipService.zipDownload(prefix);
|
||||||
}
|
}
|
||||||
@@ -327,9 +369,11 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
|||||||
{
|
{
|
||||||
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
||||||
if (meeting == null) throw new ServiceException("会议不存在");
|
if (meeting == null) throw new ServiceException("会议不存在");
|
||||||
String prefix = "download/labor/" + meetingId + "/";
|
String name = singleZipName(meeting, "劳务");
|
||||||
|
String prefix = "download/labor/" + name + "/";
|
||||||
ossZipService.clearPrefix(prefix);
|
ossZipService.clearPrefix(prefix);
|
||||||
int copied = stageLaborMaterials(meetingId, prefix, projectFolderName(meeting));
|
int copied = stageLaborMaterials(meetingId, prefix, name);
|
||||||
|
copied += stageSchedulePoster(meeting, prefix, name);
|
||||||
if (copied == 0) throw new ServiceException("该会议暂无可下载的劳务材料");
|
if (copied == 0) throw new ServiceException("该会议暂无可下载的劳务材料");
|
||||||
return ossZipService.zipDownload(prefix);
|
return ossZipService.zipDownload(prefix);
|
||||||
}
|
}
|
||||||
@@ -343,7 +387,7 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
|||||||
public String buildBatchServiceZipUrl(List<Long> meetingIds)
|
public String buildBatchServiceZipUrl(List<Long> meetingIds)
|
||||||
{
|
{
|
||||||
List<Long> ids = normalizeIds(meetingIds);
|
List<Long> ids = normalizeIds(meetingIds);
|
||||||
String prefix = "download/huiwu/batch/" + System.currentTimeMillis() + "/";
|
String prefix = "download/huiwu/batch/" + batchZipName("会务") + "/";
|
||||||
ossZipService.clearPrefix(prefix);
|
ossZipService.clearPrefix(prefix);
|
||||||
Set<String> usedFolders = new HashSet<>();
|
Set<String> usedFolders = new HashSet<>();
|
||||||
int copied = 0;
|
int copied = 0;
|
||||||
@@ -362,7 +406,7 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
|||||||
public String buildBatchLaborZipUrl(List<Long> meetingIds)
|
public String buildBatchLaborZipUrl(List<Long> meetingIds)
|
||||||
{
|
{
|
||||||
List<Long> ids = normalizeIds(meetingIds);
|
List<Long> ids = normalizeIds(meetingIds);
|
||||||
String prefix = "download/labor/batch/" + System.currentTimeMillis() + "/";
|
String prefix = "download/labor/batch/" + batchZipName("劳务") + "/";
|
||||||
ossZipService.clearPrefix(prefix);
|
ossZipService.clearPrefix(prefix);
|
||||||
Set<String> usedFolders = new HashSet<>();
|
Set<String> usedFolders = new HashSet<>();
|
||||||
int copied = 0;
|
int copied = 0;
|
||||||
@@ -370,12 +414,40 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
|||||||
{
|
{
|
||||||
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(id);
|
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(id);
|
||||||
if (meeting == null) continue;
|
if (meeting == null) continue;
|
||||||
copied += stageLaborMaterials(id, prefix, uniqueFolder(usedFolders, batchMeetingFolderName(meeting)));
|
String folder = uniqueFolder(usedFolders, batchMeetingFolderName(meeting));
|
||||||
|
copied += stageLaborMaterials(id, prefix, folder);
|
||||||
|
copied += stageSchedulePoster(meeting, prefix, folder);
|
||||||
}
|
}
|
||||||
if (copied == 0) throw new ServiceException("所选会议暂无可下载的劳务材料");
|
if (copied == 0) throw new ServiceException("所选会议暂无可下载的劳务材料");
|
||||||
return ossZipService.zipDownload(prefix);
|
return ossZipService.zipDownload(prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// 下载文件名 (FC 固定命名 output_1-xxx.zip, 需后端代理改名下发)
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String serviceZipFilename(Long meetingId)
|
||||||
|
{
|
||||||
|
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
||||||
|
if (meeting == null) throw new ServiceException("会议不存在");
|
||||||
|
return singleZipName(meeting, "会务") + ".zip";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String laborZipFilename(Long meetingId)
|
||||||
|
{
|
||||||
|
BizMeeting meeting = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
||||||
|
if (meeting == null) throw new ServiceException("会议不存在");
|
||||||
|
return singleZipName(meeting, "劳务") + ".zip";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String batchServiceZipFilename() { return batchZipName("会务") + ".zip"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String batchLaborZipFilename() { return batchZipName("劳务") + ".zip"; }
|
||||||
|
|
||||||
/** 收集该会议会务材料并 copy 到 staging 前缀 prefix + folderName 下, 返回 copy 的文件数 (0 = 无会务材料) */
|
/** 收集该会议会务材料并 copy 到 staging 前缀 prefix + folderName 下, 返回 copy 的文件数 (0 = 无会务材料) */
|
||||||
private int stageServiceMaterials(Long meetingId, String prefix, String folderName)
|
private int stageServiceMaterials(Long meetingId, String prefix, String folderName)
|
||||||
{
|
{
|
||||||
@@ -432,16 +504,38 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
|||||||
return copied;
|
return copied;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单会议 zip 顶层目录名: 项目名 (为空回退项目编号, 再回退会议ID), 统一消毒 */
|
/** 把会议日程海报 (biz_meeting.schedule_url) staging 到 zip 的"日程海报"目录, 返回写入的文件数 (0 = 无日程海报) */
|
||||||
private static String projectFolderName(BizMeeting meeting)
|
private int stageSchedulePoster(BizMeeting meeting, String prefix, String folderName)
|
||||||
{
|
{
|
||||||
String name = meeting.getProjectName();
|
if (meeting == null || meeting.getScheduleUrl() == null || meeting.getScheduleUrl().isEmpty()) return 0;
|
||||||
if (name == null || name.trim().isEmpty())
|
String srcKey = ossZipService.extractKey(meeting.getScheduleUrl());
|
||||||
{
|
if (srcKey == null || srcKey.isEmpty()) return 0;
|
||||||
name = (meeting.getProjectNo() != null && !meeting.getProjectNo().trim().isEmpty())
|
String ext = extOf(srcKey);
|
||||||
? meeting.getProjectNo() : "会议" + meeting.getMeetingId();
|
String dstKey = prefix + folderName + "/日程海报/日程海报" + ext;
|
||||||
|
ossZipService.copyObject(srcKey, dstKey);
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
return safeName(name);
|
|
||||||
|
/** 单会议下载命名 (zip 文件名 + 顶层目录): 项目编号_项目名称_第N期_{suffix} (缺项跳过, 全空回退会议ID), 统一消毒 */
|
||||||
|
private static String singleZipName(BizMeeting meeting, String suffix)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
appendPart(sb, meeting.getProjectNo());
|
||||||
|
appendPart(sb, meeting.getProjectName());
|
||||||
|
if (meeting.getPeriodNo() != null)
|
||||||
|
{
|
||||||
|
appendPart(sb, "第" + meeting.getPeriodNo() + "期");
|
||||||
|
}
|
||||||
|
String base = sb.toString();
|
||||||
|
if (base.isEmpty()) base = "会议" + meeting.getMeetingId();
|
||||||
|
return safeName(base) + "_" + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量下载命名 (zip 文件名): {suffix}_下载时间 (yyyyMMdd_HHmmss) */
|
||||||
|
private static String batchZipName(String suffix)
|
||||||
|
{
|
||||||
|
String time = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
|
||||||
|
return suffix + "_" + time;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量 zip 每个会议顶层目录名: 项目编号_会议名_第N期 (缺项跳过, 全空回退会议ID) */
|
/** 批量 zip 每个会议顶层目录名: 项目编号_会议名_第N期 (缺项跳过, 全空回退会议ID) */
|
||||||
@@ -586,24 +680,33 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
|||||||
if (files == null || files.isEmpty()) continue;
|
if (files == null || files.isEmpty()) continue;
|
||||||
List<String> names = folderNames.get(subType);
|
List<String> names = folderNames.get(subType);
|
||||||
String label = SERVICE_SUBTYPE_LABEL.getOrDefault(subType, subType);
|
String label = SERVICE_SUBTYPE_LABEL.getOrDefault(subType, subType);
|
||||||
|
BizMeetingMaterial old = existingBySubType.get(subType);
|
||||||
|
|
||||||
String ossUrl;
|
// 先算好要上传的字节 + 文件名 (单文件原样 / 多文件先打 zip), 便于做"内容未变"判断
|
||||||
String fileName;
|
String fileName;
|
||||||
|
byte[] toUpload;
|
||||||
|
boolean isZip = files.size() > 1;
|
||||||
if (files.size() == 1) {
|
if (files.size() == 1) {
|
||||||
fileName = (names != null && !names.isEmpty()) ? names.get(0) : (label + ".jpg");
|
fileName = (names != null && !names.isEmpty()) ? names.get(0) : (label + ".jpg");
|
||||||
ossUrl = ossUploader.upload(files.get(0), fileName, subDir);
|
toUpload = files.get(0);
|
||||||
} else {
|
} else {
|
||||||
// 目录内多文件 → 先打 zip 再上传 OSS
|
|
||||||
fileName = label + ".zip";
|
fileName = label + ".zip";
|
||||||
ossUrl = ossUploader.upload(zipFiles(files, names), fileName, subDir);
|
toUpload = zipFiles(files, names);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 内容未变且已计算过 (fee_status=1) → 跳过重传重 OCR, 保留旧金额, 不触发会务费重算
|
||||||
|
if (old != null && old.getFeeStatus() != null && old.getFeeStatus() == 1
|
||||||
|
&& contentEquals(old.getOssUrl(), toUpload)) {
|
||||||
|
log.info("[material] 会务材料 {} 内容未变, 跳过重传重 OCR", subType);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ossUrl = ossUploader.upload(toUpload, fileName, subDir);
|
||||||
|
|
||||||
// 会务材料都是发票类 (M_* 不在 NON_OCR 集合): 单文件可识别 (jpg/png/pdf) 或多文件打 zip → 待 OCR(0), 其余已计算(1)
|
// 会务材料都是发票类 (M_* 不在 NON_OCR 集合): 单文件可识别 (jpg/png/pdf) 或多文件打 zip → 待 OCR(0), 其余已计算(1)
|
||||||
boolean isZip = files.size() > 1;
|
|
||||||
boolean ocrNeeded = isZip || (ossUrl != null && RECOGNIZABLE.matcher(ossUrl).find());
|
boolean ocrNeeded = isZip || (ossUrl != null && RECOGNIZABLE.matcher(ossUrl).find());
|
||||||
int feeStatus = ocrNeeded ? 0 : 1;
|
int feeStatus = ocrNeeded ? 0 : 1;
|
||||||
|
|
||||||
BizMeetingMaterial old = existingBySubType.get(subType);
|
|
||||||
Long materialId;
|
Long materialId;
|
||||||
Long oldMaterialId;
|
Long oldMaterialId;
|
||||||
if (old != null) {
|
if (old != null) {
|
||||||
@@ -696,6 +799,22 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
|
|||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 下载 OSS 旧文件并与新文件字节比对是否一致. 下载失败按"不一致"处理 (走正常重传), 不阻塞打包上传. */
|
||||||
|
private boolean contentEquals(String ossUrl, byte[] newBytes) {
|
||||||
|
if (ossUrl == null || ossUrl.isEmpty() || newBytes == null) return false;
|
||||||
|
try {
|
||||||
|
java.net.URLConnection conn = URI.create(ossUrl).toURL().openConnection();
|
||||||
|
conn.setConnectTimeout(10000);
|
||||||
|
conn.setReadTimeout(30000);
|
||||||
|
try (InputStream in = conn.getInputStream()) {
|
||||||
|
return Arrays.equals(readAllBytes(in), newBytes);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[material] 下载旧材料对比失败, 按不一致处理 url={} err={}", ossUrl, e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否会被前端提交 OCR (与 MeetingDetail.saveMaterials 门控一致):
|
* 是否会被前端提交 OCR (与 MeetingDetail.saveMaterials 门控一致):
|
||||||
* 文件可识别 (jpg/jpeg/png/pdf) 且 不在非发票 subType 集合里.
|
* 文件可识别 (jpg/jpeg/png/pdf) 且 不在非发票 subType 集合里.
|
||||||
|
|||||||
+85
-3
@@ -1,14 +1,19 @@
|
|||||||
package com.ruoyi.business.service.impl;
|
package com.ruoyi.business.service.impl;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import com.ruoyi.business.domain.BizMeeting;
|
import com.ruoyi.business.domain.BizMeeting;
|
||||||
|
import com.ruoyi.business.domain.BizMeetingMaterial;
|
||||||
import com.ruoyi.business.mapper.BizMeetingMapper;
|
import com.ruoyi.business.mapper.BizMeetingMapper;
|
||||||
import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
|
import com.ruoyi.business.mapper.BizMeetingAttendeeMapper;
|
||||||
import com.ruoyi.business.mapper.BizMeetingSupervisorMapper;
|
import com.ruoyi.business.mapper.BizMeetingSupervisorMapper;
|
||||||
@@ -23,6 +28,8 @@ import com.ruoyi.common.utils.id.IdGenerator;
|
|||||||
@Service
|
@Service
|
||||||
public class BizMeetingServiceImpl implements IBizMeetingService
|
public class BizMeetingServiceImpl implements IBizMeetingService
|
||||||
{
|
{
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BizMeetingServiceImpl.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BizMeetingMapper bizMeetingMapper;
|
private BizMeetingMapper bizMeetingMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -48,6 +55,9 @@ public class BizMeetingServiceImpl implements IBizMeetingService
|
|||||||
public List<BizMeeting> selectList(BizMeeting entity)
|
public List<BizMeeting> selectList(BizMeeting entity)
|
||||||
{ return bizMeetingMapper.selectList(entity); }
|
{ return bizMeetingMapper.selectList(entity); }
|
||||||
@Override
|
@Override
|
||||||
|
public List<Map<String, Object>> selectStageStats(BizMeeting entity)
|
||||||
|
{ return bizMeetingMapper.selectStageStats(entity); }
|
||||||
|
@Override
|
||||||
public int insert(BizMeeting entity) {
|
public int insert(BizMeeting entity) {
|
||||||
// meetingId: 从 DB AUTO_INCREMENT 改为应用赋值 — 10 位数字会议ID = 开始日期(yyMMdd) + 4 位序列号(0001 起, 每开始日期 Redis 独立计数)
|
// meetingId: 从 DB AUTO_INCREMENT 改为应用赋值 — 10 位数字会议ID = 开始日期(yyMMdd) + 4 位序列号(0001 起, 每开始日期 Redis 独立计数)
|
||||||
if (entity.getMeetingId() == null) {
|
if (entity.getMeetingId() == null) {
|
||||||
@@ -145,9 +155,81 @@ public class BizMeetingServiceImpl implements IBizMeetingService
|
|||||||
{ return bizMeetingMapper.countByProjectIdExecutionUnitPeriod(projectId, executionUnitId, periodNo); }
|
{ return bizMeetingMapper.countByProjectIdExecutionUnitPeriod(projectId, executionUnitId, periodNo); }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void markFeeCalcPending(Long meetingId) {
|
public int countByProjectIdExecutionUnitPeriodExclude(Long projectId, Long executionUnitId, Long periodNo, Long excludeMeetingId)
|
||||||
if (meetingId != null) {
|
{ return bizMeetingMapper.countByProjectIdExecutionUnitPeriodExclude(projectId, executionUnitId, periodNo, excludeMeetingId); }
|
||||||
bizMeetingMapper.markFeeCalcPending(meetingId);
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void recomputeMeetingFee(Long meetingId) {
|
||||||
|
if (meetingId == null) return;
|
||||||
|
// ① 置计算中 -1, 让 FeeCalcScheduler (只扫 =0) 不并发重复算
|
||||||
|
bizMeetingMapper.updateFeeCalcStatus(meetingId, -1);
|
||||||
|
try {
|
||||||
|
List<BizMeetingMaterial> mats = materialMapper.selectByMeetingId(meetingId);
|
||||||
|
// ② 任一材料发票仍未 OCR 完 (fee_status=0) → 会务费金额未定, 回滚 0 走调度器兜底 (选项 A)
|
||||||
|
boolean ocrReady = true;
|
||||||
|
if (mats != null) {
|
||||||
|
for (BizMeetingMaterial m : mats) {
|
||||||
|
if (m.getFeeStatus() != null && m.getFeeStatus() == 0) {
|
||||||
|
ocrReady = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!ocrReady) {
|
||||||
|
bizMeetingMapper.updateFeeCalcStatus(meetingId, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ③ 汇总: labor_fee + meeting_fee → total_fee, 成功置 1 (updateFeeSummary 内含)
|
||||||
|
BigDecimal laborFee = attendeeMapper.sumFeePreTaxByMeetingId(meetingId);
|
||||||
|
if (laborFee == null) laborFee = BigDecimal.ZERO;
|
||||||
|
BigDecimal meetingFee = computeMeetingFee(mats);
|
||||||
|
BigDecimal totalFee = laborFee.add(meetingFee);
|
||||||
|
bizMeetingMapper.updateFeeSummary(meetingId, laborFee, meetingFee, totalFee);
|
||||||
|
log.info("[recomputeMeetingFee] 立即汇总完成 meetingId={} labor={} meeting={} total={}",
|
||||||
|
meetingId, laborFee, meetingFee, totalFee);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ④ 算错了 → 回滚 0, 交给 FeeCalcScheduler 下分钟兜底 (不让 -1 残留卡死)
|
||||||
|
log.warn("[recomputeMeetingFee] 立即汇总失败, 回滚 0 走调度器兜底 meetingId={} err={}", meetingId, e.getMessage(), e);
|
||||||
|
bizMeetingMapper.updateFeeCalcStatus(meetingId, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会务费口径: 有总发票 (M_INVOICE 金额>0) 则只用总发票; 否则各 SUB 子类 (M_ 开头) 金额之和,
|
||||||
|
* 排除 M_INVOICE / M_SETTLEMENT (两者是汇总单据, 非子类发票).
|
||||||
|
*/
|
||||||
|
private BigDecimal computeMeetingFee(List<BizMeetingMaterial> mats) {
|
||||||
|
if (mats == null || mats.isEmpty()) return BigDecimal.ZERO;
|
||||||
|
BigDecimal main = null;
|
||||||
|
for (BizMeetingMaterial m : mats) {
|
||||||
|
if ("M_INVOICE".equals(m.getSubType())) {
|
||||||
|
main = m.getAmount();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (main != null && main.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
return main;
|
||||||
|
}
|
||||||
|
BigDecimal sum = BigDecimal.ZERO;
|
||||||
|
for (BizMeetingMaterial m : mats) {
|
||||||
|
if (m.getSubType() == null || !m.getSubType().startsWith("M_")) continue;
|
||||||
|
if ("M_INVOICE".equals(m.getSubType()) || "M_SETTLEMENT".equals(m.getSubType())) continue;
|
||||||
|
if (m.getAmount() != null) sum = sum.add(m.getAmount());
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void recomputeLaborFee(Long meetingId) {
|
||||||
|
if (meetingId == null) return;
|
||||||
|
BigDecimal laborFee = attendeeMapper.sumFeePreTaxByMeetingId(meetingId);
|
||||||
|
if (laborFee == null) laborFee = BigDecimal.ZERO;
|
||||||
|
BizMeeting m = bizMeetingMapper.selectByPrimaryKey(meetingId);
|
||||||
|
if (m == null) return;
|
||||||
|
BigDecimal meetingFee = m.getMeetingFee() != null ? m.getMeetingFee() : BigDecimal.ZERO;
|
||||||
|
BigDecimal totalFee = laborFee.add(meetingFee);
|
||||||
|
bizMeetingMapper.updateLaborFee(meetingId, laborFee, totalFee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+16
@@ -38,6 +38,22 @@ public class BizMessageServiceImpl implements IBizMessageService
|
|||||||
return bizMessageMapper.selectMyRecent(q);
|
return bizMessageMapper.selectMyRecent(q);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BizMessage> selectMyPage(Long receiverUserId, int offset, int pageSize)
|
||||||
|
{
|
||||||
|
BizMessage q = new BizMessage();
|
||||||
|
q.setReceiverUserId(receiverUserId);
|
||||||
|
q.getParams().put("offset", offset);
|
||||||
|
q.getParams().put("limit", pageSize);
|
||||||
|
return bizMessageMapper.selectMyRecent(q);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int countMy(Long receiverUserId)
|
||||||
|
{
|
||||||
|
return bizMessageMapper.countMy(receiverUserId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int countUnread(Long receiverUserId)
|
public int countUnread(Long receiverUserId)
|
||||||
{
|
{
|
||||||
|
|||||||
+24
-1
@@ -34,12 +34,35 @@ public class BizOrgServiceImpl implements IBizOrgService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int insert(BizOrg entity) {
|
public int insert(BizOrg entity) {
|
||||||
|
// 单位名称查重: 同 orgType 下 org_name 不能重复 (trim 后精确匹配)
|
||||||
|
String orgName = entity.getOrgName() == null ? null : entity.getOrgName().trim();
|
||||||
|
if (orgName != null && !orgName.isEmpty()) {
|
||||||
|
BizOrg probe = new BizOrg();
|
||||||
|
probe.setOrgType(entity.getOrgType());
|
||||||
|
probe.setOrgName(orgName);
|
||||||
|
if (bizOrgMapper.countByOrgName(probe) > 0) {
|
||||||
|
throw new ServiceException("单位名称「" + orgName + "」已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
// 主键由 AUTO_INCREMENT 自增, 不需要 Snowflake
|
// 主键由 AUTO_INCREMENT 自增, 不需要 Snowflake
|
||||||
return bizOrgMapper.insert(entity);
|
return bizOrgMapper.insert(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int updateByPrimaryKey(BizOrg entity) { return bizOrgMapper.updateByPrimaryKey(entity); }
|
public int updateByPrimaryKey(BizOrg entity) {
|
||||||
|
// 改名时同样查重: 同 orgType 下 org_name 不能重复 (trim 精确匹配), 排除自身
|
||||||
|
String orgName = entity.getOrgName() == null ? null : entity.getOrgName().trim();
|
||||||
|
if (orgName != null && !orgName.isEmpty()) {
|
||||||
|
BizOrg probe = new BizOrg();
|
||||||
|
probe.setOrgType(entity.getOrgType());
|
||||||
|
probe.setOrgName(orgName);
|
||||||
|
probe.setOrgId(entity.getOrgId());
|
||||||
|
if (bizOrgMapper.countByOrgName(probe) > 0) {
|
||||||
|
throw new ServiceException("单位名称「" + orgName + "」已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bizOrgMapper.updateByPrimaryKey(entity);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int deleteByPrimaryKeys(Long[] orgIds) {
|
public int deleteByPrimaryKeys(Long[] orgIds) {
|
||||||
|
|||||||
+21
-1
@@ -79,6 +79,15 @@ public class BizPersonServiceImpl implements IBizPersonService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. 创建 sys_user 子账号
|
// 1. 创建 sys_user 子账号
|
||||||
|
// 归属父账号: 优先取所属机构主账号 (biz_org.user_id), 而非 getUserId() (当前登录人).
|
||||||
|
// 否则 admin/manager 在 sponsor-people 建人时, 子账号会错挂到 admin 自己名下,
|
||||||
|
// 导致 sponsor 主账号在自己的 sponsor/people 页 (parent_user_id 过滤) 看不到这些人.
|
||||||
|
Long parentUid = mainUserId;
|
||||||
|
BizOrg ownerOrg = bizOrgMapper.selectByPrimaryKey(entity.getOrgId());
|
||||||
|
if (ownerOrg != null && ownerOrg.getUserId() != null) {
|
||||||
|
parentUid = ownerOrg.getUserId();
|
||||||
|
}
|
||||||
|
|
||||||
// role_type 取 person.unitType (sponsor/executor/doctor), 而非继承创建者角色:
|
// role_type 取 person.unitType (sponsor/executor/doctor), 而非继承创建者角色:
|
||||||
// 否则 admin/manager 在 admin/sponsor-people 建人会把子账号错位成 admin/manager (后台管理员).
|
// 否则 admin/manager 在 admin/sponsor-people 建人会把子账号错位成 admin/manager (后台管理员).
|
||||||
// 仅当 unitType 缺失时才回退到主账号 role_type 兜底.
|
// 仅当 unitType 缺失时才回退到主账号 role_type 兜底.
|
||||||
@@ -94,7 +103,7 @@ public class BizPersonServiceImpl implements IBizPersonService
|
|||||||
newUser.setEmail(entity.getEmail());
|
newUser.setEmail(entity.getEmail());
|
||||||
newUser.setPassword(SecurityUtils.encryptPassword(entity.getLoginPassword()));
|
newUser.setPassword(SecurityUtils.encryptPassword(entity.getLoginPassword()));
|
||||||
newUser.setAccountType("SUB");
|
newUser.setAccountType("SUB");
|
||||||
newUser.setParentUserId(mainUserId);
|
newUser.setParentUserId(parentUid);
|
||||||
newUser.setStatus("0");
|
newUser.setStatus("0");
|
||||||
newUser.setDelFlag("0");
|
newUser.setDelFlag("0");
|
||||||
if (roleType != null) {
|
if (roleType != null) {
|
||||||
@@ -139,6 +148,17 @@ public class BizPersonServiceImpl implements IBizPersonService
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 个人资料编辑: 按 user_id 反查 personId, 再复用 updateByPrimaryKey (更新 person + 同步 nick_name/phonenumber/email) */
|
||||||
|
@Override
|
||||||
|
public int updateProfileByUserId(BizPerson entity) {
|
||||||
|
BizPerson existed = bizPersonMapper.selectByUserId(entity.getUserId());
|
||||||
|
if (existed == null) {
|
||||||
|
throw new ServiceException("未找到人员档案");
|
||||||
|
}
|
||||||
|
entity.setPersonId(existed.getPersonId());
|
||||||
|
return updateByPrimaryKey(entity);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int deleteByPrimaryKey(String personId)
|
public int deleteByPrimaryKey(String personId)
|
||||||
{
|
{
|
||||||
|
|||||||
+14
-1
@@ -1,8 +1,10 @@
|
|||||||
package com.ruoyi.business.service.impl;
|
package com.ruoyi.business.service.impl;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
import com.ruoyi.business.domain.BizProjectPlan;
|
import com.ruoyi.business.domain.BizProjectPlan;
|
||||||
import com.ruoyi.business.mapper.BizProjectPlanMapper;
|
import com.ruoyi.business.mapper.BizProjectPlanMapper;
|
||||||
import com.ruoyi.business.notify.BizNotifyService;
|
import com.ruoyi.business.notify.BizNotifyService;
|
||||||
@@ -23,7 +25,14 @@ public class BizProjectPlanServiceImpl implements IBizProjectPlanService
|
|||||||
public List<BizProjectPlan> selectList(BizProjectPlan entity)
|
public List<BizProjectPlan> selectList(BizProjectPlan entity)
|
||||||
{ return bizProjectPlanMapper.selectList(entity); }
|
{ return bizProjectPlanMapper.selectList(entity); }
|
||||||
@Override
|
@Override
|
||||||
public int insert(BizProjectPlan entity) { com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "planId"); return bizProjectPlanMapper.insert(entity); }
|
public int insert(BizProjectPlan entity)
|
||||||
|
{
|
||||||
|
com.ruoyi.common.utils.id.SnowflakeId.injectIfEmpty(entity, "planId");
|
||||||
|
// 补 create_by / create_time (历史一直没写, 导致"提交时间"为空; 列表按 create_time 倒序)
|
||||||
|
if (entity.getCreateBy() == null) entity.setCreateBy(SecurityUtils.getUsername());
|
||||||
|
if (entity.getCreateTime() == null) entity.setCreateTime(new Date());
|
||||||
|
return bizProjectPlanMapper.insert(entity);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用 update. #4 触发点 (方案审核结果通知) 在这里插桩:
|
* 通用 update. #4 触发点 (方案审核结果通知) 在这里插桩:
|
||||||
@@ -42,6 +51,10 @@ public class BizProjectPlanServiceImpl implements IBizProjectPlanService
|
|||||||
oldStatus = existed.getStatus();
|
oldStatus = existed.getStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 提交动作 (状态 → '1' 待审核): 写入提交时间; 拒绝 '3' 重提也会刷新
|
||||||
|
if ("1".equals(entity.getStatus()) && !"1".equals(oldStatus)) {
|
||||||
|
entity.setSubmitTime(new Date());
|
||||||
|
}
|
||||||
int n = bizProjectPlanMapper.updateByPrimaryKey(entity);
|
int n = bizProjectPlanMapper.updateByPrimaryKey(entity);
|
||||||
if (n > 0 && entity.getStatus() != null && !entity.getStatus().equals(oldStatus)) {
|
if (n > 0 && entity.getStatus() != null && !entity.getStatus().equals(oldStatus)) {
|
||||||
// 重新读一次拿 submitterId/planName/auditOpinion (entity 可能只传了 planId+status+opinion)
|
// 重新读一次拿 submitterId/planName/auditOpinion (entity 可能只传了 planId+status+opinion)
|
||||||
|
|||||||
+7
@@ -62,6 +62,13 @@ public class BizProjectServiceImpl implements IBizProjectService
|
|||||||
if (entity.getCreateUserId() == null) {
|
if (entity.getCreateUserId() == null) {
|
||||||
entity.setCreateUserId(SecurityUtils.getUserId());
|
entity.setCreateUserId(SecurityUtils.getUserId());
|
||||||
}
|
}
|
||||||
|
// 补 create_by / create_time (历史一直没写, 导致项目列表"创建人/创建时间"两列为空)
|
||||||
|
if (entity.getCreateBy() == null) {
|
||||||
|
entity.setCreateBy(SecurityUtils.getUsername());
|
||||||
|
}
|
||||||
|
if (entity.getCreateTime() == null) {
|
||||||
|
entity.setCreateTime(new java.util.Date());
|
||||||
|
}
|
||||||
return bizProjectMapper.insert(entity);
|
return bizProjectMapper.insert(entity);
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+27
-2
@@ -16,6 +16,7 @@ import com.ruoyi.business.domain.BizExpert;
|
|||||||
import com.ruoyi.business.domain.BizLaborProtocolTemplate;
|
import com.ruoyi.business.domain.BizLaborProtocolTemplate;
|
||||||
import com.ruoyi.business.domain.BizMeeting;
|
import com.ruoyi.business.domain.BizMeeting;
|
||||||
import com.ruoyi.business.domain.BizMeetingAttendee;
|
import com.ruoyi.business.domain.BizMeetingAttendee;
|
||||||
|
import com.ruoyi.business.domain.BizOrg;
|
||||||
import com.ruoyi.business.domain.BizProject;
|
import com.ruoyi.business.domain.BizProject;
|
||||||
import com.ruoyi.business.mapper.BizExpertMapper;
|
import com.ruoyi.business.mapper.BizExpertMapper;
|
||||||
import com.ruoyi.business.mapper.BizLaborProtocolTemplateMapper;
|
import com.ruoyi.business.mapper.BizLaborProtocolTemplateMapper;
|
||||||
@@ -23,6 +24,7 @@ import com.ruoyi.business.mapper.BizMeetingMapper;
|
|||||||
import com.ruoyi.business.mapper.BizProjectMapper;
|
import com.ruoyi.business.mapper.BizProjectMapper;
|
||||||
import com.ruoyi.business.service.BizSignService;
|
import com.ruoyi.business.service.BizSignService;
|
||||||
import com.ruoyi.business.service.IBizMeetingAttendeeService;
|
import com.ruoyi.business.service.IBizMeetingAttendeeService;
|
||||||
|
import com.ruoyi.business.service.IBizOrgService;
|
||||||
import com.ruoyi.business.service.PdfService;
|
import com.ruoyi.business.service.PdfService;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
@@ -42,6 +44,8 @@ public class BizSignServiceImpl implements BizSignService {
|
|||||||
private PdfService pdfService;
|
private PdfService pdfService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private BizProjectMapper projectMapper;
|
private BizProjectMapper projectMapper;
|
||||||
|
@Autowired
|
||||||
|
private IBizOrgService bizOrgService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> getSignInfo(Long attendeeId) {
|
public Map<String, Object> getSignInfo(Long attendeeId) {
|
||||||
@@ -134,7 +138,12 @@ public class BizSignServiceImpl implements BizSignService {
|
|||||||
result.put("attendeeId", attendeeId);
|
result.put("attendeeId", attendeeId);
|
||||||
result.put("meetingName", meeting != null ? meeting.getMeetingName() : "");
|
result.put("meetingName", meeting != null ? meeting.getMeetingName() : "");
|
||||||
result.put("periodNo", meeting != null ? meeting.getPeriodNo() : null);
|
result.put("periodNo", meeting != null ? meeting.getPeriodNo() : null);
|
||||||
result.put("totalPeriods", meeting != null ? meeting.getTotalPeriods() : null);
|
result.put("totalPeriods", assignedTotalPeriods(meeting));
|
||||||
|
|
||||||
|
// 已签署状态: laborProtocol 非空即已签 (重复打开签署链接 → 前端直接展示 PDF)
|
||||||
|
boolean signed = attendee.getLaborProtocol() != null && !attendee.getLaborProtocol().trim().isEmpty();
|
||||||
|
result.put("signed", signed);
|
||||||
|
result.put("laborProtocol", attendee.getLaborProtocol());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,11 +168,27 @@ public class BizSignServiceImpl implements BizSignService {
|
|||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("meetingName", meeting.getMeetingName());
|
result.put("meetingName", meeting.getMeetingName());
|
||||||
result.put("periodNo", meeting.getPeriodNo());
|
result.put("periodNo", meeting.getPeriodNo());
|
||||||
result.put("totalPeriods", meeting.getTotalPeriods());
|
result.put("totalPeriods", assignedTotalPeriods(meeting));
|
||||||
result.put("attendeeId", attendeeId);
|
result.put("attendeeId", attendeeId);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 期数分母: 分配给执行方 (本公司) 的总场次 (biz_project_assign.sessions 之和, 按 execution_unit_id 隔离).
|
||||||
|
* 会议无 execution_unit_id / 反查不到执行方 MAIN / assigned 算出 0 时, 回退项目总期数, 避免显示 1/0. */
|
||||||
|
private Long assignedTotalPeriods(BizMeeting meeting) {
|
||||||
|
if (meeting == null) return null;
|
||||||
|
if (meeting.getExecutionUnitId() == null || meeting.getProjectId() == null) {
|
||||||
|
return meeting.getTotalPeriods();
|
||||||
|
}
|
||||||
|
BizOrg org = bizOrgService.getById(meeting.getExecutionUnitId());
|
||||||
|
Long executorUserId = org == null ? null : org.getUserId();
|
||||||
|
if (executorUserId == null) {
|
||||||
|
return meeting.getTotalPeriods();
|
||||||
|
}
|
||||||
|
int assigned = projectMapper.countAssignedSessions(meeting.getProjectId(), executorUserId);
|
||||||
|
return assigned > 0 ? (long) assigned : meeting.getTotalPeriods();
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, String> map(String value, String label) {
|
private Map<String, String> map(String value, String label) {
|
||||||
Map<String, String> m = new HashMap<>();
|
Map<String, String> m = new HashMap<>();
|
||||||
m.put("value", value);
|
m.put("value", value);
|
||||||
|
|||||||
+8
@@ -7,6 +7,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import com.ruoyi.business.service.IBizMeetingInvoiceService;
|
import com.ruoyi.business.service.IBizMeetingInvoiceService;
|
||||||
import com.ruoyi.business.service.IBizMeetingMaterialService;
|
import com.ruoyi.business.service.IBizMeetingMaterialService;
|
||||||
|
import com.ruoyi.business.service.IBizMeetingService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -58,6 +59,9 @@ public class InvoiceOcrService
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IBizMeetingMaterialService materialService;
|
private IBizMeetingMaterialService materialService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IBizMeetingService bizMeetingService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("ocrExecutor")
|
@Qualifier("ocrExecutor")
|
||||||
private ExecutorService ocrExecutor;
|
private ExecutorService ocrExecutor;
|
||||||
@@ -138,6 +142,8 @@ public class InvoiceOcrService
|
|||||||
{
|
{
|
||||||
// OCR 处理完毕 (成功/非发票/失败), 该材料金额最终确定 → fee_status=1
|
// OCR 处理完毕 (成功/非发票/失败), 该材料金额最终确定 → fee_status=1
|
||||||
materialService.updateFeeStatus(materialId, 1);
|
materialService.updateFeeStatus(materialId, 1);
|
||||||
|
// 立即尝试重算会务费 (若仍有其它发票待 OCR 会回滚 0 走兜底), 不再等 FeeCalcScheduler 每分钟扫
|
||||||
|
bizMeetingService.recomputeMeetingFee(meetingId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -302,6 +308,8 @@ public class InvoiceOcrService
|
|||||||
{
|
{
|
||||||
// 兜底 OCR 处理完毕, 金额最终确定 → fee_status=1
|
// 兜底 OCR 处理完毕, 金额最终确定 → fee_status=1
|
||||||
materialService.updateFeeStatus(inv.getMaterialId(), 1);
|
materialService.updateFeeStatus(inv.getMaterialId(), 1);
|
||||||
|
// 立即尝试重算会务费 (若仍有其它发票待 OCR 会回滚 0 走兜底)
|
||||||
|
bizMeetingService.recomputeMeetingFee(inv.getMeetingId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,10 +75,11 @@ public class AliyunSmsSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送电子签短信 (模板占位符 name=姓名, date=日期 MM月dd日, link=签署链接).
|
* 发送邀请签署短信 (模板 SMS_512040098 占位符 name=姓名, time=会议时间, attendeeId=参会人id).
|
||||||
* 与验证码模板 (ruoyi.sms.template) 分离, 成功返回 true.
|
* 与验证码模板 (ruoyi.sms.template) 分离, 成功返回 true.
|
||||||
|
* time 由 startTime/endTime 统一格式化: 当天 "8月13日18:20 - 19:20", 跨天 "8月13日18:20 - 8月16日19:20".
|
||||||
*/
|
*/
|
||||||
public boolean sendEsign(String phone, String name, Date date, String link) {
|
public boolean sendEsign(String phone, String name, Date startTime, Date endTime, Long attendeeId) {
|
||||||
try {
|
try {
|
||||||
SendSmsRequest req = new SendSmsRequest();
|
SendSmsRequest req = new SendSmsRequest();
|
||||||
req.setPhoneNumbers(phone);
|
req.setPhoneNumbers(phone);
|
||||||
@@ -86,28 +87,42 @@ public class AliyunSmsSender {
|
|||||||
req.setTemplateCode(esignTemplate);
|
req.setTemplateCode(esignTemplate);
|
||||||
Map<String, String> params = new LinkedHashMap<>();
|
Map<String, String> params = new LinkedHashMap<>();
|
||||||
params.put("name", name != null ? name : "");
|
params.put("name", name != null ? name : "");
|
||||||
params.put("date", date != null ? new SimpleDateFormat("MM月dd日").format(date) : "");
|
params.put("time", formatMeetingTime(startTime, endTime));
|
||||||
params.put("link", link != null ? link : "");
|
params.put("attendeeId", attendeeId != null ? String.valueOf(attendeeId) : "");
|
||||||
req.setTemplateParam(JSONUtil.toJsonStr(params));
|
req.setTemplateParam(JSONUtil.toJsonStr(params));
|
||||||
SendSmsResponse resp = getClient().getAcsResponse(req);
|
SendSmsResponse resp = getClient().getAcsResponse(req);
|
||||||
if ("OK".equalsIgnoreCase(resp.getCode())) {
|
if ("OK".equalsIgnoreCase(resp.getCode())) {
|
||||||
log.info("[SMS] 电子签短信发送成功 phone={}, bizId={}", phone, resp.getBizId());
|
log.info("[SMS] 邀请签署短信发送成功 phone={}, bizId={}", phone, resp.getBizId());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
log.error("[SMS] 电子签短信发送失败 phone={}, code={}, msg={}, requestId={}",
|
log.error("[SMS] 邀请签署短信发送失败 phone={}, code={}, msg={}, requestId={}",
|
||||||
phone, resp.getCode(), resp.getMessage(), resp.getRequestId());
|
phone, resp.getCode(), resp.getMessage(), resp.getRequestId());
|
||||||
return false;
|
return false;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[SMS] 电子签短信异常 phone={}", phone, e);
|
log.error("[SMS] 邀请签署短信异常 phone={}", phone, e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 会议时间统一格式化: 当天 "M月d日HH:mm - HH:mm", 跨天 "M月d日HH:mm - M月d日HH:mm" */
|
||||||
|
private String formatMeetingTime(Date start, Date end) {
|
||||||
|
if (start == null && end == null) return "";
|
||||||
|
SimpleDateFormat full = new SimpleDateFormat("M月d日HH:mm");
|
||||||
|
SimpleDateFormat hm = new SimpleDateFormat("HH:mm");
|
||||||
|
if (start == null) return full.format(end);
|
||||||
|
if (end == null) return full.format(start);
|
||||||
|
SimpleDateFormat day = new SimpleDateFormat("yyyyMMdd");
|
||||||
|
if (day.format(start).equals(day.format(end))) {
|
||||||
|
return full.format(start) + " - " + hm.format(end);
|
||||||
|
}
|
||||||
|
return full.format(start) + " - " + full.format(end);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拼电子签签署链接: {esignBaseUrl}/#/doctor/sign-fill?attendeeId={attendeeId}
|
* 拼电子签签署链接: {esignBaseUrl}/#/doctor/sign-fill?attendeeId={attendeeId}
|
||||||
* 例: https://risingdoctor.com/hg/#/doctor/sign-fill?attendeeId=123
|
* 例: https://hegui.bahim.org.cn/#/doctor/sign-fill?attendeeId=123
|
||||||
* (nginx 子路径 /hg 已配在 ruoyi.sms.esignBaseUrl 里, 前端 Vue Router 是 hash 模式,
|
* (前端 Vue Router 是 hash 模式, Java 只拼 #/doctor/sign-fill 路由 + attendeeId,
|
||||||
* 所以 Java 只拼 #/doctor/sign-fill 路由 + attendeeId)
|
* 域名/子路径由 ruoyi.sms.esignBaseUrl 提供)
|
||||||
*/
|
*/
|
||||||
public String esignLink(Long attendeeId) {
|
public String esignLink(Long attendeeId) {
|
||||||
return esignBaseUrl + "/#/doctor/sign-fill?attendeeId=" + attendeeId;
|
return esignBaseUrl + "/#/doctor/sign-fill?attendeeId=" + attendeeId;
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.ruoyi.business.supplier;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供应商账号接口解密后的单条账号 (对应 refer/bindingdemo 里 decrypted rows[] 的一行).
|
||||||
|
* <p>
|
||||||
|
* 登录名 = account/email (邮箱); password 为 BCrypt 哈希 (不可逆), 同步时写入 sys_user.password2.
|
||||||
|
* 字段名与接口返回 JSON 的 camelCase 一致, 直接由 Jackson 反序列化.
|
||||||
|
*/
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class SupplierAccount
|
||||||
|
{
|
||||||
|
/** 登录账号 (= 邮箱) */
|
||||||
|
private String account;
|
||||||
|
/** 邮箱 */
|
||||||
|
private String email;
|
||||||
|
/** 密码 (BCrypt 哈希, 不可逆) */
|
||||||
|
private String password;
|
||||||
|
/** 联系人 */
|
||||||
|
private String contactName;
|
||||||
|
/** 联系电话 */
|
||||||
|
private String contactPhone;
|
||||||
|
/** 企业名称 */
|
||||||
|
private String enterpriseName;
|
||||||
|
/** 企业类型 */
|
||||||
|
private String enterpriseType;
|
||||||
|
/** 公司地址 */
|
||||||
|
private String companyAddress;
|
||||||
|
/** 税号 */
|
||||||
|
private String taxNo;
|
||||||
|
/** 删除标识 (true=已删除) */
|
||||||
|
private Boolean deleted;
|
||||||
|
/** 禁用标识 (true=已禁用) */
|
||||||
|
private Boolean disabled;
|
||||||
|
/** 供应商编码 */
|
||||||
|
private String supplierCode;
|
||||||
|
|
||||||
|
public String getAccount() { return account; }
|
||||||
|
public void setAccount(String account) { this.account = account; }
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
public String getPassword() { return password; }
|
||||||
|
public void setPassword(String password) { this.password = password; }
|
||||||
|
public String getContactName() { return contactName; }
|
||||||
|
public void setContactName(String contactName) { this.contactName = contactName; }
|
||||||
|
public String getContactPhone() { return contactPhone; }
|
||||||
|
public void setContactPhone(String contactPhone) { this.contactPhone = contactPhone; }
|
||||||
|
public String getEnterpriseName() { return enterpriseName; }
|
||||||
|
public void setEnterpriseName(String enterpriseName) { this.enterpriseName = enterpriseName; }
|
||||||
|
public String getEnterpriseType() { return enterpriseType; }
|
||||||
|
public void setEnterpriseType(String enterpriseType) { this.enterpriseType = enterpriseType; }
|
||||||
|
public String getCompanyAddress() { return companyAddress; }
|
||||||
|
public void setCompanyAddress(String companyAddress) { this.companyAddress = companyAddress; }
|
||||||
|
public String getTaxNo() { return taxNo; }
|
||||||
|
public void setTaxNo(String taxNo) { this.taxNo = taxNo; }
|
||||||
|
public Boolean getDeleted() { return deleted; }
|
||||||
|
public void setDeleted(Boolean deleted) { this.deleted = deleted; }
|
||||||
|
public Boolean getDisabled() { return disabled; }
|
||||||
|
public void setDisabled(Boolean disabled) { this.disabled = disabled; }
|
||||||
|
public String getSupplierCode() { return supplierCode; }
|
||||||
|
public void setSupplierCode(String supplierCode) { this.supplierCode = supplierCode; }
|
||||||
|
}
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
package com.ruoyi.business.supplier;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.ruoyi.business.domain.BizOrg;
|
||||||
|
import com.ruoyi.business.domain.BizPerson;
|
||||||
|
import com.ruoyi.business.mapper.BizOrgMapper;
|
||||||
|
import com.ruoyi.business.mapper.BizPersonMapper;
|
||||||
|
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||||
|
import com.ruoyi.common.utils.id.SnowflakeId;
|
||||||
|
import com.ruoyi.system.mapper.SysUserMapper;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供应商账号同步: 把供应商接口拉取/解密的账号, upsert 到执行方侧三张表.
|
||||||
|
* <p>
|
||||||
|
* 去重键 = 邮箱 (account == email). 每个供应商账号对应:
|
||||||
|
* 1. sys_user — 登录账号 (user_name/email=邮箱, password2=供应商 BCrypt 哈希, role_type=executor)
|
||||||
|
* 2. biz_org — 企业档案 (org_type=executor)
|
||||||
|
* 3. biz_person — 联系人档案 (unit_type=executor, is_synced=1)
|
||||||
|
* <p>
|
||||||
|
* 状态同步: disabled → sys_user.status='1' + biz_org.status='1'; deleted → sys_user.del_flag='2'.
|
||||||
|
* 幂等: 每分钟重跑, 按邮箱 upsert, 单条失败只 log 不影响后续.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class SupplierAccountSyncService
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private SysUserMapper sysUserMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BizOrgMapper bizOrgMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BizPersonMapper bizPersonMapper;
|
||||||
|
|
||||||
|
public int sync(List<SupplierAccount> accounts)
|
||||||
|
{
|
||||||
|
if (accounts == null || accounts.isEmpty())
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int done = 0;
|
||||||
|
int failed = 0;
|
||||||
|
for (SupplierAccount a : accounts)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
syncOne(a);
|
||||||
|
done++;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
failed++;
|
||||||
|
log.warn("[SupplierAccountSync] 同步失败 account={} err={}", a.getAccount(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("[SupplierAccountSync] 本轮 {} 条, 成功 {} 失败 {}", accounts.size(), done, failed);
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncOne(SupplierAccount a)
|
||||||
|
{
|
||||||
|
String email = firstNonBlank(a.getEmail(), a.getAccount());
|
||||||
|
if (email == null)
|
||||||
|
{
|
||||||
|
log.warn("[SupplierAccountSync] 账号缺邮箱, 跳过 supplierCode={}", a.getSupplierCode());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String status = Boolean.TRUE.equals(a.getDisabled()) ? "1" : "0";
|
||||||
|
String delFlag = Boolean.TRUE.equals(a.getDeleted()) ? "2" : "0";
|
||||||
|
|
||||||
|
// 供应商侧部分账号缺联系人/电话, 做非空回退 + 按列宽截断 (避免 NOT NULL/UNIQUE/超长报错)
|
||||||
|
String nickName = truncate(firstNonBlank(a.getContactName(), a.getEnterpriseName(), email), 30); // sys_user.nick_name
|
||||||
|
String personName = truncate(firstNonBlank(a.getContactName(), a.getEnterpriseName(), email), 50); // biz_person.name
|
||||||
|
String phone = truncate(firstNonBlank(a.getContactPhone(), a.getSupplierCode(), email), 20); // biz_person.phone 非空唯一
|
||||||
|
String phonenumber = truncate(a.getContactPhone(), 11); // sys_user.phonenumber 可空
|
||||||
|
String orgName = truncate(firstNonBlank(a.getEnterpriseName(), email), 200); // biz_org.org_name
|
||||||
|
String businessNature = truncate(a.getEnterpriseType(), 20);
|
||||||
|
String address = truncate(a.getCompanyAddress(), 500);
|
||||||
|
String taxNo = truncate(a.getTaxNo(), 50);
|
||||||
|
|
||||||
|
// 1. sys_user upsert (去重键=邮箱, 不过滤 del_flag, 以支持"删除→恢复")
|
||||||
|
SysUser user = sysUserMapper.selectUserByEmailIgnoreDel(email);
|
||||||
|
Long userId;
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
SysUser nu = new SysUser();
|
||||||
|
nu.setUserName(email);
|
||||||
|
nu.setNickName(nickName);
|
||||||
|
nu.setEmail(email);
|
||||||
|
nu.setPhonenumber(phonenumber);
|
||||||
|
nu.setPassword2(a.getPassword());
|
||||||
|
nu.setStatus(status);
|
||||||
|
nu.setDelFlag(delFlag);
|
||||||
|
nu.setAccountType("MAIN");
|
||||||
|
nu.setRoleType("executor");
|
||||||
|
sysUserMapper.insertSyncedUser(nu);
|
||||||
|
userId = nu.getUserId();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userId = user.getUserId();
|
||||||
|
SysUser upd = new SysUser();
|
||||||
|
upd.setUserId(userId);
|
||||||
|
upd.setPassword2(a.getPassword());
|
||||||
|
upd.setNickName(nickName);
|
||||||
|
upd.setPhonenumber(phonenumber);
|
||||||
|
upd.setEmail(email);
|
||||||
|
upd.setStatus(status);
|
||||||
|
upd.setDelFlag(delFlag);
|
||||||
|
sysUserMapper.updateSyncedUser(upd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. biz_org upsert (按主账号 user_id)
|
||||||
|
BizOrg q = new BizOrg();
|
||||||
|
q.setUserId(userId);
|
||||||
|
q.setOrgType("executor");
|
||||||
|
List<BizOrg> orgs = bizOrgMapper.selectList(q);
|
||||||
|
BizOrg org = orgs.isEmpty() ? null : orgs.get(0);
|
||||||
|
if (org == null)
|
||||||
|
{
|
||||||
|
BizOrg no = new BizOrg();
|
||||||
|
no.setUserId(userId);
|
||||||
|
no.setOrgName(orgName);
|
||||||
|
no.setOrgType("executor");
|
||||||
|
no.setBusinessNature(businessNature);
|
||||||
|
no.setAddress(address);
|
||||||
|
no.setTaxNo(taxNo);
|
||||||
|
no.setContactName(a.getContactName());
|
||||||
|
no.setContactPhone(a.getContactPhone());
|
||||||
|
no.setStatus(status);
|
||||||
|
bizOrgMapper.insert(no);
|
||||||
|
org = no;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
org.setOrgName(orgName);
|
||||||
|
org.setBusinessNature(businessNature);
|
||||||
|
org.setAddress(address);
|
||||||
|
org.setTaxNo(taxNo);
|
||||||
|
org.setContactName(a.getContactName());
|
||||||
|
org.setContactPhone(a.getContactPhone());
|
||||||
|
org.setStatus(status);
|
||||||
|
bizOrgMapper.updateByPrimaryKey(org);
|
||||||
|
}
|
||||||
|
Long orgId = org.getOrgId();
|
||||||
|
|
||||||
|
// 3. biz_person upsert (按 user_id), 打同步标记
|
||||||
|
BizPerson person = bizPersonMapper.selectByUserId(userId);
|
||||||
|
if (person == null)
|
||||||
|
{
|
||||||
|
BizPerson np = new BizPerson();
|
||||||
|
SnowflakeId.injectIfEmpty(np, "personId");
|
||||||
|
np.setName(personName);
|
||||||
|
np.setPhone(phone);
|
||||||
|
np.setOrgId(orgId);
|
||||||
|
np.setUnitType("executor");
|
||||||
|
np.setUserId(userId);
|
||||||
|
np.setIsSynced(1);
|
||||||
|
np.setCreateBy("supplier-sync");
|
||||||
|
np.setUpdateBy("supplier-sync");
|
||||||
|
bizPersonMapper.insert(np);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
person.setName(personName);
|
||||||
|
person.setPhone(phone);
|
||||||
|
person.setOrgId(orgId);
|
||||||
|
person.setIsSynced(1);
|
||||||
|
person.setUpdateBy("supplier-sync");
|
||||||
|
bizPersonMapper.updateByPrimaryKey(person);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstNonBlank(String... vals)
|
||||||
|
{
|
||||||
|
for (String v : vals)
|
||||||
|
{
|
||||||
|
if (v != null && !v.isBlank())
|
||||||
|
{
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String truncate(String s, int max)
|
||||||
|
{
|
||||||
|
if (s == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return s.length() > max ? s.substring(0, max) : s;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<result property="titleCertUrl" column="title_cert_url" />
|
<result property="titleCertUrl" column="title_cert_url" />
|
||||||
<result property="bankCard" column="bank_card" />
|
<result property="bankCard" column="bank_card" />
|
||||||
<result property="bankName" column="bank_name" />
|
<result property="bankName" column="bank_name" />
|
||||||
|
<result property="bankBranch" column="bank_branch" />
|
||||||
<result property="bankRegion" column="bank_region" />
|
<result property="bankRegion" column="bank_region" />
|
||||||
<result property="bankAddress" column="bank_address" />
|
<result property="bankAddress" column="bank_address" />
|
||||||
<result property="idCardAttachments" column="id_card_attachments" />
|
<result property="idCardAttachments" column="id_card_attachments" />
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
<result property="updateTime" column="update_time" />
|
<result property="updateTime" column="update_time" />
|
||||||
</resultMap>
|
</resultMap>
|
||||||
<sql id="selectFields">
|
<sql id="selectFields">
|
||||||
select expert_id, user_id, name, phone, region, id_card, work_unit, department, title, practice_cert_url, title_cert_url, bank_card, bank_name, bank_region, bank_address, id_card_attachments, audit_status, audit_by, audit_time, status, create_by, create_time, update_by, update_time
|
select expert_id, user_id, name, phone, region, id_card, work_unit, department, title, practice_cert_url, title_cert_url, bank_card, bank_name, bank_branch, bank_region, bank_address, id_card_attachments, audit_status, audit_by, audit_time, status, create_by, create_time, update_by, update_time
|
||||||
from biz_expert
|
from biz_expert
|
||||||
</sql>
|
</sql>
|
||||||
<select id="selectByUserId" resultMap="BizExpertResult" parameterType="Long">
|
<select id="selectByUserId" resultMap="BizExpertResult" parameterType="Long">
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
<if test="titleCertUrl != null">title_cert_url,</if>
|
<if test="titleCertUrl != null">title_cert_url,</if>
|
||||||
<if test="bankCard != null">bank_card,</if>
|
<if test="bankCard != null">bank_card,</if>
|
||||||
<if test="bankName != null">bank_name,</if>
|
<if test="bankName != null">bank_name,</if>
|
||||||
|
<if test="bankBranch != null">bank_branch,</if>
|
||||||
<if test="bankRegion != null">bank_region,</if>
|
<if test="bankRegion != null">bank_region,</if>
|
||||||
<if test="bankAddress != null">bank_address,</if>
|
<if test="bankAddress != null">bank_address,</if>
|
||||||
<if test="auditStatus != null">audit_status,</if>
|
<if test="auditStatus != null">audit_status,</if>
|
||||||
@@ -94,6 +96,7 @@
|
|||||||
<if test="titleCertUrl != null">#{titleCertUrl},</if>
|
<if test="titleCertUrl != null">#{titleCertUrl},</if>
|
||||||
<if test="bankCard != null">#{bankCard},</if>
|
<if test="bankCard != null">#{bankCard},</if>
|
||||||
<if test="bankName != null">#{bankName},</if>
|
<if test="bankName != null">#{bankName},</if>
|
||||||
|
<if test="bankBranch != null">#{bankBranch},</if>
|
||||||
<if test="bankRegion != null">#{bankRegion},</if>
|
<if test="bankRegion != null">#{bankRegion},</if>
|
||||||
<if test="bankAddress != null">#{bankAddress},</if>
|
<if test="bankAddress != null">#{bankAddress},</if>
|
||||||
<if test="auditStatus != null">#{auditStatus},</if>
|
<if test="auditStatus != null">#{auditStatus},</if>
|
||||||
@@ -118,6 +121,7 @@
|
|||||||
<if test="idCard != null">id_card = #{idCard},</if>
|
<if test="idCard != null">id_card = #{idCard},</if>
|
||||||
<if test="bankCard != null">bank_card = #{bankCard},</if>
|
<if test="bankCard != null">bank_card = #{bankCard},</if>
|
||||||
<if test="bankName != null">bank_name = #{bankName},</if>
|
<if test="bankName != null">bank_name = #{bankName},</if>
|
||||||
|
<if test="bankBranch != null">bank_branch = #{bankBranch},</if>
|
||||||
<if test="bankRegion != null">bank_region = #{bankRegion},</if>
|
<if test="bankRegion != null">bank_region = #{bankRegion},</if>
|
||||||
<if test="bankAddress != null">bank_address = #{bankAddress},</if>
|
<if test="bankAddress != null">bank_address = #{bankAddress},</if>
|
||||||
<if test="idCardAttachments != null">id_card_attachments = #{idCardAttachments},</if>
|
<if test="idCardAttachments != null">id_card_attachments = #{idCardAttachments},</if>
|
||||||
@@ -144,6 +148,7 @@
|
|||||||
<if test="idCard != null">id_card,</if>
|
<if test="idCard != null">id_card,</if>
|
||||||
<if test="bankCard != null">bank_card,</if>
|
<if test="bankCard != null">bank_card,</if>
|
||||||
<if test="bankName != null">bank_name,</if>
|
<if test="bankName != null">bank_name,</if>
|
||||||
|
<if test="bankBranch != null">bank_branch,</if>
|
||||||
<if test="bankRegion != null">bank_region,</if>
|
<if test="bankRegion != null">bank_region,</if>
|
||||||
<if test="bankAddress != null">bank_address,</if>
|
<if test="bankAddress != null">bank_address,</if>
|
||||||
<if test="idCardAttachments != null">id_card_attachments,</if>
|
<if test="idCardAttachments != null">id_card_attachments,</if>
|
||||||
@@ -162,6 +167,7 @@
|
|||||||
<if test="idCard != null">#{idCard},</if>
|
<if test="idCard != null">#{idCard},</if>
|
||||||
<if test="bankCard != null">#{bankCard},</if>
|
<if test="bankCard != null">#{bankCard},</if>
|
||||||
<if test="bankName != null">#{bankName},</if>
|
<if test="bankName != null">#{bankName},</if>
|
||||||
|
<if test="bankBranch != null">#{bankBranch},</if>
|
||||||
<if test="bankRegion != null">#{bankRegion},</if>
|
<if test="bankRegion != null">#{bankRegion},</if>
|
||||||
<if test="bankAddress != null">#{bankAddress},</if>
|
<if test="bankAddress != null">#{bankAddress},</if>
|
||||||
<if test="idCardAttachments != null">#{idCardAttachments},</if>
|
<if test="idCardAttachments != null">#{idCardAttachments},</if>
|
||||||
@@ -178,6 +184,7 @@
|
|||||||
<if test="idCard != null and idCard != ''">id_card = #{idCard},</if>
|
<if test="idCard != null and idCard != ''">id_card = #{idCard},</if>
|
||||||
<if test="bankCard != null and bankCard != ''">bank_card = #{bankCard},</if>
|
<if test="bankCard != null and bankCard != ''">bank_card = #{bankCard},</if>
|
||||||
<if test="bankName != null and bankName != ''">bank_name = #{bankName},</if>
|
<if test="bankName != null and bankName != ''">bank_name = #{bankName},</if>
|
||||||
|
<if test="bankBranch != null and bankBranch != ''">bank_branch = #{bankBranch},</if>
|
||||||
<if test="bankRegion != null and bankRegion != ''">bank_region = #{bankRegion},</if>
|
<if test="bankRegion != null and bankRegion != ''">bank_region = #{bankRegion},</if>
|
||||||
<if test="bankAddress != null and bankAddress != ''">bank_address = #{bankAddress},</if>
|
<if test="bankAddress != null and bankAddress != ''">bank_address = #{bankAddress},</if>
|
||||||
<if test="idCardAttachments != null and idCardAttachments != ''">id_card_attachments = #{idCardAttachments},</if>
|
<if test="idCardAttachments != null and idCardAttachments != ''">id_card_attachments = #{idCardAttachments},</if>
|
||||||
@@ -201,7 +208,7 @@
|
|||||||
(expert_id, user_id, name, phone, region, id_card,
|
(expert_id, user_id, name, phone, region, id_card,
|
||||||
work_unit, department, title,
|
work_unit, department, title,
|
||||||
practice_cert_url, title_cert_url,
|
practice_cert_url, title_cert_url,
|
||||||
bank_card, bank_name, bank_region, bank_address,
|
bank_card, bank_name, bank_branch, bank_region, bank_address,
|
||||||
id_card_attachments,
|
id_card_attachments,
|
||||||
audit_status, audit_by, audit_time, status,
|
audit_status, audit_by, audit_time, status,
|
||||||
create_by, create_time)
|
create_by, create_time)
|
||||||
@@ -210,7 +217,7 @@
|
|||||||
(#{e.expertId}, #{e.userId}, #{e.name}, #{e.phone}, #{e.region}, #{e.idCard},
|
(#{e.expertId}, #{e.userId}, #{e.name}, #{e.phone}, #{e.region}, #{e.idCard},
|
||||||
#{e.workUnit}, #{e.department}, #{e.title},
|
#{e.workUnit}, #{e.department}, #{e.title},
|
||||||
#{e.practiceCertUrl}, #{e.titleCertUrl},
|
#{e.practiceCertUrl}, #{e.titleCertUrl},
|
||||||
#{e.bankCard}, #{e.bankName}, #{e.bankRegion}, #{e.bankAddress},
|
#{e.bankCard}, #{e.bankName}, #{e.bankBranch}, #{e.bankRegion}, #{e.bankAddress},
|
||||||
#{e.idCardAttachments},
|
#{e.idCardAttachments},
|
||||||
#{e.auditStatus}, #{e.auditBy}, #{e.auditTime}, #{e.status},
|
#{e.auditStatus}, #{e.auditBy}, #{e.auditTime}, #{e.status},
|
||||||
#{e.createBy}, sysdate())
|
#{e.createBy}, sysdate())
|
||||||
|
|||||||
+7
-1
@@ -41,6 +41,7 @@
|
|||||||
<result property="isDeleted" column="is_deleted" />
|
<result property="isDeleted" column="is_deleted" />
|
||||||
<result property="isEsigned" column="is_esigned" />
|
<result property="isEsigned" column="is_esigned" />
|
||||||
<result property="isInvited" column="is_invited" />
|
<result property="isInvited" column="is_invited" />
|
||||||
|
<result property="hasIntent" column="has_intent" />
|
||||||
</resultMap>
|
</resultMap>
|
||||||
<insert id="insert" parameterType="BizMeetingAttendee">
|
<insert id="insert" parameterType="BizMeetingAttendee">
|
||||||
insert into biz_meeting_attendee(id, meeting_id, user_id, create_by, create_time)
|
insert into biz_meeting_attendee(id, meeting_id, user_id, create_by, create_time)
|
||||||
@@ -152,7 +153,12 @@
|
|||||||
delete from biz_meeting_attendee where meeting_id = #{meetingId} and user_id = #{userId}
|
delete from biz_meeting_attendee where meeting_id = #{meetingId} and user_id = #{userId}
|
||||||
</delete>
|
</delete>
|
||||||
<select id="selectByMeetingId" resultMap="BizMeetingAttendeeResult" parameterType="Long">
|
<select id="selectByMeetingId" resultMap="BizMeetingAttendeeResult" parameterType="Long">
|
||||||
select id, meeting_id, user_id, name, phone, work_unit, department, title, id_card, bank_card, bank_name, bank_branch, bank_region, bank_address, account_name, id_card_attachments, labor_form, fee_pre_tax, tax, fee, vat_and_surcharge, summary, on_site_photos, signed_at, signed_ip, handsign, labor_protocol, labor_protocol_masked, create_by, create_time, is_deleted, is_esigned, is_invited
|
select id, meeting_id, user_id, name, phone, work_unit, department, title, id_card, bank_card, bank_name, bank_branch, bank_region, bank_address, account_name, id_card_attachments, labor_form, fee_pre_tax, tax, fee, vat_and_surcharge, summary, on_site_photos, signed_at, signed_ip, handsign, labor_protocol, labor_protocol_masked, create_by, create_time, is_deleted, is_esigned, is_invited,
|
||||||
|
(select if(exists(
|
||||||
|
select 1 from biz_execution_intent e
|
||||||
|
where e.user_id = biz_meeting_attendee.user_id
|
||||||
|
and e.project_no collate utf8mb4_unicode_ci = (select p.project_no collate utf8mb4_unicode_ci from biz_meeting m join biz_project p on m.project_id = p.project_id where m.meeting_id = #{meetingId})
|
||||||
|
), 1, 0)) as has_intent
|
||||||
from biz_meeting_attendee where meeting_id = #{meetingId} and is_deleted = 0
|
from biz_meeting_attendee where meeting_id = #{meetingId} and is_deleted = 0
|
||||||
order by id
|
order by id
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<result property="meetingName" column="meeting_name" />
|
<result property="meetingName" column="meeting_name" />
|
||||||
<result property="periodNo" column="period_no" />
|
<result property="periodNo" column="period_no" />
|
||||||
<result property="totalPeriods" column="total_periods" />
|
<result property="totalPeriods" column="total_periods" />
|
||||||
|
<result property="assignedSessions" column="assigned_sessions" />
|
||||||
<result property="projectForm" column="project_form" />
|
<result property="projectForm" column="project_form" />
|
||||||
<result property="startTime" column="start_time" />
|
<result property="startTime" column="start_time" />
|
||||||
<result property="endTime" column="end_time" />
|
<result property="endTime" column="end_time" />
|
||||||
@@ -62,12 +63,14 @@
|
|||||||
from biz_meeting
|
from biz_meeting
|
||||||
where meeting_id = #{meetingId} and is_deleted = 0
|
where meeting_id = #{meetingId} and is_deleted = 0
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectList" resultMap="BizMeetingResult" parameterType="BizMeeting">
|
<select id="selectList" resultMap="BizMeetingResult" parameterType="BizMeeting">
|
||||||
select
|
select
|
||||||
<if test="userId != null">(select a.id from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId} and a.is_deleted = 0 limit 1) as attendee_id,</if>
|
<if test="userId != null">(select a.id from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId} and a.is_deleted = 0 limit 1) as attendee_id,</if>
|
||||||
<if test="userId != null">(select a.labor_protocol from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId} and a.is_deleted = 0 limit 1) as attendee_labor_protocol,</if>
|
<if test="userId != null">(select a.labor_protocol from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId} and a.is_deleted = 0 limit 1) as attendee_labor_protocol,</if>
|
||||||
(select o.org_name from biz_project p join biz_org o on o.org_id = p.sponsor_org_id where p.project_id = biz_meeting.project_id limit 1) as org_name,
|
(select o.org_name from biz_project p join biz_org o on o.org_id = p.sponsor_org_id where p.project_id = biz_meeting.project_id limit 1) as org_name,
|
||||||
(select p.invitation_url from biz_project p where p.project_id = biz_meeting.project_id limit 1) as project_invitation_url,
|
(select p.invitation_url from biz_project p where p.project_id = biz_meeting.project_id limit 1) as project_invitation_url,
|
||||||
|
<if test="params.assignedExecutorUserId != null">(select coalesce(sum(a.sessions), 0) from biz_project_assign a where a.project_id = biz_meeting.project_id and a.is_deleted = 0 and a.execution_unit_id = (select org_id from biz_org where user_id = #{params.assignedExecutorUserId} and org_type = 'executor')) as assigned_sessions,</if>
|
||||||
<include refid="selectFields"/>
|
<include refid="selectFields"/>
|
||||||
from biz_meeting
|
from biz_meeting
|
||||||
<where>
|
<where>
|
||||||
@@ -78,15 +81,15 @@
|
|||||||
<if test="periodNo != null">and period_no = #{periodNo}</if>
|
<if test="periodNo != null">and period_no = #{periodNo}</if>
|
||||||
<if test="projectForm != null and projectForm != ''">and project_form = #{projectForm}</if>
|
<if test="projectForm != null and projectForm != ''">and project_form = #{projectForm}</if>
|
||||||
<if test="currentStage != null and currentStage != ''">and current_stage = #{currentStage}</if>
|
<if test="currentStage != null and currentStage != ''">and current_stage = #{currentStage}</if>
|
||||||
|
<if test="currentStageNotIn != null and currentStageNotIn != ''">
|
||||||
|
and current_stage not in
|
||||||
|
<foreach collection="currentStageNotIn.split(',')" item="s" open="(" separator="," close=")">#{s}</foreach>
|
||||||
|
</if>
|
||||||
<if test="startTime != null">and start_time >= #{startTime}</if>
|
<if test="startTime != null">and start_time >= #{startTime}</if>
|
||||||
<if test="endTime != null">and end_time <= #{endTime}</if>
|
<if test="endTime != null">and end_time <= #{endTime}</if>
|
||||||
<!-- doctor 角色按 user_id 过滤 (走 biz_meeting_attendee 中间表, 同时 attendee 也需 is_deleted=0) -->
|
|
||||||
<if test="userId != null">and exists (select 1 from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId} and a.is_deleted = 0)</if>
|
<if test="userId != null">and exists (select 1 from biz_meeting_attendee a where a.meeting_id = biz_meeting.meeting_id and a.user_id = #{userId} and a.is_deleted = 0)</if>
|
||||||
<!-- sponsor 数据权限: 只看"我的项目"下的会议 (project_id ∈ 我的项目). MAIN 走 sponsor_org_id (经 user_id 反查 org_id), SUB 走 sponsor_assign.monitor_user_id.
|
|
||||||
刻意不带 biz_publicity_support_intent 关联 (与项目列表 selectSponsorList 的区别点) -->
|
|
||||||
<if test="params.sponsorAdminUserId != null">and project_id in (select project_id from biz_project where sponsor_org_id = (select org_id from biz_org where user_id = #{params.sponsorAdminUserId} and org_type = 'sponsor') and is_deleted = 0)</if>
|
<if test="params.sponsorAdminUserId != null">and project_id in (select project_id from biz_project where sponsor_org_id = (select org_id from biz_org where user_id = #{params.sponsorAdminUserId} and org_type = 'sponsor') and is_deleted = 0)</if>
|
||||||
<if test="params.monitorUserId != null">and project_id in (select distinct project_id from biz_project_sponsor_assign where monitor_user_id = #{params.monitorUserId} and is_deleted = 0)</if>
|
<if test="params.monitorUserId != null">and project_id in (select distinct project_id from biz_project_sponsor_assign where monitor_user_id = #{params.monitorUserId} and is_deleted = 0)</if>
|
||||||
<!-- 结题且未开通的项目, sponsor 会议不可见 (与项目列表 selectSponsorList 同口径) -->
|
|
||||||
<if test="params.sponsorAdminUserId != null or params.monitorUserId != null">
|
<if test="params.sponsorAdminUserId != null or params.monitorUserId != null">
|
||||||
and not exists (
|
and not exists (
|
||||||
select 1 from biz_project p2
|
select 1 from biz_project p2
|
||||||
@@ -95,20 +98,39 @@
|
|||||||
and p2.is_finished = '1' and p2.open_status = 'N'
|
and p2.is_finished = '1' and p2.open_status = 'N'
|
||||||
)
|
)
|
||||||
</if>
|
</if>
|
||||||
<!-- executor 数据权限: MAIN 看本执行单位项目下的会议 (biz_project_assign.execution_unit_id), SUB(执行人) 只看本人创建的会议 (create_by) -->
|
|
||||||
<if test="params.executorUserId != null">and project_id in (
|
<if test="params.executorUserId != null">and project_id in (
|
||||||
select distinct a.project_id from biz_project_assign a
|
select distinct a.project_id from biz_project_assign a
|
||||||
where a.is_deleted = 0
|
where a.is_deleted = 0
|
||||||
and a.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')
|
and a.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')
|
||||||
)</if>
|
)</if>
|
||||||
<if test="params.executorCreatorUsername != null and params.executorCreatorUsername != ''">and create_by = #{params.executorCreatorUsername}</if>
|
<if test="params.executorCreatorUsername != null and params.executorCreatorUsername != ''">and create_by = #{params.executorCreatorUsername}</if>
|
||||||
<!-- 合规人员(manager) 数据权限: 只看本人创建项目的会议 -->
|
|
||||||
<if test="params.managerCreateUserId != null">and project_id in (
|
<if test="params.managerCreateUserId != null">and project_id in (
|
||||||
select project_id from biz_project where create_user_id = #{params.managerCreateUserId} and is_deleted = 0
|
select project_id from biz_project where create_user_id = #{params.managerCreateUserId} and is_deleted = 0
|
||||||
)</if>
|
)</if>
|
||||||
</where>
|
</where>
|
||||||
order by meeting_id desc
|
order by meeting_id desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 会议阶段统计 (仅 sponsor/Home KPI 用): 按 current_stage 分组计数, 只套 sponsor 数据权限 (其他角色单独修, 不在此复用多角色过滤) -->
|
||||||
|
<select id="selectStageStats" resultType="java.util.HashMap" parameterType="BizMeeting">
|
||||||
|
select current_stage as currentStage, count(*) as cnt
|
||||||
|
from biz_meeting
|
||||||
|
<where>
|
||||||
|
is_deleted = 0
|
||||||
|
<if test="params.sponsorAdminUserId != null">and project_id in (select project_id from biz_project where sponsor_org_id = (select org_id from biz_org where user_id = #{params.sponsorAdminUserId} and org_type = 'sponsor') and is_deleted = 0)</if>
|
||||||
|
<if test="params.monitorUserId != null">and project_id in (select distinct project_id from biz_project_sponsor_assign where monitor_user_id = #{params.monitorUserId} and is_deleted = 0)</if>
|
||||||
|
<if test="params.sponsorAdminUserId != null or params.monitorUserId != null">
|
||||||
|
and not exists (
|
||||||
|
select 1 from biz_project p2
|
||||||
|
where p2.project_id = biz_meeting.project_id
|
||||||
|
and p2.is_deleted = 0
|
||||||
|
and p2.is_finished = '1' and p2.open_status = 'N'
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
group by current_stage
|
||||||
|
</select>
|
||||||
|
|
||||||
<insert id="insert" parameterType="BizMeeting">
|
<insert id="insert" parameterType="BizMeeting">
|
||||||
insert into biz_meeting
|
insert into biz_meeting
|
||||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||||
@@ -250,7 +272,27 @@
|
|||||||
and execution_unit_id = #{executionUnitId}
|
and execution_unit_id = #{executionUnitId}
|
||||||
and period_no = #{periodNo}
|
and period_no = #{periodNo}
|
||||||
</select>
|
</select>
|
||||||
<!-- 自动流转 (MeetingStageScheduler 每分钟调): start_time 已过 且 material 未提交 且未执行的会议 → is_executed=1 + RUNNING. 已软删/已冻结/已提交材料的不动. -->
|
<!-- 修改校验用 (执行方隔离): 同上, 但排除指定 meetingId (修改自身会议不触发相同期数误报) -->
|
||||||
|
<select id="countByProjectIdExecutionUnitPeriodExclude" resultType="int">
|
||||||
|
select count(*) from biz_meeting where project_id = #{projectId} and is_deleted = 0
|
||||||
|
and execution_unit_id = #{executionUnitId}
|
||||||
|
and period_no = #{periodNo}
|
||||||
|
and meeting_id != #{excludeMeetingId}
|
||||||
|
</select>
|
||||||
|
<!-- 自动流转 (MeetingStageScheduler 每分钟调): start_time 已过 且 end_time 未到 且 material 未提交 且未执行的会议 → current_stage = IN_PROGRESS (执行中). 已软删/已冻结/已执行的不动. -->
|
||||||
|
<update id="markInProgress">
|
||||||
|
update biz_meeting
|
||||||
|
set current_stage = 'IN_PROGRESS'
|
||||||
|
where is_deleted = 0
|
||||||
|
and is_executed = 0
|
||||||
|
and is_frozen = 0
|
||||||
|
and start_time is not null
|
||||||
|
and start_time <= NOW()
|
||||||
|
and end_time > NOW()
|
||||||
|
and labor_audit_stage = 'NOT_SUBMITTED'
|
||||||
|
and service_audit_stage = 'NOT_SUBMITTED'
|
||||||
|
</update>
|
||||||
|
<!-- 自动流转 (MeetingStageScheduler 每分钟调): end_time 已过 且 material 未提交 且未执行的会议 → is_executed=1 + RUNNING (已执行). 已软删/已冻结/已提交材料的不动. -->
|
||||||
<update id="markExecuted">
|
<update id="markExecuted">
|
||||||
update biz_meeting
|
update biz_meeting
|
||||||
set is_executed = 1,
|
set is_executed = 1,
|
||||||
@@ -259,8 +301,8 @@
|
|||||||
where is_deleted = 0
|
where is_deleted = 0
|
||||||
and is_executed = 0
|
and is_executed = 0
|
||||||
and is_frozen = 0
|
and is_frozen = 0
|
||||||
and start_time is not null
|
and end_time is not null
|
||||||
and start_time <= NOW()
|
and end_time <= NOW()
|
||||||
and labor_audit_stage = 'NOT_SUBMITTED'
|
and labor_audit_stage = 'NOT_SUBMITTED'
|
||||||
and service_audit_stage = 'NOT_SUBMITTED'
|
and service_audit_stage = 'NOT_SUBMITTED'
|
||||||
</update>
|
</update>
|
||||||
@@ -282,9 +324,9 @@
|
|||||||
<select id="selectPendingFeeCalcIds" resultType="Long">
|
<select id="selectPendingFeeCalcIds" resultType="Long">
|
||||||
select meeting_id from biz_meeting where fee_calc_status = 0 and is_deleted = 0
|
select meeting_id from biz_meeting where fee_calc_status = 0 and is_deleted = 0
|
||||||
</select>
|
</select>
|
||||||
<!-- 置未汇总 (人员/材料变化触发, 幂等) -->
|
<!-- 费用重算状态机: 直接置 fee_calc_status (-1 计算中 / 0 待算兜底); 成功(1) 由 updateFeeSummary 一并写入 -->
|
||||||
<update id="markFeeCalcPending" parameterType="Long">
|
<update id="updateFeeCalcStatus">
|
||||||
update biz_meeting set fee_calc_status = 0 where meeting_id = #{meetingId}
|
update biz_meeting set fee_calc_status = #{status} where meeting_id = #{meetingId}
|
||||||
</update>
|
</update>
|
||||||
<!-- 汇总回写 3 个费用字段 + 置已汇总 -->
|
<!-- 汇总回写 3 个费用字段 + 置已汇总 -->
|
||||||
<update id="updateFeeSummary">
|
<update id="updateFeeSummary">
|
||||||
@@ -295,4 +337,11 @@
|
|||||||
fee_calc_status = 1
|
fee_calc_status = 1
|
||||||
where meeting_id = #{meetingId}
|
where meeting_id = #{meetingId}
|
||||||
</update>
|
</update>
|
||||||
|
<!-- 同步重算劳务费 (参会人增删改后立即调用): 只回写 labor_fee + total_fee, 不动 meeting_fee / fee_calc_status -->
|
||||||
|
<update id="updateLaborFee">
|
||||||
|
update biz_meeting
|
||||||
|
set labor_fee = #{laborFee},
|
||||||
|
total_fee = #{totalFee}
|
||||||
|
where meeting_id = #{meetingId}
|
||||||
|
</update>
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -21,9 +21,11 @@
|
|||||||
<sql id="selectFields">
|
<sql id="selectFields">
|
||||||
select m.msg_id, m.receiver_user_id, m.msg_type, m.title, m.content, m.biz_type, m.biz_id,
|
select m.msg_id, m.receiver_user_id, m.msg_type, m.title, m.content, m.biz_type, m.biz_id,
|
||||||
m.is_read, m.read_time, m.create_by, m.create_time, m.update_by, m.update_time,
|
m.is_read, m.read_time, m.create_by, m.create_time, m.update_by, m.update_time,
|
||||||
u.user_name as receiver_name
|
COALESCE(NULLIF(bp.name, ''), NULLIF(be.name, ''), u.nick_name) as receiver_name
|
||||||
from biz_message m
|
from biz_message m
|
||||||
left join sys_user u on u.user_id = m.receiver_user_id
|
left join sys_user u on u.user_id = m.receiver_user_id
|
||||||
|
left join biz_person bp on bp.user_id = m.receiver_user_id
|
||||||
|
left join biz_expert be on be.user_id = m.receiver_user_id
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<select id="selectByPrimaryKey" resultMap="BizMessageResult" parameterType="Long">
|
<select id="selectByPrimaryKey" resultMap="BizMessageResult" parameterType="Long">
|
||||||
@@ -49,9 +51,23 @@
|
|||||||
m.receiver_user_id = #{receiverUserId}
|
m.receiver_user_id = #{receiverUserId}
|
||||||
</where>
|
</where>
|
||||||
order by m.is_read asc, m.msg_id desc
|
order by m.is_read asc, m.msg_id desc
|
||||||
|
<!-- 分页: params.offset 有值时用 offset,limit; 否则只用 limit (嵌入式列表) -->
|
||||||
|
<choose>
|
||||||
|
<when test="params.offset != null">
|
||||||
|
limit #{params.offset}, #{params.limit}
|
||||||
|
</when>
|
||||||
|
<otherwise>
|
||||||
<if test="params.limit != null">
|
<if test="params.limit != null">
|
||||||
limit #{params.limit}
|
limit #{params.limit}
|
||||||
</if>
|
</if>
|
||||||
|
</otherwise>
|
||||||
|
</choose>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 某用户的全部消息总数 (分页 total 用, 与 countUnread 解耦) -->
|
||||||
|
<select id="countMy" resultType="int" parameterType="Long">
|
||||||
|
select count(*) from biz_message
|
||||||
|
where receiver_user_id = #{receiverUserId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="countUnread" resultType="int" parameterType="Long">
|
<select id="countUnread" resultType="int" parameterType="Long">
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
<if test="orgId != null">and o.org_id = #{orgId}</if>
|
<if test="orgId != null">and o.org_id = #{orgId}</if>
|
||||||
<if test="orgName != null and orgName != ''">and o.org_name like concat('%', #{orgName}, '%')</if>
|
<if test="orgName != null and orgName != ''">and o.org_name like concat('%', #{orgName}, '%')</if>
|
||||||
order by o.org_id desc
|
order by o.org_id desc
|
||||||
LIMIT 5
|
LIMIT 10
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
<if test="orgId != null">and o.org_id = #{orgId}</if>
|
<if test="orgId != null">and o.org_id = #{orgId}</if>
|
||||||
<if test="orgName != null and orgName != ''">and o.org_name like concat('%', #{orgName}, '%')</if>
|
<if test="orgName != null and orgName != ''">and o.org_name like concat('%', #{orgName}, '%')</if>
|
||||||
order by o.org_id desc
|
order by o.org_id desc
|
||||||
LIMIT 5
|
LIMIT 10
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -198,4 +198,12 @@
|
|||||||
(select org_id from biz_person where user_id = #{userId} limit 1)
|
(select org_id from biz_person where user_id = #{userId} limit 1)
|
||||||
)
|
)
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 单位名称查重 (新增/改名前): 同 orgType 下 org_name 精确匹配 (trim 后) 的条数; orgId 传非空则排除自身 (改名用) -->
|
||||||
|
<select id="countByOrgName" parameterType="BizOrg" resultType="int">
|
||||||
|
select count(*) from biz_org
|
||||||
|
where org_type = #{orgType}
|
||||||
|
and trim(org_name) = #{orgName}
|
||||||
|
<if test="orgId != null"> and org_id != #{orgId}</if>
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<result property="department" column="department" />
|
<result property="department" column="department" />
|
||||||
<result property="position" column="position" />
|
<result property="position" column="position" />
|
||||||
<result property="userId" column="user_id" />
|
<result property="userId" column="user_id" />
|
||||||
|
<result property="isSynced" column="is_synced" />
|
||||||
<result property="unitType" column="unit_type" />
|
<result property="unitType" column="unit_type" />
|
||||||
<result property="accountType" column="account_type" />
|
<result property="accountType" column="account_type" />
|
||||||
<result property="parentUserId" column="parent_user_id" />
|
<result property="parentUserId" column="parent_user_id" />
|
||||||
@@ -23,14 +24,14 @@
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectFields">
|
<sql id="selectFields">
|
||||||
select person_id, name, phone, org_id, department, position, unit_type, user_id, create_by, create_time, update_by, update_time
|
select person_id, name, phone, org_id, department, position, unit_type, is_synced, user_id, create_by, create_time, update_by, update_time
|
||||||
from biz_person
|
from biz_person
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<!-- 通用列表: LEFT JOIN biz_org 取公司名/类型, LEFT JOIN sys_user 取账号状态/类型 -->
|
<!-- 通用列表: LEFT JOIN biz_org 取公司名/类型, LEFT JOIN sys_user 取账号状态/类型 -->
|
||||||
<sql id="selectFieldsWithAccount">
|
<sql id="selectFieldsWithAccount">
|
||||||
select p.person_id, p.name, p.phone, p.org_id, o.org_name, o.org_type,
|
select p.person_id, p.name, p.phone, p.org_id, o.org_name, o.org_type,
|
||||||
p.department, p.position, p.unit_type, p.user_id,
|
p.department, p.position, p.unit_type, p.is_synced, p.user_id,
|
||||||
p.create_by, p.create_time, p.update_by, p.update_time,
|
p.create_by, p.create_time, p.update_by, p.update_time,
|
||||||
u.account_type, u.parent_user_id, u.status, u.del_flag as user_del_flag,
|
u.account_type, u.parent_user_id, u.status, u.del_flag as user_del_flag,
|
||||||
u.user_name as user_name,
|
u.user_name as user_name,
|
||||||
@@ -82,6 +83,7 @@
|
|||||||
<if test="department != null">department,</if>
|
<if test="department != null">department,</if>
|
||||||
<if test="position != null">position,</if>
|
<if test="position != null">position,</if>
|
||||||
<if test="unitType != null">unit_type,</if>
|
<if test="unitType != null">unit_type,</if>
|
||||||
|
<if test="isSynced != null">is_synced,</if>
|
||||||
<if test="userId != null">user_id,</if>
|
<if test="userId != null">user_id,</if>
|
||||||
<if test="createBy != null and createBy != ''">create_by,</if>
|
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||||
create_time,
|
create_time,
|
||||||
@@ -96,6 +98,7 @@
|
|||||||
<if test="department != null">#{department},</if>
|
<if test="department != null">#{department},</if>
|
||||||
<if test="position != null">#{position},</if>
|
<if test="position != null">#{position},</if>
|
||||||
<if test="unitType != null">#{unitType},</if>
|
<if test="unitType != null">#{unitType},</if>
|
||||||
|
<if test="isSynced != null">#{isSynced},</if>
|
||||||
<if test="userId != null">#{userId},</if>
|
<if test="userId != null">#{userId},</if>
|
||||||
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
||||||
sysdate(),
|
sysdate(),
|
||||||
@@ -110,6 +113,7 @@
|
|||||||
<if test="userId != null and userId != ''">user_id = #{userId},</if>
|
<if test="userId != null and userId != ''">user_id = #{userId},</if>
|
||||||
<if test="orgId != null">org_id = #{orgId},</if>
|
<if test="orgId != null">org_id = #{orgId},</if>
|
||||||
<if test="unitType != null and unitType != ''">unit_type = #{unitType},</if>
|
<if test="unitType != null and unitType != ''">unit_type = #{unitType},</if>
|
||||||
|
<if test="isSynced != null">is_synced = #{isSynced},</if>
|
||||||
<if test="name != null">name = #{name},</if>
|
<if test="name != null">name = #{name},</if>
|
||||||
<if test="phone != null">phone = #{phone},</if>
|
<if test="phone != null">phone = #{phone},</if>
|
||||||
<if test="department != null">department = #{department},</if>
|
<if test="department != null">department = #{department},</if>
|
||||||
@@ -145,15 +149,13 @@
|
|||||||
</foreach>
|
</foreach>
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
<!-- sponsor 专属: 通过 biz_person.user_id 关联 sys_user, 利用 sys_user.parent_user_id 过滤主账号归属 -->
|
<!-- sponsor 专属: SQL 硬编码 unit_type='sponsor' + 严格按 org_id 圈本机构所有用户 (含主账号自己) -->
|
||||||
<!-- 主账号自己 (parent_user_id=NULL) 也通过 OR u.user_id=#{sponsorOwnerUid} 一并带上 (否则主账号看不到自己) -->
|
|
||||||
<select id="selectSponsorList" resultMap="BizPersonResult" parameterType="BizPerson">
|
<select id="selectSponsorList" resultMap="BizPersonResult" parameterType="BizPerson">
|
||||||
<include refid="selectFieldsWithAccount"/>
|
<include refid="selectFieldsWithAccount"/>
|
||||||
<where>
|
<where>
|
||||||
u.del_flag = '0'
|
u.del_flag = '0'
|
||||||
and p.unit_type = 'sponsor'
|
and p.unit_type = 'sponsor'
|
||||||
and (u.parent_user_id = #{params.sponsorOwnerUid}
|
and p.org_id = #{params.sponsorOrgId}
|
||||||
or u.user_id = #{params.sponsorOwnerUid})
|
|
||||||
<if test="name != null and name != ''"> and p.name like concat('%', #{name}, '%')</if>
|
<if test="name != null and name != ''"> and p.name like concat('%', #{name}, '%')</if>
|
||||||
<if test="phone != null and phone != ''"> and p.phone = #{phone}</if>
|
<if test="phone != null and phone != ''"> and p.phone = #{phone}</if>
|
||||||
<if test="orgId != null"> and p.org_id = #{orgId}</if>
|
<if test="orgId != null"> and p.org_id = #{orgId}</if>
|
||||||
@@ -165,15 +167,13 @@
|
|||||||
order by p.person_id desc
|
order by p.person_id desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- executor 专属: 同 sponsor, SQL 硬编码 unit_type='executor' + parent_user_id=当前主账号 (前端绕不开) -->
|
<!-- executor 专属: SQL 硬编码 unit_type='executor' + 严格按 org_id 圈本机构所有用户 (含主账号自己) -->
|
||||||
<!-- 主账号自己 (parent_user_id=NULL) 也通过 OR u.user_id=#{executorOwnerUid} 一并带上 (否则主账号看不到自己) -->
|
|
||||||
<select id="selectExecutorList" resultMap="BizPersonResult" parameterType="BizPerson">
|
<select id="selectExecutorList" resultMap="BizPersonResult" parameterType="BizPerson">
|
||||||
<include refid="selectFieldsWithAccount"/>
|
<include refid="selectFieldsWithAccount"/>
|
||||||
<where>
|
<where>
|
||||||
u.del_flag = '0'
|
u.del_flag = '0'
|
||||||
and p.unit_type = 'executor'
|
and p.unit_type = 'executor'
|
||||||
and (u.parent_user_id = #{params.executorOwnerUid}
|
and p.org_id = #{params.executorOrgId}
|
||||||
or u.user_id = #{params.executorOwnerUid})
|
|
||||||
<if test="name != null and name != ''"> and p.name like concat('%', #{name}, '%')</if>
|
<if test="name != null and name != ''"> and p.name like concat('%', #{name}, '%')</if>
|
||||||
<if test="phone != null and phone != ''"> and p.phone = #{phone}</if>
|
<if test="phone != null and phone != ''"> and p.phone = #{phone}</if>
|
||||||
<if test="orgId != null"> and p.org_id = #{orgId}</if>
|
<if test="orgId != null"> and p.org_id = #{orgId}</if>
|
||||||
@@ -185,4 +185,21 @@
|
|||||||
order by p.person_id desc
|
order by p.person_id desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 按 user_id 反查人员档案 (个人资料编辑: 拿 personId 再走 updateByPrimaryKey) -->
|
||||||
|
<select id="selectByUserId" resultMap="BizPersonResult" parameterType="Long">
|
||||||
|
<include refid="selectFields"/>
|
||||||
|
where user_id = #{userId}
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 按 org_id 反查该机构主账号 user_id: biz_person.org_id 绑定 + sys_user.account_type='MAIN', 不依赖 biz_org.user_id -->
|
||||||
|
<select id="selectMainUserIdByOrgId" resultType="java.lang.Long" parameterType="Long">
|
||||||
|
select u.user_id
|
||||||
|
from biz_person p
|
||||||
|
join sys_user u on u.user_id = p.user_id
|
||||||
|
where p.org_id = #{orgId} and u.account_type = 'MAIN' and u.del_flag = '0'
|
||||||
|
order by u.user_id
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
+4
-2
@@ -35,12 +35,14 @@
|
|||||||
|
|
||||||
<select id="selectByProjectId" resultMap="BaseResultMap">
|
<select id="selectByProjectId" resultMap="BaseResultMap">
|
||||||
SELECT a.*,
|
SELECT a.*,
|
||||||
e.user_name AS executor_user_name,
|
COALESCE(NULLIF(ep.name, ''), e.nick_name) AS executor_user_name,
|
||||||
s.user_name AS staff_user_name
|
COALESCE(NULLIF(sp.name, ''), s.nick_name) AS staff_user_name
|
||||||
FROM biz_project_executor_assign a
|
FROM biz_project_executor_assign a
|
||||||
LEFT JOIN biz_org o ON o.org_id = a.executor_org_id
|
LEFT JOIN biz_org o ON o.org_id = a.executor_org_id
|
||||||
LEFT JOIN sys_user e ON e.user_id = o.user_id
|
LEFT JOIN sys_user e ON e.user_id = o.user_id
|
||||||
|
LEFT JOIN biz_person ep ON ep.user_id = o.user_id
|
||||||
LEFT JOIN sys_user s ON a.staff_user_id = s.user_id
|
LEFT JOIN sys_user s ON a.staff_user_id = s.user_id
|
||||||
|
LEFT JOIN biz_person sp ON sp.user_id = a.staff_user_id
|
||||||
WHERE a.project_id = #{projectId} and a.is_deleted = 0
|
WHERE a.project_id = #{projectId} and a.is_deleted = 0
|
||||||
ORDER BY a.create_time DESC
|
ORDER BY a.create_time DESC
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -51,7 +51,15 @@
|
|||||||
<result property="isDeleted" column="is_deleted" />
|
<result property="isDeleted" column="is_deleted" />
|
||||||
</resultMap>
|
</resultMap>
|
||||||
<sql id="selectFields">
|
<sql id="selectFields">
|
||||||
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount,
|
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.total_amount,
|
||||||
|
<!-- 已执行会议数: stage 走过 NOT_STARTED/IN_PROGRESS 就算 (含 FROZEN 已结算异常态) -->
|
||||||
|
(select count(*) from biz_meeting m
|
||||||
|
where m.project_id = p.project_id and m.is_deleted = 0
|
||||||
|
and m.current_stage not in ('NOT_STARTED','IN_PROGRESS')) as done_sessions,
|
||||||
|
<!-- 未执行会议数: 仅 NOT_STARTED -->
|
||||||
|
(select count(*) from biz_meeting m
|
||||||
|
where m.project_id = p.project_id and m.is_deleted = 0
|
||||||
|
and m.current_stage = 'NOT_STARTED') as todo_sessions,
|
||||||
(select ifnull(sum(m.labor_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0) as paid_labor_amount,
|
(select ifnull(sum(m.labor_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0) as paid_labor_amount,
|
||||||
(select ifnull(sum(m.meeting_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0) as paid_meeting_amount,
|
(select ifnull(sum(m.meeting_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0) as paid_meeting_amount,
|
||||||
(p.total_amount - ifnull(p.manage_fee, 0)
|
(p.total_amount - ifnull(p.manage_fee, 0)
|
||||||
@@ -59,8 +67,8 @@
|
|||||||
- (select ifnull(sum(m.meeting_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0)) as available_amount,
|
- (select ifnull(sum(m.meeting_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0)) as available_amount,
|
||||||
p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.is_published, p.publish_time, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url, p.schedule_url, p.open_deadline, p.open_status, p.is_deleted,
|
p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.is_published, p.publish_time, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url, p.schedule_url, p.open_deadline, p.open_status, p.is_deleted,
|
||||||
o.org_name as sponsor_org_name,
|
o.org_name as sponsor_org_name,
|
||||||
su.user_name as sponsor_admin_user_name,
|
COALESCE(NULLIF(sup.name, ''), su.nick_name) as sponsor_admin_user_name,
|
||||||
lu.user_name as lead_user_name,
|
COALESCE(NULLIF(lup.name, ''), lu.nick_name) as lead_user_name,
|
||||||
bp.name as create_user_name,
|
bp.name as create_user_name,
|
||||||
(select group_concat(distinct o2.org_name separator ',')
|
(select group_concat(distinct o2.org_name separator ',')
|
||||||
from biz_project_assign bpa
|
from biz_project_assign bpa
|
||||||
@@ -70,13 +78,21 @@
|
|||||||
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
||||||
left join sys_user su on su.user_id = o.user_id
|
left join sys_user su on su.user_id = o.user_id
|
||||||
left join sys_user lu on lu.user_id = p.lead_user_id
|
left join sys_user lu on lu.user_id = p.lead_user_id
|
||||||
|
left join biz_person sup on sup.user_id = o.user_id
|
||||||
|
left join biz_person lup on lup.user_id = p.lead_user_id
|
||||||
left join biz_person bp on bp.user_id = p.create_user_id
|
left join biz_person bp on bp.user_id = p.create_user_id
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<!-- sponsor 端专属查询: 与 selectFields 等价 (sponsor 评分改走 biz_project_rating 子表, 这里只查项目本体) -->
|
<!-- sponsor 端专属查询: 与 selectFields 等价 (sponsor 评分改走 biz_project_rating 子表, 这里只查项目本体) -->
|
||||||
<sql id="selectFieldsForSponsor">
|
<sql id="selectFieldsForSponsor">
|
||||||
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions,
|
select p.project_id, p.project_no, p.project_name, p.total_sessions,
|
||||||
p.total_amount,
|
p.total_amount,
|
||||||
|
(select count(*) from biz_meeting m
|
||||||
|
where m.project_id = p.project_id and m.is_deleted = 0
|
||||||
|
and m.current_stage not in ('NOT_STARTED','IN_PROGRESS')) as done_sessions,
|
||||||
|
(select count(*) from biz_meeting m
|
||||||
|
where m.project_id = p.project_id and m.is_deleted = 0
|
||||||
|
and m.current_stage = 'NOT_STARTED') as todo_sessions,
|
||||||
(select ifnull(sum(m.labor_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0) as paid_labor_amount,
|
(select ifnull(sum(m.labor_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0) as paid_labor_amount,
|
||||||
(select ifnull(sum(m.meeting_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0) as paid_meeting_amount,
|
(select ifnull(sum(m.meeting_fee), 0) from biz_meeting m where m.project_id = p.project_id and m.is_settled = 1 and m.is_deleted = 0) as paid_meeting_amount,
|
||||||
(p.total_amount - ifnull(p.manage_fee, 0)
|
(p.total_amount - ifnull(p.manage_fee, 0)
|
||||||
@@ -89,8 +105,8 @@
|
|||||||
p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url,
|
p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url,
|
||||||
p.publish_url, p.schedule_url, p.open_deadline, p.open_status, p.is_deleted,
|
p.publish_url, p.schedule_url, p.open_deadline, p.open_status, p.is_deleted,
|
||||||
o.org_name as sponsor_org_name,
|
o.org_name as sponsor_org_name,
|
||||||
su.user_name as sponsor_admin_user_name,
|
COALESCE(NULLIF(sup.name, ''), su.nick_name) as sponsor_admin_user_name,
|
||||||
lu.user_name as lead_user_name,
|
COALESCE(NULLIF(lup.name, ''), lu.nick_name) as lead_user_name,
|
||||||
bp.name as create_user_name,
|
bp.name as create_user_name,
|
||||||
(select group_concat(distinct o2.org_name separator ',')
|
(select group_concat(distinct o2.org_name separator ',')
|
||||||
from biz_project_assign bpa
|
from biz_project_assign bpa
|
||||||
@@ -100,6 +116,8 @@
|
|||||||
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
||||||
left join sys_user su on su.user_id = o.user_id
|
left join sys_user su on su.user_id = o.user_id
|
||||||
left join sys_user lu on lu.user_id = p.lead_user_id
|
left join sys_user lu on lu.user_id = p.lead_user_id
|
||||||
|
left join biz_person sup on sup.user_id = o.user_id
|
||||||
|
left join biz_person lup on lup.user_id = p.lead_user_id
|
||||||
left join biz_person bp on bp.user_id = p.create_user_id
|
left join biz_person bp on bp.user_id = p.create_user_id
|
||||||
</sql>
|
</sql>
|
||||||
<select id="selectByPrimaryKey" resultMap="BizProjectResult" parameterType="Long">
|
<select id="selectByPrimaryKey" resultMap="BizProjectResult" parameterType="Long">
|
||||||
@@ -150,6 +168,7 @@
|
|||||||
<if test="startTime != null">and p.start_time >= #{startTime}</if>
|
<if test="startTime != null">and p.start_time >= #{startTime}</if>
|
||||||
<if test="endTime != null">and p.end_time <= #{endTime}</if>
|
<if test="endTime != null">and p.end_time <= #{endTime}</if>
|
||||||
<if test="isFinished != null and isFinished != ''">and p.is_finished = #{isFinished}</if>
|
<if test="isFinished != null and isFinished != ''">and p.is_finished = #{isFinished}</if>
|
||||||
|
<if test="isSettled != null and isSettled != ''">and p.is_settled = #{isSettled}</if>
|
||||||
</where>
|
</where>
|
||||||
order by p.project_id desc
|
order by p.project_id desc
|
||||||
</select>
|
</select>
|
||||||
@@ -164,10 +183,10 @@
|
|||||||
biz_meeting.execution_unit_id = 本执行方 org 过滤, 与项目级 total_amount/available_amount 无关.
|
biz_meeting.execution_unit_id = 本执行方 org 过滤, 与项目级 total_amount/available_amount 无关.
|
||||||
-->
|
-->
|
||||||
<select id="selectExecutorList" resultMap="BizProjectResult" parameterType="BizProject">
|
<select id="selectExecutorList" resultMap="BizProjectResult" parameterType="BizProject">
|
||||||
select distinct p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.is_published, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url,
|
select distinct p.project_id, p.project_no, p.project_name, p.total_sessions, p.total_amount, p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.is_published, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url,
|
||||||
o.org_name as sponsor_org_name,
|
o.org_name as sponsor_org_name,
|
||||||
su.user_name as sponsor_admin_user_name,
|
COALESCE(NULLIF(sup.name, ''), su.nick_name) as sponsor_admin_user_name,
|
||||||
lu.user_name as lead_user_name,
|
COALESCE(NULLIF(lup.name, ''), lu.nick_name) as lead_user_name,
|
||||||
bp.name as create_user_name,
|
bp.name as create_user_name,
|
||||||
(select group_concat(distinct o2.org_name separator ',')
|
(select group_concat(distinct o2.org_name separator ',')
|
||||||
from biz_project_assign bpa2
|
from biz_project_assign bpa2
|
||||||
@@ -213,7 +232,17 @@
|
|||||||
and m.is_deleted = 0
|
and m.is_deleted = 0
|
||||||
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as available_amount,
|
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as available_amount,
|
||||||
(select count(*) from biz_meeting m where m.project_id = p.project_id and m.is_deleted = 0
|
(select count(*) from biz_meeting m where m.project_id = p.project_id and m.is_deleted = 0
|
||||||
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as meeting_count
|
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as meeting_count,
|
||||||
|
<!-- 执行方级已执行: 同上 execution_unit_id 隔离, FROZEN 算已执行 -->
|
||||||
|
(select count(*) from biz_meeting m
|
||||||
|
where m.project_id = p.project_id and m.is_deleted = 0
|
||||||
|
and m.current_stage not in ('NOT_STARTED','IN_PROGRESS')
|
||||||
|
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as done_sessions,
|
||||||
|
<!-- 执行方级未执行 -->
|
||||||
|
(select count(*) from biz_meeting m
|
||||||
|
where m.project_id = p.project_id and m.is_deleted = 0
|
||||||
|
and m.current_stage = 'NOT_STARTED'
|
||||||
|
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as todo_sessions
|
||||||
from biz_project p
|
from biz_project p
|
||||||
<!-- 执行方专属 join: 把 executor 限定条件放进 ON (而不是 WHERE), 这样 join 只命中分给当前执行方的 assignment, 1 行/项目. SELECT DISTINCT 保留以防 LEFT JOIN 副作用 -->
|
<!-- 执行方专属 join: 把 executor 限定条件放进 ON (而不是 WHERE), 这样 join 只命中分给当前执行方的 assignment, 1 行/项目. SELECT DISTINCT 保留以防 LEFT JOIN 副作用 -->
|
||||||
join biz_project_assign a on a.project_id = p.project_id
|
join biz_project_assign a on a.project_id = p.project_id
|
||||||
@@ -222,6 +251,8 @@
|
|||||||
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
||||||
left join sys_user su on su.user_id = o.user_id
|
left join sys_user su on su.user_id = o.user_id
|
||||||
left join sys_user lu on lu.user_id = p.lead_user_id
|
left join sys_user lu on lu.user_id = p.lead_user_id
|
||||||
|
left join biz_person sup on sup.user_id = o.user_id
|
||||||
|
left join biz_person lup on lup.user_id = p.lead_user_id
|
||||||
left join biz_person bp on bp.user_id = p.create_user_id
|
left join biz_person bp on bp.user_id = p.create_user_id
|
||||||
<where>
|
<where>
|
||||||
p.is_deleted = 0
|
p.is_deleted = 0
|
||||||
@@ -230,6 +261,7 @@
|
|||||||
<if test="startTime != null">and p.start_time >= #{startTime}</if>
|
<if test="startTime != null">and p.start_time >= #{startTime}</if>
|
||||||
<if test="endTime != null">and p.end_time <= #{endTime}</if>
|
<if test="endTime != null">and p.end_time <= #{endTime}</if>
|
||||||
<if test="isFinished != null and isFinished != ''">and p.is_finished = #{isFinished}</if>
|
<if test="isFinished != null and isFinished != ''">and p.is_finished = #{isFinished}</if>
|
||||||
|
<if test="isSettled != null and isSettled != ''">and p.is_settled = #{isSettled}</if>
|
||||||
</where>
|
</where>
|
||||||
order by p.project_id desc
|
order by p.project_id desc
|
||||||
</select>
|
</select>
|
||||||
@@ -242,10 +274,10 @@
|
|||||||
已支付劳务/会务/可用金额同样按 biz_meeting.execution_unit_id = 本公司 org 过滤 (执行方级).
|
已支付劳务/会务/可用金额同样按 biz_meeting.execution_unit_id = 本公司 org 过滤 (执行方级).
|
||||||
-->
|
-->
|
||||||
<select id="selectExecutorStaffList" resultMap="BizProjectResult" parameterType="BizProject">
|
<select id="selectExecutorStaffList" resultMap="BizProjectResult" parameterType="BizProject">
|
||||||
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions, p.total_amount, p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.is_published, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url,
|
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.total_amount, p.manager_score, p.sponsor_score, p.project_form, p.is_finished, p.is_settled, p.is_published, p.sponsor_org_id, p.lead_user_id, p.is_bid_project, p.create_by, p.create_user_id, p.create_time, p.update_by, p.update_time, p.manage_fee, p.role_labor, p.start_time, p.end_time, p.submit_deadline_days, p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url, p.publish_url,
|
||||||
o.org_name as sponsor_org_name,
|
o.org_name as sponsor_org_name,
|
||||||
su.user_name as sponsor_admin_user_name,
|
COALESCE(NULLIF(sup.name, ''), su.nick_name) as sponsor_admin_user_name,
|
||||||
lu.user_name as lead_user_name,
|
COALESCE(NULLIF(lup.name, ''), lu.nick_name) as lead_user_name,
|
||||||
bp.name as create_user_name,
|
bp.name as create_user_name,
|
||||||
(select group_concat(distinct o2.org_name separator ',')
|
(select group_concat(distinct o2.org_name separator ',')
|
||||||
from biz_project_assign bpa2
|
from biz_project_assign bpa2
|
||||||
@@ -291,11 +323,22 @@
|
|||||||
and m.is_deleted = 0
|
and m.is_deleted = 0
|
||||||
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as available_amount,
|
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as available_amount,
|
||||||
(select count(*) from biz_meeting m where m.project_id = p.project_id and m.is_deleted = 0
|
(select count(*) from biz_meeting m where m.project_id = p.project_id and m.is_deleted = 0
|
||||||
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as meeting_count
|
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as meeting_count,
|
||||||
|
<!-- 执行人视角: 仍按主账号 org_id 聚合 (执行人看公司数据不是个人), FROZEN 算已执行 -->
|
||||||
|
(select count(*) from biz_meeting m
|
||||||
|
where m.project_id = p.project_id and m.is_deleted = 0
|
||||||
|
and m.current_stage not in ('NOT_STARTED','IN_PROGRESS')
|
||||||
|
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as done_sessions,
|
||||||
|
(select count(*) from biz_meeting m
|
||||||
|
where m.project_id = p.project_id and m.is_deleted = 0
|
||||||
|
and m.current_stage = 'NOT_STARTED'
|
||||||
|
and m.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')) as todo_sessions
|
||||||
from biz_project p
|
from biz_project p
|
||||||
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
left join biz_org o on o.org_id = p.sponsor_org_id and o.org_type = 'sponsor'
|
||||||
left join sys_user su on su.user_id = o.user_id
|
left join sys_user su on su.user_id = o.user_id
|
||||||
left join sys_user lu on lu.user_id = p.lead_user_id
|
left join sys_user lu on lu.user_id = p.lead_user_id
|
||||||
|
left join biz_person sup on sup.user_id = o.user_id
|
||||||
|
left join biz_person lup on lup.user_id = p.lead_user_id
|
||||||
left join biz_person bp on bp.user_id = p.create_user_id
|
left join biz_person bp on bp.user_id = p.create_user_id
|
||||||
<where>
|
<where>
|
||||||
p.is_deleted = 0
|
p.is_deleted = 0
|
||||||
@@ -309,6 +352,7 @@
|
|||||||
<if test="startTime != null">and p.start_time >= #{startTime}</if>
|
<if test="startTime != null">and p.start_time >= #{startTime}</if>
|
||||||
<if test="endTime != null">and p.end_time <= #{endTime}</if>
|
<if test="endTime != null">and p.end_time <= #{endTime}</if>
|
||||||
<if test="isFinished != null and isFinished != ''">and p.is_finished = #{isFinished}</if>
|
<if test="isFinished != null and isFinished != ''">and p.is_finished = #{isFinished}</if>
|
||||||
|
<if test="isSettled != null and isSettled != ''">and p.is_settled = #{isSettled}</if>
|
||||||
</where>
|
</where>
|
||||||
order by p.project_id desc
|
order by p.project_id desc
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<result property="createTime" column="create_time" />
|
<result property="createTime" column="create_time" />
|
||||||
<result property="updateBy" column="update_by" />
|
<result property="updateBy" column="update_by" />
|
||||||
<result property="updateTime" column="update_time" />
|
<result property="updateTime" column="update_time" />
|
||||||
|
<result property="submitTime" column="submit_time" />
|
||||||
<result property="isDeleted" column="is_deleted" />
|
<result property="isDeleted" column="is_deleted" />
|
||||||
</resultMap>
|
</resultMap>
|
||||||
<sql id="selectFields">
|
<sql id="selectFields">
|
||||||
@@ -32,9 +33,9 @@
|
|||||||
s.title as plan_direction_title,
|
s.title as plan_direction_title,
|
||||||
p.plan_category, p.project_form, p.design_file_url, p.subject_direction, p.status, p.is_settled,
|
p.plan_category, p.project_form, p.design_file_url, p.subject_direction, p.status, p.is_settled,
|
||||||
p.project_no, p.remark, p.audit_opinion, p.audit_by, p.audit_time, p.submitter_id,
|
p.project_no, p.remark, p.audit_opinion, p.audit_by, p.audit_time, p.submitter_id,
|
||||||
COALESCE(bp.name, u.user_name) as submitter_name,
|
COALESCE(bp.name, u.nick_name) as submitter_name,
|
||||||
proj.project_name as project_name,
|
proj.project_name as project_name,
|
||||||
p.create_by, p.create_time, p.update_by, p.update_time, p.is_deleted
|
p.create_by, p.create_time, p.update_by, p.update_time, p.submit_time, p.is_deleted
|
||||||
from biz_project_plan p
|
from biz_project_plan p
|
||||||
left join biz_special_plan s on s.id = p.plan_direction_id
|
left join biz_special_plan s on s.id = p.plan_direction_id
|
||||||
left join sys_user u on u.user_id = p.submitter_id
|
left join sys_user u on u.user_id = p.submitter_id
|
||||||
@@ -49,6 +50,8 @@
|
|||||||
<include refid="selectFields"/>
|
<include refid="selectFields"/>
|
||||||
<where>
|
<where>
|
||||||
p.is_deleted = 0
|
p.is_deleted = 0
|
||||||
|
<!-- 管理角色 (admin/manager): 未提交草稿 status='0' 不进列表 (controller 注入 params.excludeDraft) -->
|
||||||
|
<if test="params.excludeDraft == true"> and p.status != '0'</if>
|
||||||
<if test="planName != null and planName != ''"> and p.plan_name like concat('%', #{planName}, '%')</if>
|
<if test="planName != null and planName != ''"> and p.plan_name like concat('%', #{planName}, '%')</if>
|
||||||
<if test="planDirectionId != null"> and p.plan_direction_id = #{planDirectionId}</if>
|
<if test="planDirectionId != null"> and p.plan_direction_id = #{planDirectionId}</if>
|
||||||
<if test="planCategory != null and planCategory != ''"> and p.plan_category = #{planCategory}</if>
|
<if test="planCategory != null and planCategory != ''"> and p.plan_category = #{planCategory}</if>
|
||||||
@@ -57,11 +60,11 @@
|
|||||||
<choose>
|
<choose>
|
||||||
<!-- 选了具体状态: 等值匹配 -->
|
<!-- 选了具体状态: 等值匹配 -->
|
||||||
<when test="status != null and status != ''"> and p.status = #{status}</when>
|
<when test="status != null and status != ''"> and p.status = #{status}</when>
|
||||||
<!-- 未选: 不加 status 过滤, 由前端按角色决定默认值 (经理侧默认查 1/2/3, 医生侧默认查全部含 0) -->
|
<!-- 未选: 不加 status 等值过滤; 未提交草稿 '0' 已由上方 params.excludeDraft 对管理角色排除 -->
|
||||||
</choose>
|
</choose>
|
||||||
<if test="remark != null and remark != ''"> and p.remark like concat('%', #{remark}, '%')</if>
|
<if test="remark != null and remark != ''"> and p.remark like concat('%', #{remark}, '%')</if>
|
||||||
</where>
|
</where>
|
||||||
order by p.plan_id desc
|
order by (p.submit_time is null), p.submit_time desc, p.plan_id desc
|
||||||
</select>
|
</select>
|
||||||
<insert id="insert" parameterType="BizProjectPlan">
|
<insert id="insert" parameterType="BizProjectPlan">
|
||||||
insert into biz_project_plan
|
insert into biz_project_plan
|
||||||
@@ -79,6 +82,8 @@
|
|||||||
<if test="projectNo != null and projectNo != ''">project_no,</if>
|
<if test="projectNo != null and projectNo != ''">project_no,</if>
|
||||||
<if test="remark != null">remark,</if>
|
<if test="remark != null">remark,</if>
|
||||||
<if test="submitterId != null">submitter_id,</if>
|
<if test="submitterId != null">submitter_id,</if>
|
||||||
|
<if test="createBy != null">create_by,</if>
|
||||||
|
<if test="createTime != null">create_time,</if>
|
||||||
</trim>
|
</trim>
|
||||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||||
<if test="planId != null and planId != ''">#{planId},</if>
|
<if test="planId != null and planId != ''">#{planId},</if>
|
||||||
@@ -94,6 +99,8 @@
|
|||||||
<if test="projectNo != null and projectNo != ''">#{projectNo},</if>
|
<if test="projectNo != null and projectNo != ''">#{projectNo},</if>
|
||||||
<if test="remark != null">#{remark},</if>
|
<if test="remark != null">#{remark},</if>
|
||||||
<if test="submitterId != null">#{submitterId},</if>
|
<if test="submitterId != null">#{submitterId},</if>
|
||||||
|
<if test="createBy != null">#{createBy},</if>
|
||||||
|
<if test="createTime != null">#{createTime},</if>
|
||||||
</trim>
|
</trim>
|
||||||
</insert>
|
</insert>
|
||||||
<update id="updateByPrimaryKey" parameterType="BizProjectPlan">
|
<update id="updateByPrimaryKey" parameterType="BizProjectPlan">
|
||||||
@@ -114,6 +121,7 @@
|
|||||||
<if test="status != null">status = #{status},</if>
|
<if test="status != null">status = #{status},</if>
|
||||||
<if test="remark != null">remark = #{remark},</if>
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
<if test="submitterId != null">submitter_id = #{submitterId},</if>
|
<if test="submitterId != null">submitter_id = #{submitterId},</if>
|
||||||
|
<if test="submitTime != null">submit_time = #{submitTime},</if>
|
||||||
</trim>
|
</trim>
|
||||||
where plan_id = #{planId}
|
where plan_id = #{planId}
|
||||||
</update>
|
</update>
|
||||||
|
|||||||
+5
-3
@@ -35,12 +35,14 @@
|
|||||||
|
|
||||||
<select id="selectByProjectId" resultMap="BaseResultMap">
|
<select id="selectByProjectId" resultMap="BaseResultMap">
|
||||||
SELECT a.*,
|
SELECT a.*,
|
||||||
s.user_name AS sponsor_user_name,
|
COALESCE(NULLIF(sp.name, ''), s.nick_name) AS sponsor_user_name,
|
||||||
m.user_name AS monitor_user_name
|
COALESCE(NULLIF(p.name, ''), m.nick_name) AS monitor_user_name
|
||||||
FROM biz_project_sponsor_assign a
|
FROM biz_project_sponsor_assign a
|
||||||
LEFT JOIN biz_org o ON o.org_id = a.sponsor_org_id
|
LEFT JOIN biz_org o ON o.org_id = a.sponsor_org_id
|
||||||
LEFT JOIN sys_user s ON s.user_id = o.user_id
|
LEFT JOIN sys_user s ON s.user_id = o.user_id
|
||||||
LEFT JOIN sys_user m ON a.monitor_user_id = m.user_id
|
LEFT JOIN biz_person sp ON sp.user_id = o.user_id
|
||||||
|
LEFT JOIN biz_person p ON p.user_id = a.monitor_user_id
|
||||||
|
INNER JOIN sys_user m ON a.monitor_user_id = m.user_id
|
||||||
WHERE a.project_id = #{projectId} and a.is_deleted = 0
|
WHERE a.project_id = #{projectId} and a.is_deleted = 0
|
||||||
ORDER BY a.create_time DESC
|
ORDER BY a.create_time DESC
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ public class SysUser extends BaseEntity
|
|||||||
/** 密码 */
|
/** 密码 */
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
|
/** 供应商同步密码 (BCrypt 哈希, 登录时优先于 password 校验) */
|
||||||
|
private String password2;
|
||||||
|
|
||||||
/** 账号状态(0正常 1停用) */
|
/** 账号状态(0正常 1停用) */
|
||||||
@Excel(name = "账号状态", readConverterExp = "0=正常,1=停用")
|
@Excel(name = "账号状态", readConverterExp = "0=正常,1=停用")
|
||||||
private String status;
|
private String status;
|
||||||
@@ -180,6 +183,15 @@ public class SysUser extends BaseEntity
|
|||||||
{
|
{
|
||||||
this.password = password;
|
this.password = password;
|
||||||
}
|
}
|
||||||
|
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||||
|
public String getPassword2()
|
||||||
|
{
|
||||||
|
return password2;
|
||||||
|
}
|
||||||
|
public void setPassword2(String password2)
|
||||||
|
{
|
||||||
|
this.password2 = password2;
|
||||||
|
}
|
||||||
public String getStatus()
|
public String getStatus()
|
||||||
{
|
{
|
||||||
return status;
|
return status;
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ package com.ruoyi.common.enums;
|
|||||||
* <pre>
|
* <pre>
|
||||||
* NOT_STARTED 未执行 会议开始时间前
|
* NOT_STARTED 未执行 会议开始时间前
|
||||||
* ↓ (过 startTime, scheduler)
|
* ↓ (过 startTime, scheduler)
|
||||||
* RUNNING 执行中 已开始, 执行方未提交材料
|
* IN_PROGRESS 执行中 会议进行中 (开始时间已到, 结束时间未到)
|
||||||
|
* ↓ (过 endTime, scheduler)
|
||||||
|
* RUNNING 已执行 会议已结束, 执行方未提交材料
|
||||||
* ↓ (执行方提交材料)
|
* ↓ (执行方提交材料)
|
||||||
* AWAITING_COMPLIANCE 待合规审核 执行方已提交, 等合规人员审核 (支持方此时只读, 不能审)
|
* AWAITING_COMPLIANCE 待合规审核 执行方已提交, 等合规人员审核 (支持方此时只读, 不能审)
|
||||||
* ↓ (合规审通过) ↘ (合规退回)
|
* ↓ (合规审通过) ↘ (合规退回)
|
||||||
@@ -41,8 +43,11 @@ public enum BizMeetingStageEnum
|
|||||||
/** 会议开始时间前 */
|
/** 会议开始时间前 */
|
||||||
NOT_STARTED("NOT_STARTED", "未执行", "会议开始时间前"),
|
NOT_STARTED("NOT_STARTED", "未执行", "会议开始时间前"),
|
||||||
|
|
||||||
/** 已开始, 执行方未提交材料 */
|
/** 会议进行中 (开始时间已到, 结束时间未到) */
|
||||||
RUNNING("RUNNING", "执行中", "已开始, 执行方未提交"),
|
IN_PROGRESS("IN_PROGRESS", "执行中", "会议进行中 (开始时间已到, 结束时间未到)"),
|
||||||
|
|
||||||
|
/** 会议已结束, 执行方未提交材料 */
|
||||||
|
RUNNING("RUNNING", "已执行", "会议已结束, 执行方未提交材料"),
|
||||||
|
|
||||||
/** 执行方已提交, 等合规人员审核 (支持方此阶段只读, 不可审) */
|
/** 执行方已提交, 等合规人员审核 (支持方此阶段只读, 不可审) */
|
||||||
AWAITING_COMPLIANCE("AWAITING_COMPLIANCE", "待合规审核", "执行方提交, 等合规审"),
|
AWAITING_COMPLIANCE("AWAITING_COMPLIANCE", "待合规审核", "执行方提交, 等合规审"),
|
||||||
|
|||||||
+6
@@ -70,6 +70,12 @@ public class UserDetailsServiceImpl implements UserDetailsService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 供应商同步账号: 优先用 password2 (供应商 BCrypt 哈希) 参与密码比对
|
||||||
|
if (StringUtils.isNotBlank(user.getPassword2()))
|
||||||
|
{
|
||||||
|
user.setPassword(user.getPassword2());
|
||||||
|
}
|
||||||
|
|
||||||
passwordService.validate(user);
|
passwordService.validate(user);
|
||||||
|
|
||||||
return createLoginUser(user);
|
return createLoginUser(user);
|
||||||
|
|||||||
@@ -61,6 +61,14 @@ public interface SysUserMapper
|
|||||||
*/
|
*/
|
||||||
public SysUser selectUserByEmail(String email);
|
public SysUser selectUserByEmail(String email);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过邮箱查询用户, 不过滤 del_flag (供应商账号同步去重用: 已软删的账号也要能查到以便恢复)
|
||||||
|
*
|
||||||
|
* @param email 邮箱
|
||||||
|
* @return 用户对象信息
|
||||||
|
*/
|
||||||
|
public SysUser selectUserByEmailIgnoreDel(String email);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过用户ID查询用户
|
* 通过用户ID查询用户
|
||||||
*
|
*
|
||||||
@@ -77,6 +85,14 @@ public interface SysUserMapper
|
|||||||
*/
|
*/
|
||||||
public int insertUser(SysUser user);
|
public int insertUser(SysUser user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增供应商同步用户 (写 password2/del_flag 等, 不写 password 列)
|
||||||
|
*
|
||||||
|
* @param user 用户信息
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public int insertSyncedUser(SysUser user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改用户信息
|
* 修改用户信息
|
||||||
*
|
*
|
||||||
@@ -85,6 +101,14 @@ public interface SysUserMapper
|
|||||||
*/
|
*/
|
||||||
public int updateUser(SysUser user);
|
public int updateUser(SysUser user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新供应商同步用户 (password2/nick_name/phonenumber/email/status/del_flag)
|
||||||
|
*
|
||||||
|
* @param user 用户信息
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public int updateSyncedUser(SysUser user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改用户头像
|
* 修改用户头像
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="sex" column="sex" />
|
<result property="sex" column="sex" />
|
||||||
<result property="avatar" column="avatar" />
|
<result property="avatar" column="avatar" />
|
||||||
<result property="password" column="password" />
|
<result property="password" column="password" />
|
||||||
|
<result property="password2" column="password2" />
|
||||||
<result property="status" column="status" />
|
<result property="status" column="status" />
|
||||||
<result property="delFlag" column="del_flag" />
|
<result property="delFlag" column="del_flag" />
|
||||||
<result property="loginIp" column="login_ip" />
|
<result property="loginIp" column="login_ip" />
|
||||||
@@ -57,7 +58,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectUserVo">
|
<sql id="selectUserVo">
|
||||||
select u.user_id, u.dept_id, u.user_name, u.nick_name, u.account_type, u.parent_user_id, u.role_type, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.update_by, u.update_time, u.remark,
|
select u.user_id, u.dept_id, u.user_name, u.nick_name, u.account_type, u.parent_user_id, u.role_type, u.email, u.avatar, u.phonenumber, u.password, u.password2, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.update_by, u.update_time, u.remark,
|
||||||
d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,
|
d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,
|
||||||
r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
|
r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
|
||||||
from sys_user u
|
from sys_user u
|
||||||
@@ -108,6 +109,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
from sys_user u
|
from sys_user u
|
||||||
left join sys_dept d on u.dept_id = d.dept_id
|
left join sys_dept d on u.dept_id = d.dept_id
|
||||||
where u.del_flag = '0'
|
where u.del_flag = '0'
|
||||||
|
<!-- 支持方/执行人: 单位不存在的过滤掉 (单位被硬删后 org_name 为 NULL, 孤儿账号不展示) -->
|
||||||
|
AND (
|
||||||
|
coalesce(u.role_type, '') not in ('sponsor', 'executor')
|
||||||
|
OR exists (select 1 from biz_org o where o.user_id = u.user_id)
|
||||||
|
OR exists (select 1 from biz_person p join biz_org o2 on o2.org_id = p.org_id where p.user_id = u.user_id)
|
||||||
|
)
|
||||||
<if test="userId != null and userId != 0">
|
<if test="userId != null and userId != 0">
|
||||||
AND u.user_id = #{userId}
|
AND u.user_id = #{userId}
|
||||||
</if>
|
</if>
|
||||||
@@ -350,4 +357,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
select user_id from sys_user where parent_user_id = #{parentUserId} and del_flag = '0'
|
select user_id from sys_user where parent_user_id = #{parentUserId} and del_flag = '0'
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectUserByEmailIgnoreDel" parameterType="String" resultMap="SysUserResult">
|
||||||
|
<include refid="selectUserVo"/>
|
||||||
|
where u.email = #{email}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 供应商账号同步: 新建执行方登录账号 (密码哈希写入 password2, 不写 password) -->
|
||||||
|
<insert id="insertSyncedUser" parameterType="SysUser" useGeneratedKeys="true" keyProperty="userId">
|
||||||
|
insert into sys_user(user_name, nick_name, email, phonenumber, password2, status, del_flag, account_type, role_type, create_time)
|
||||||
|
values(#{userName}, #{nickName}, #{email}, #{phonenumber}, #{password2}, #{status}, #{delFlag}, #{accountType}, #{roleType}, sysdate())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 供应商账号同步: 更新执行方登录账号 (含恢复/删除 del_flag) -->
|
||||||
|
<update id="updateSyncedUser" parameterType="SysUser">
|
||||||
|
update sys_user
|
||||||
|
set password2 = #{password2},
|
||||||
|
nick_name = #{nickName},
|
||||||
|
phonenumber = #{phonenumber},
|
||||||
|
email = #{email},
|
||||||
|
status = #{status},
|
||||||
|
del_flag = #{delFlag},
|
||||||
|
update_time = sysdate()
|
||||||
|
where user_id = #{userId}
|
||||||
|
</update>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
@@ -1,7 +1,7 @@
|
|||||||
/* =============================================================
|
/* =============================================================
|
||||||
品牌主题色(Brand Theme)— 低调绿色主调
|
品牌主题色(Brand Theme)— 低调绿色主调
|
||||||
------------------------------------------------------------
|
------------------------------------------------------------
|
||||||
主色:#15803D 低饱和深绿 (政务/卫健委稳重型, 不刺眼)
|
主色:#42a288 低饱和绿 (政务/卫健委稳重型, 不刺眼)
|
||||||
次色:#0E7490 青蓝 (保留原品牌青蓝, 用作次要强调/链接)
|
次色:#0E7490 青蓝 (保留原品牌青蓝, 用作次要强调/链接)
|
||||||
配色逻辑:
|
配色逻辑:
|
||||||
- 主色绿 = 品牌色 (按钮/标题/侧栏高亮/KPI)
|
- 主色绿 = 品牌色 (按钮/标题/侧栏高亮/KPI)
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* —— 主色:低调深绿 (政务/卫健委风格) —— */
|
/* —— 主色:低调深绿 (政务/卫健委风格) —— */
|
||||||
--brand-primary: #15803D;
|
--brand-primary: #42a288;
|
||||||
--brand-primary-deep: #0F5F2E; /* hover / active */
|
--brand-primary-deep: #0F5F2E; /* hover / active */
|
||||||
--brand-primary-darker: #073D1D; /* 深底 (登录页 banner / 关键装饰) */
|
--brand-primary-darker: #073D1D; /* 深底 (登录页 banner / 关键装饰) */
|
||||||
--brand-primary-text: #16A34A; /* 链接/文字绿 (中等可读) */
|
--brand-primary-text: #16A34A; /* 链接/文字绿 (中等可读) */
|
||||||
|
|||||||
@@ -41,6 +41,19 @@
|
|||||||
<li v-if="!rows.length" class="empty">{{ emptyText }}</li>
|
<li v-if="!rows.length" class="empty">{{ emptyText }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<!-- 分页 (pageable=true 时显示; 嵌入式小列表 pageable=false 隐藏) -->
|
||||||
|
<div v-if="pageable && total > 0" class="notice-pager">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page.pageNum"
|
||||||
|
v-model:page-size="page.pageSize"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="load"
|
||||||
|
@size-change="load"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="detailOpen"
|
v-model="detailOpen"
|
||||||
:title="detail.title || '消息详情'"
|
:title="detail.title || '消息详情'"
|
||||||
@@ -70,7 +83,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
import { listMyMessages, markMessageRead, markAllMessagesRead } from '@/api/public'
|
import { listMyMessages, markMessageRead, markAllMessagesRead } from '@/api/public'
|
||||||
@@ -80,10 +93,18 @@ const props = defineProps({
|
|||||||
limit: { type: Number, default: 5 },
|
limit: { type: Number, default: 5 },
|
||||||
showCategory: { type: Boolean, default: false },
|
showCategory: { type: Boolean, default: false },
|
||||||
showHeader: { type: Boolean, default: true },
|
showHeader: { type: Boolean, default: true },
|
||||||
emptyText: { type: String, default: '暂无通知' }
|
emptyText: { type: String, default: '暂无通知' },
|
||||||
|
/**
|
||||||
|
* 是否启用分页
|
||||||
|
* - false (默认): 嵌入式小列表, 拉 props.limit 条, 不显示 el-pagination
|
||||||
|
* - true: 独立页, 显示 el-pagination, 按 page.pageSize 拉, total 读后端真实值
|
||||||
|
*/
|
||||||
|
pageable: { type: Boolean, default: false }
|
||||||
})
|
})
|
||||||
|
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = reactive({ pageNum: 1, pageSize: 20 })
|
||||||
const detailOpen = ref(false)
|
const detailOpen = ref(false)
|
||||||
const detail = ref({})
|
const detail = ref({})
|
||||||
const detailLink = ref('')
|
const detailLink = ref('')
|
||||||
@@ -94,18 +115,29 @@ let unsubscribeNewMessage = null
|
|||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const r = await listMyMessages({ limit: props.limit })
|
// 分页模式: 传 pageNum/pageSize (后端返回真实 total); 非分页模式: 传 limit
|
||||||
|
const params = props.pageable
|
||||||
|
? { pageNum: page.pageNum, pageSize: page.pageSize }
|
||||||
|
: { limit: props.limit }
|
||||||
|
const r = await listMyMessages(params)
|
||||||
rows.value = r.rows || []
|
rows.value = r.rows || []
|
||||||
|
total.value = r.total || 0
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
rows.value = []
|
rows.value = []
|
||||||
|
total.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** SSE 触发的静默重拉 (失败不打扰用户, 下次手动刷新可见) */
|
/** SSE 触发的静默重拉 (失败不打扰用户, 下次手动刷新可见)
|
||||||
|
* 分页模式: 重拉当前页; 非分页模式: 仍按 limit 拉 */
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
try {
|
try {
|
||||||
const r = await listMyMessages({ limit: props.limit })
|
const params = props.pageable
|
||||||
|
? { pageNum: page.pageNum, pageSize: page.pageSize }
|
||||||
|
: { limit: props.limit }
|
||||||
|
const r = await listMyMessages(params)
|
||||||
rows.value = r.rows || []
|
rows.value = r.rows || []
|
||||||
|
total.value = r.total || 0
|
||||||
} catch (e) { /* swallow */ }
|
} catch (e) { /* swallow */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,6 +292,13 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 分页 (独立页用, 嵌入式不显示) */
|
||||||
|
.notice-pager {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 详情 dialog */
|
/* 详情 dialog */
|
||||||
.detail-body .meta {
|
.detail-body .meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -64,7 +64,12 @@ function goPublicity() { router.push('/publicity') }
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.container { max-width: 1354px; margin: 0 auto; padding: 0 60px; }
|
.container { max-width: 1354px; margin: 0 auto; padding: 0 60px; }
|
||||||
.footer { background: var(--brand-primary-darker); color: rgba(255, 255, 255, 0.65); padding: 48px 0 0; }
|
.footer {
|
||||||
|
/* 保持 brand-primary 色调, 叠一层 18% 黑色遮罩让整体变暗, 不改色相 */
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.18)), var(--brand-primary);
|
||||||
|
color: #fff;
|
||||||
|
padding: 48px 0 0;
|
||||||
|
}
|
||||||
.footer-main { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1.2fr auto; gap: 48px; padding-bottom: 36px; }
|
.footer-main { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1.2fr auto; gap: 48px; padding-bottom: 36px; }
|
||||||
.footer-brand .brand-row { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
.footer-brand .brand-row { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||||
.footer-logo-icon {
|
.footer-logo-icon {
|
||||||
@@ -74,8 +79,8 @@ function goPublicity() { router.push('/publicity') }
|
|||||||
}
|
}
|
||||||
.footer-logo-icon img { width: 100%; height: 100%; object-fit: contain; display: block; }
|
.footer-logo-icon img { width: 100%; height: 100%; object-fit: contain; display: block; }
|
||||||
.footer-brand-name { font-size: 16px; font-weight: 600; color: #fff; letter-spacing: 1px; }
|
.footer-brand-name { font-size: 16px; font-weight: 600; color: #fff; letter-spacing: 1px; }
|
||||||
.footer-brand-en { font-size: 10px; color: rgba(255, 255, 255, 0.4); letter-spacing: 0.5px; margin-top: 2px; }
|
.footer-brand-en { font-size: 10px; color: #fff; letter-spacing: 0.5px; margin-top: 2px; }
|
||||||
.footer-desc { font-size: 13px; line-height: 1.9; color: rgba(255, 255, 255, 0.55); }
|
.footer-desc { font-size: 13px; line-height: 1.9; color: #fff; }
|
||||||
.footer-col h4 {
|
.footer-col h4 {
|
||||||
font-size: 14px; font-weight: 600; color: #fff;
|
font-size: 14px; font-weight: 600; color: #fff;
|
||||||
letter-spacing: 1px; margin-bottom: 16px; padding-bottom: 10px;
|
letter-spacing: 1px; margin-bottom: 16px; padding-bottom: 10px;
|
||||||
@@ -86,7 +91,7 @@ function goPublicity() { router.push('/publicity') }
|
|||||||
width: 24px; height: 2px; background: #93c5fd;
|
width: 24px; height: 2px; background: #93c5fd;
|
||||||
}
|
}
|
||||||
.footer-col a, .footer-col p {
|
.footer-col a, .footer-col p {
|
||||||
display: block; font-size: 13px; color: rgba(255, 255, 255, 0.6);
|
display: block; font-size: 13px; color: #fff;
|
||||||
line-height: 2.1; transition: color 0.2s;
|
line-height: 2.1; transition: color 0.2s;
|
||||||
}
|
}
|
||||||
.footer-col a { cursor: pointer; }
|
.footer-col a { cursor: pointer; }
|
||||||
@@ -98,12 +103,12 @@ function goPublicity() { router.push('/publicity') }
|
|||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
.qr-label { font-size: 12px; color: rgba(255, 255, 255, 0.5); margin-top: 10px; }
|
.qr-label { font-size: 12px; color: #fff; margin-top: 10px; }
|
||||||
.footer-bottom {
|
.footer-bottom {
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
padding: 18px 0;
|
padding: 18px 0;
|
||||||
display: flex; justify-content: space-between;
|
display: flex; justify-content: space-between;
|
||||||
font-size: 12px; color: rgba(255, 255, 255, 0.4);
|
font-size: 12px; color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 公开门户共享顶部导航 (首页 / 项目公示 / 公示详情 共用)
|
<!-- 公开门户共享顶部导航 (首页 / 项目公示 / 公示详情 共用)
|
||||||
active 态按当前 route 推导, 无需各页面各自传参 -->
|
active 态按当前 route 推导, 无需各页面各自传参 -->
|
||||||
<header class="top-nav" :class="topNavClass">
|
<header class="top-nav" :class="[topNavClass, themeClass, isHomeMobileClass]">
|
||||||
<a class="logo" title="返回首页" @click.prevent="goHome">
|
<a class="logo" title="返回首页" @click.prevent="goHome">
|
||||||
<div class="logo-icon"><img src="/logo.png" alt="logo" /></div>
|
<div class="logo-icon"><img src="/logo.png" alt="logo" /></div>
|
||||||
<div class="logo-text">
|
<div class="logo-text">
|
||||||
@@ -95,17 +95,30 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 主题: '' = 默认首页透明/滚动变白; 'solid-brand' = brand-primary 实色 + 白字 (与 PortalShell 一致)
|
||||||
|
const props = defineProps({
|
||||||
|
theme: { type: String, default: '' }
|
||||||
|
})
|
||||||
|
|
||||||
const isScrolled = ref(false)
|
const isScrolled = ref(false)
|
||||||
const drawerOpen = ref(false)
|
const drawerOpen = ref(false)
|
||||||
|
|
||||||
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
|
||||||
const loggedIn = computed(() => !!userStore.token)
|
|
||||||
const userName = computed(() => userStore.user?.userName || '用户')
|
|
||||||
|
|
||||||
// active 态: 按当前路由推导 (首页=年度项目规划, /publicity* = 项目公示)
|
// active 态: 按当前路由推导 (首页=年度项目规划, /publicity* = 项目公示)
|
||||||
const isHome = computed(() => route.path === '/')
|
const isHome = computed(() => route.path === '/')
|
||||||
const isPublicity = computed(() => route.path === '/publicity' || route.path.startsWith('/publicity/'))
|
const isPublicity = computed(() => route.path === '/publicity' || route.path.startsWith('/publicity/'))
|
||||||
|
|
||||||
|
// 顶栏风格: theme=solid-brand 时强制走品牌色 (与 PortalShell 一致, 不跟随滚动/路由变白)
|
||||||
|
const themeClass = computed(() => props.theme === 'solid-brand' ? 'is-solid-brand' : '')
|
||||||
|
// mobile 下首页标记: 用于媒体查询里选择性隐藏 logo (避免与 banner.jpg 文字重叠)
|
||||||
|
const isHomeMobileClass = computed(() => isHome.value ? 'is-home-mobile' : '')
|
||||||
|
const topNavClass = computed(() => {
|
||||||
|
if (props.theme === 'solid-brand') return ''
|
||||||
|
if (!isHome.value) return 'is-solid'
|
||||||
|
return isScrolled.value ? 'is-solid' : 'is-transparent'
|
||||||
|
})
|
||||||
|
const loggedIn = computed(() => !!userStore.token)
|
||||||
|
const userName = computed(() => userStore.user?.userName || '用户')
|
||||||
|
|
||||||
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
||||||
onMounted(() => window.addEventListener('scroll', handleScroll, { passive: true }))
|
onMounted(() => window.addEventListener('scroll', handleScroll, { passive: true }))
|
||||||
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))
|
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))
|
||||||
@@ -120,7 +133,7 @@ async function onUserCmd(cmd) {
|
|||||||
if (cmd === 'logout') return goLogout()
|
if (cmd === 'logout') return goLogout()
|
||||||
if (cmd === 'account') {
|
if (cmd === 'account') {
|
||||||
const r = (userStore.user?.role || '')
|
const r = (userStore.user?.role || '')
|
||||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
|
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/overview', sponsor: '/sponsor/home' }
|
||||||
router.push(map[r] || '/admin/workbench')
|
router.push(map[r] || '/admin/workbench')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,23 +152,67 @@ a { color: inherit; text-decoration: none; }
|
|||||||
|
|
||||||
/* ========== 顶部导航 ========== */
|
/* ========== 顶部导航 ========== */
|
||||||
.top-nav {
|
.top-nav {
|
||||||
position: sticky;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
height: 72px;
|
height: 72px;
|
||||||
padding: 0 60px;
|
padding: 0 60px;
|
||||||
background: var(--brand-primary);
|
display: grid;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
grid-template-columns: auto 1fr auto;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
transition: background 0.3s, box-shadow 0.3s, border-color 0.3s, color 0.3s;
|
||||||
transition: box-shadow 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-nav.is-scrolled {
|
/* 透明态: 首页置顶, 文字白, 背景透明与 banner 融合 */
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
.top-nav.is-transparent {
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: none;
|
||||||
|
box-shadow: none;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 白色态: 滚动后 / 非首页 */
|
||||||
|
.top-nav.is-solid {
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: none;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
/* 一刀切: 白底态下所有未显式覆盖的子元素都用深灰, 避免继承 is-transparent 的 #fff */
|
||||||
|
.top-nav.is-solid,
|
||||||
|
.top-nav.is-solid * {
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
.top-nav.is-solid .nav-item:hover .nav-link,
|
||||||
|
.top-nav.is-solid .nav-link.active {
|
||||||
|
color: var(--brand-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 品牌实色态: 公示页/公示详情专用, 与 PortalShell 一致 (brand-primary 实色 + 白字) */
|
||||||
|
.top-nav.is-solid-brand {
|
||||||
|
background: var(--brand-primary);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.top-nav.is-solid-brand,
|
||||||
|
.top-nav.is-solid-brand * {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.top-nav.is-solid-brand .nav-link { color: rgba(255, 255, 255, 0.85); }
|
||||||
|
.top-nav.is-solid-brand .nav-item:hover .nav-link,
|
||||||
|
.top-nav.is-solid-brand .nav-link.active { color: #fff; }
|
||||||
|
.top-nav.is-solid-brand .nav-link::after { background: #fff; }
|
||||||
|
.top-nav.is-solid-brand .logo-title { color: #fff; }
|
||||||
|
.top-nav.is-solid-brand .logo-subtitle { color: rgba(255, 255, 255, 0.6); }
|
||||||
|
.top-nav.is-solid-brand .hamburger { color: #fff; }
|
||||||
|
.top-nav.is-solid-brand .hamburger:hover { background: rgba(255, 255, 255, 0.12); }
|
||||||
|
.top-nav.is-solid-brand .hamburger:active { background: rgba(255, 255, 255, 0.2); }
|
||||||
|
.top-nav.is-solid-brand .user-link { color: rgba(255, 255, 255, 0.85); }
|
||||||
|
.top-nav.is-solid-brand .user-link:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -183,19 +240,25 @@ a { color: inherit; text-decoration: none; }
|
|||||||
.logo-title {
|
.logo-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #fff;
|
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
transition: color 0.3s;
|
||||||
}
|
}
|
||||||
|
.top-nav.is-transparent .logo-title { color: #fff; }
|
||||||
|
.top-nav.is-solid .logo-title { color: #1f2937; }
|
||||||
|
|
||||||
.logo-subtitle {
|
.logo-subtitle {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.3px;
|
||||||
|
transition: color 0.3s;
|
||||||
}
|
}
|
||||||
|
.top-nav.is-transparent .logo-subtitle { color: rgba(255, 255, 255, 0.6); }
|
||||||
|
.top-nav.is-solid .logo-subtitle { color: #6b7280; }
|
||||||
|
|
||||||
.nav-list {
|
.nav-list {
|
||||||
flex: 1;
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -213,7 +276,6 @@ a { color: inherit; text-decoration: none; }
|
|||||||
.nav-link {
|
.nav-link {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.25s;
|
transition: color 0.25s;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -221,6 +283,8 @@ a { color: inherit; text-decoration: none; }
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.top-nav.is-transparent .nav-link { color: rgba(255, 255, 255, 0.85); }
|
||||||
|
.top-nav.is-solid .nav-link { color: #4b5563; }
|
||||||
|
|
||||||
.nav-link::after {
|
.nav-link::after {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -229,15 +293,20 @@ a { color: inherit; text-decoration: none; }
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: #fff;
|
background: currentColor;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:hover .nav-link,
|
.nav-item:hover .nav-link,
|
||||||
.nav-link.active {
|
.nav-link.active {
|
||||||
color: #fff;
|
color: currentColor;
|
||||||
}
|
}
|
||||||
|
.top-nav.is-transparent .nav-item:hover .nav-link,
|
||||||
|
.top-nav.is-transparent .nav-link.active { color: #fff; }
|
||||||
|
.top-nav.is-solid .nav-item:hover .nav-link,
|
||||||
|
.top-nav.is-solid .nav-link.active { color: var(--brand-primary); }
|
||||||
|
.top-nav.is-solid .nav-link::after { background: var(--brand-primary); }
|
||||||
|
|
||||||
.nav-item:hover .nav-link::after,
|
.nav-item:hover .nav-link::after,
|
||||||
.nav-link.active::after {
|
.nav-link.active::after {
|
||||||
@@ -249,38 +318,34 @@ a { color: inherit; text-decoration: none; }
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-btn {
|
.login-btn {
|
||||||
padding: 7px 20px;
|
padding: 6px 0;
|
||||||
background: #fff;
|
font-size: 14px;
|
||||||
color: var(--brand-primary);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
.login-btn:hover { opacity: 0.7; }
|
||||||
.login-btn:hover {
|
|
||||||
background: #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-link {
|
.user-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s, color 0.2s;
|
transition: background 0.2s, color 0.2s;
|
||||||
}
|
}
|
||||||
|
.top-nav.is-transparent .user-link { color: rgba(255, 255, 255, 0.85); }
|
||||||
.user-link:hover {
|
.top-nav.is-transparent .user-link:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
||||||
background: rgba(255, 255, 255, 0.1);
|
.top-nav.is-solid .user-link { color: #4b5563; }
|
||||||
color: #fff;
|
.top-nav.is-solid .user-link:hover { background: #f3f4f6; color: var(--brand-primary); }
|
||||||
}
|
|
||||||
|
|
||||||
/* ========== 汉堡按钮 (桌面隐藏, 手机显示) ========== */
|
/* ========== 汉堡按钮 (桌面隐藏, 手机显示) ========== */
|
||||||
.hamburger {
|
.hamburger {
|
||||||
@@ -289,15 +354,18 @@ a { color: inherit; text-decoration: none; }
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-left: auto;
|
grid-column: 3;
|
||||||
|
justify-self: end;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s, color 0.3s;
|
||||||
|
color: currentColor;
|
||||||
}
|
}
|
||||||
.hamburger:hover { background: rgba(255, 255, 255, 0.12); }
|
.top-nav.is-transparent .hamburger:hover { background: rgba(255, 255, 255, 0.12); }
|
||||||
.hamburger:active { background: rgba(255, 255, 255, 0.2); }
|
.top-nav.is-transparent .hamburger:active { background: rgba(255, 255, 255, 0.2); }
|
||||||
|
.top-nav.is-solid .hamburger:hover { background: #f3f4f6; }
|
||||||
|
.top-nav.is-solid .hamburger:active { background: #e5e7eb; }
|
||||||
|
|
||||||
/* ========== 抽屉 (手机端) ========== */
|
/* ========== 抽屉 (手机端) ========== */
|
||||||
.drawer-mask {
|
.drawer-mask {
|
||||||
@@ -400,8 +468,10 @@ a { color: inherit; text-decoration: none; }
|
|||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* 顶栏: 高度收窄 + padding 减小 */
|
/* 顶栏: 高度收窄 + padding 减小 */
|
||||||
.top-nav { height: 56px !important; padding: 0 16px !important; }
|
.top-nav { height: 56px !important; padding: 0 16px !important; }
|
||||||
.logo-title { font-size: 14px !important; }
|
/* mobile: 只有首页隐藏 logo (与 banner.jpg 文字重叠); 公示/公示详情保留 logo */
|
||||||
.logo-subtitle { display: none; }
|
.is-home-mobile .logo { display: none !important; }
|
||||||
|
/* 首页滚动后 navbar 变白底, 此时显示 logo (白色 navbar 上有 logo 图标+标题更明确) */
|
||||||
|
.is-home-mobile.is-solid .logo { display: flex !important; }
|
||||||
/* 桌面 nav-list + 顶部 tools 隐藏, 改用汉堡 */
|
/* 桌面 nav-list + 顶部 tools 隐藏, 改用汉堡 */
|
||||||
.nav-list { display: none !important; }
|
.nav-list { display: none !important; }
|
||||||
.top-tools { display: none !important; }
|
.top-tools { display: none !important; }
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ async function onUserCmd(cmd) {
|
|||||||
if (cmd === 'logout') return goLogout()
|
if (cmd === 'logout') return goLogout()
|
||||||
if (cmd === 'account') {
|
if (cmd === 'account') {
|
||||||
const r = (userStore.user?.role || '')
|
const r = (userStore.user?.role || '')
|
||||||
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
|
const map = { admin: '/admin/workbench', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/overview', sponsor: '/sponsor/home' }
|
||||||
router.push(map[r] || '/')
|
router.push(map[r] || '/')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,6 +98,8 @@ onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))
|
|||||||
background: #f5f6f8;
|
background: #f5f6f8;
|
||||||
min-width: 1354px;
|
min-width: 1354px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
/* PortalLayout 的 padding-top: 72px 对注册页是冗余的 (PortalShell 自带 sticky 顶栏占位), 上移抵消 (与 Login.vue 同处理) */
|
||||||
|
margin-top: -72px;
|
||||||
}
|
}
|
||||||
a { color: inherit; text-decoration: none; }
|
a { color: inherit; text-decoration: none; }
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ a { color: inherit; text-decoration: none; }
|
|||||||
transition: box-shadow 0.3s;
|
transition: box-shadow 0.3s;
|
||||||
}
|
}
|
||||||
.top-nav.is-scrolled { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); }
|
.top-nav.is-scrolled { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); }
|
||||||
.logo { display: flex; align-items: center; gap: 12px; }
|
.logo { display: flex; align-items: center; gap: 12px; cursor: pointer; }
|
||||||
.logo-icon {
|
.logo-icon {
|
||||||
width: 36px; height: 36px;
|
width: 36px; height: 36px;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
@@ -124,7 +126,10 @@ a { color: inherit; text-decoration: none; }
|
|||||||
.logo-subtitle { font-size: 11px; color: rgba(255, 255, 255, 0.6); margin-top: 2px; letter-spacing: 0.3px; }
|
.logo-subtitle { font-size: 11px; color: rgba(255, 255, 255, 0.6); margin-top: 2px; letter-spacing: 0.3px; }
|
||||||
|
|
||||||
.nav-list {
|
.nav-list {
|
||||||
flex: 1; display: flex; align-items: center; justify-content: center;
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
gap: 36px; list-style: none;
|
gap: 36px; list-style: none;
|
||||||
}
|
}
|
||||||
.nav-item { position: relative; height: 72px; display: flex; align-items: center; }
|
.nav-item { position: relative; height: 72px; display: flex; align-items: center; }
|
||||||
@@ -141,13 +146,19 @@ a { color: inherit; text-decoration: none; }
|
|||||||
.nav-item:hover .nav-link, .nav-link.active { color: #fff; }
|
.nav-item:hover .nav-link, .nav-link.active { color: #fff; }
|
||||||
.nav-item:hover .nav-link::after, .nav-link.active::after { width: 100%; }
|
.nav-item:hover .nav-link::after, .nav-link.active::after { width: 100%; }
|
||||||
|
|
||||||
.top-tools { display: flex; align-items: center; gap: 16px; }
|
.top-tools { display: flex; align-items: center; gap: 16px; margin-left: auto; }
|
||||||
.login-btn {
|
.login-btn {
|
||||||
padding: 7px 20px; background: #fff; color: var(--brand-primary);
|
padding: 6px 0;
|
||||||
font-size: 13px; font-weight: 500;
|
background: none;
|
||||||
cursor: pointer; transition: background 0.2s; letter-spacing: 1px;
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
.login-btn:hover { background: #f3f4f6; }
|
.login-btn:hover { opacity: 0.7; }
|
||||||
.user-link {
|
.user-link {
|
||||||
display: flex; align-items: center; gap: 6px;
|
display: flex; align-items: center; gap: 6px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item command="account" @click="$router.push('/' + (role || 'admin') + '/account')">账号信息</el-dropdown-item>
|
<el-dropdown-item command="home" @click="goMyHome">我的主页</el-dropdown-item>
|
||||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
@@ -145,6 +145,18 @@ function goNotice() {
|
|||||||
router.push(p)
|
router.push(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 头像下拉「我的主页」: 跳到各角色首页 (工作台/首页) */
|
||||||
|
const HOME_PATH = {
|
||||||
|
admin: '/admin/workbench',
|
||||||
|
manager: '/manager/workbench',
|
||||||
|
doctor: '/doctor/home',
|
||||||
|
executor: '/executor/overview',
|
||||||
|
sponsor: '/sponsor/home'
|
||||||
|
}
|
||||||
|
function goMyHome() {
|
||||||
|
router.push(HOME_PATH[role.value] || '/')
|
||||||
|
}
|
||||||
|
|
||||||
/** doctor 角色: 拉 biz_expert.auditStatus 同步到 store, 控制侧栏 menu + Home pannel */
|
/** doctor 角色: 拉 biz_expert.auditStatus 同步到 store, 控制侧栏 menu + Home pannel */
|
||||||
async function loadExpertAuditStatus() {
|
async function loadExpertAuditStatus() {
|
||||||
if (store.role !== 'doctor') return
|
if (store.role !== 'doctor') return
|
||||||
@@ -245,6 +257,7 @@ const MENU = {
|
|||||||
{ path: '/doctor/account', title: '账号信息', icon: User }
|
{ path: '/doctor/account', title: '账号信息', icon: User }
|
||||||
],
|
],
|
||||||
executor: [
|
executor: [
|
||||||
|
{ path: '/executor/overview', title: '首页', icon: House },
|
||||||
{ path: '/executor/submissions', title: '我的项目策划方案', icon: EditPen },
|
{ path: '/executor/submissions', title: '我的项目策划方案', icon: EditPen },
|
||||||
{ path: '/executor/projects', title: '项目列表', icon: Document },
|
{ path: '/executor/projects', title: '项目列表', icon: Document },
|
||||||
{ path: '/executor/meetings', title: '会议列表', icon: Calendar },
|
{ path: '/executor/meetings', title: '会议列表', icon: Calendar },
|
||||||
@@ -254,7 +267,6 @@ const MENU = {
|
|||||||
],
|
],
|
||||||
sponsor: [
|
sponsor: [
|
||||||
{ path: '/sponsor/home', title: '首页', icon: House },
|
{ path: '/sponsor/home', title: '首页', icon: House },
|
||||||
{ path: '/sponsor/submissions', title: '我的项目策划方案', icon: EditPen },
|
|
||||||
{ path: '/sponsor/my-projects', title: '我的项目', icon: Document },
|
{ path: '/sponsor/my-projects', title: '我的项目', icon: Document },
|
||||||
{ path: '/sponsor/meetings', title: '会议列表', icon: Calendar },
|
{ path: '/sponsor/meetings', title: '会议列表', icon: Calendar },
|
||||||
{ path: '/sponsor/people', title: '人员管理', icon: User, requireMain: true },
|
{ path: '/sponsor/people', title: '人员管理', icon: User, requireMain: true },
|
||||||
|
|||||||
@@ -9,5 +9,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.portal-layout { min-height: 100vh; background: #fff; }
|
.portal-layout { min-height: 100vh; background: #fff; padding-top: 72px; }
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* mobile 下 navbar 收窄到 56px, padding-top 同步收窄, 避免 hero 与 navbar 之间出现 16px 空白 */
|
||||||
|
.portal-layout { padding-top: 56px; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -26,6 +26,7 @@ const routes = [
|
|||||||
{ path: 'users', name: 'admin-users', component: () => import('@/views/admin/Users.vue'), meta: { title: '用户管理' } },
|
{ path: 'users', name: 'admin-users', component: () => import('@/views/admin/Users.vue'), meta: { title: '用户管理' } },
|
||||||
{ path: 'roles', name: 'admin-roles', component: () => import('@/views/admin/Roles.vue'), meta: { title: '角色管理' } },
|
{ path: 'roles', name: 'admin-roles', component: () => import('@/views/admin/Roles.vue'), meta: { title: '角色管理' } },
|
||||||
{ path: 'projects', name: 'admin-projects', component: () => import('@/views/manager/Projects.vue'), meta: { title: '项目管理' } },
|
{ path: 'projects', name: 'admin-projects', component: () => import('@/views/manager/Projects.vue'), meta: { title: '项目管理' } },
|
||||||
|
{ path: 'projects/detail/:projectId', name: 'admin-projects-detail', component: () => import('@/views/manager/ManagerProjectDetail.vue'), meta: { title: '项目详情' } },
|
||||||
{ path: 'meetings', name: 'admin-meetings', component: () => import('@/views/meetings/Meetings.vue'), meta: { title: '会议管理' } },
|
{ path: 'meetings', name: 'admin-meetings', component: () => import('@/views/meetings/Meetings.vue'), meta: { title: '会议管理' } },
|
||||||
{ path: 'meetings/detail/:meetingId', name: 'admin-meetings-detail', component: () => import('@/views/meetings/MeetingDetail.vue'), meta: { title: '会议详情' } },
|
{ path: 'meetings/detail/:meetingId', name: 'admin-meetings-detail', component: () => import('@/views/meetings/MeetingDetail.vue'), meta: { title: '会议详情' } },
|
||||||
{ path: 'meetings/view/:meetingId', name: 'admin-meetings-view', component: () => import('@/views/meetings/MeetingDetail.vue'), meta: { title: '会议详情', readonly: true } },
|
{ path: 'meetings/view/:meetingId', name: 'admin-meetings-view', component: () => import('@/views/meetings/MeetingDetail.vue'), meta: { title: '会议详情', readonly: true } },
|
||||||
@@ -49,6 +50,7 @@ const routes = [
|
|||||||
{ path: 'article', name: 'admin-article', component: () => import('@/views/admin/BizArticleAdmin.vue'), meta: { title: '协议管理' } },
|
{ path: 'article', name: 'admin-article', component: () => import('@/views/admin/BizArticleAdmin.vue'), meta: { title: '协议管理' } },
|
||||||
{ path: 'article/edit/:id', name: 'admin-article-edit', component: () => import('@/views/admin/BizArticleEdit.vue'), meta: { title: '编辑文章' } },
|
{ path: 'article/edit/:id', name: 'admin-article-edit', component: () => import('@/views/admin/BizArticleEdit.vue'), meta: { title: '编辑文章' } },
|
||||||
{ path: 'special-plan', name: 'admin-special-plan', component: () => import('@/views/admin/BizSpecialPlanAdmin.vue'), meta: { title: '专项计划管理' } },
|
{ path: 'special-plan', name: 'admin-special-plan', component: () => import('@/views/admin/BizSpecialPlanAdmin.vue'), meta: { title: '专项计划管理' } },
|
||||||
|
{ path: 'special-plan/new', name: 'admin-special-plan-new', component: () => import('@/views/admin/BizSpecialPlanEdit.vue'), meta: { title: '新建专项计划' } },
|
||||||
{ path: 'special-plan/edit/:id', name: 'admin-special-plan-edit', component: () => import('@/views/admin/BizSpecialPlanEdit.vue'), meta: { title: '编辑专项计划' } },
|
{ path: 'special-plan/edit/:id', name: 'admin-special-plan-edit', component: () => import('@/views/admin/BizSpecialPlanEdit.vue'), meta: { title: '编辑专项计划' } },
|
||||||
{ path: 'project-category', name: 'admin-project-category', component: () => import('@/views/admin/ProjectCategory.vue'), meta: { title: '项目类别管理' } },
|
{ path: 'project-category', name: 'admin-project-category', component: () => import('@/views/admin/ProjectCategory.vue'), meta: { title: '项目类别管理' } },
|
||||||
{ path: 'labor-protocol', name: 'admin-labor-protocol', component: () => import('@/views/admin/LaborProtocol.vue'), meta: { title: '劳务协议配置' } },
|
{ path: 'labor-protocol', name: 'admin-labor-protocol', component: () => import('@/views/admin/LaborProtocol.vue'), meta: { title: '劳务协议配置' } },
|
||||||
@@ -109,7 +111,7 @@ const routes = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ path: '/executor', component: AdminLayout, meta: { role: 'executor' }, children: [
|
{ path: '/executor', component: AdminLayout, meta: { role: 'executor' }, children: [
|
||||||
{ path: '', redirect: { name: 'executor-submissions' } },
|
{ path: '', redirect: { name: 'executor-overview' } },
|
||||||
{ path: 'overview', name: 'executor-overview', component: () => import('@/views/executor/Overview.vue'), meta: { title: '首页' } },
|
{ path: 'overview', name: 'executor-overview', component: () => import('@/views/executor/Overview.vue'), meta: { title: '首页' } },
|
||||||
{ path: 'submissions', name: 'executor-submissions', component: () => import('@/views/doctor/Submissions.vue'), meta: { title: '我的项目策划方案' } },
|
{ path: 'submissions', name: 'executor-submissions', component: () => import('@/views/doctor/Submissions.vue'), meta: { title: '我的项目策划方案' } },
|
||||||
{ path: 'submission/new', name: 'executor-submission-new', component: () => import('@/views/doctor/SubmissionNew.vue'), meta: { title: '新建项目策划方案' } },
|
{ path: 'submission/new', name: 'executor-submission-new', component: () => import('@/views/doctor/SubmissionNew.vue'), meta: { title: '新建项目策划方案' } },
|
||||||
@@ -159,6 +161,15 @@ const router = createRouter({
|
|||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 各角色首页映射 (role_type → 首页, 与 Login.vue roleHome 保持一致)
|
||||||
|
const ROLE_HOME = {
|
||||||
|
admin: '/admin/workbench',
|
||||||
|
manager: '/manager/workbench',
|
||||||
|
doctor: '/doctor/home',
|
||||||
|
executor: '/executor/overview',
|
||||||
|
sponsor: '/sponsor/home'
|
||||||
|
}
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
document.title = (to.meta?.title || 'BAHIM') + ' - 合规系统'
|
document.title = (to.meta?.title || 'BAHIM') + ' - 合规系统'
|
||||||
// 公开路由 (无 role meta) 不拦截
|
// 公开路由 (无 role meta) 不拦截
|
||||||
@@ -168,7 +179,11 @@ router.beforeEach((to, from, next) => {
|
|||||||
// 扫码带 token 直登 (签劳务): 放行, 由页面 onMounted 用 token 完成登录
|
// 扫码带 token 直登 (签劳务): 放行, 由页面 onMounted 用 token 完成登录
|
||||||
if (!user && to.query?.token) return next()
|
if (!user && to.query?.token) return next()
|
||||||
if (!user) return next({ name: 'login', query: { redirect: to.fullPath } })
|
if (!user) return next({ name: 'login', query: { redirect: to.fullPath } })
|
||||||
// 已登录: role 不匹配由后端 401 拦截, 不在前端强跳 (避免误判让用户卡死)
|
// 已登录: 前端校验角色匹配 (role_type 单一可信源), 不匹配跳回自己角色首页
|
||||||
|
const userRole = user.role
|
||||||
|
if (userRole && to.meta.role !== userRole) {
|
||||||
|
return next(ROLE_HOME[userRole] || '/')
|
||||||
|
}
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -76,13 +76,14 @@ function chooseState(role, labor, service) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 单轨措辞 (代表轨状态 + 角色 → 展示名). */
|
/** 单轨措辞 (代表轨状态 + 角色 → 展示名). */
|
||||||
function render(role, s, executed) {
|
function render(role, s, phase) {
|
||||||
switch (s) {
|
switch (s) {
|
||||||
case 0: // R 退回
|
case 0: // R 退回
|
||||||
return role === 'executor' ? '已退回' : '待整改'
|
return role === 'executor' ? '已退回' : '待整改'
|
||||||
case 1: // N 未提交
|
case 1: // N 未提交 (时间驱动三态: 未执行 → 执行中 → 已执行, 所有角色统一)
|
||||||
if (!executed) return '未执行'
|
if (phase === 0) return '未执行'
|
||||||
return role === 'executor' ? '执行中' : '已执行未传材料'
|
if (phase === 1) return '执行中'
|
||||||
|
return '已执行'
|
||||||
case 2: // C0 合规审中
|
case 2: // C0 合规审中
|
||||||
if (role === 'sponsor') return '已执行未传材料' // 只读
|
if (role === 'sponsor') return '已执行未传材料' // 只读
|
||||||
return '待审核'
|
return '待审核'
|
||||||
@@ -94,6 +95,21 @@ function render(role, s, executed) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行进度三态: 0=未执行 (now < startTime), 1=执行中 (startTime ≤ now < endTime),
|
||||||
|
* 2=已执行 (now ≥ endTime 或 isExecuted=1). 仅用于材料未提交 (N) 的展示措辞.
|
||||||
|
* isExecuted=1 是 scheduler 在 end_time 到点落库的「已执行」事实, 优先采信.
|
||||||
|
*/
|
||||||
|
function executionPhase(row) {
|
||||||
|
if (isTrue(row.isExecuted)) return 2
|
||||||
|
const now = Date.now()
|
||||||
|
const end = row.endTime ? new Date(row.endTime).getTime() : NaN
|
||||||
|
const start = row.startTime ? new Date(row.startTime).getTime() : NaN
|
||||||
|
if (!Number.isNaN(end) && now >= end) return 2
|
||||||
|
if (!Number.isNaN(start) && now >= start) return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 各角色展示阶段名 (镜像后端 StageDeriver.deriveDisplay).
|
* 各角色展示阶段名 (镜像后端 StageDeriver.deriveDisplay).
|
||||||
* role ∈ {executor, sponsor, manager, admin, doctor, expert}; 非流程角色回退 admin 中性.
|
* role ∈ {executor, sponsor, manager, admin, doctor, expert}; 非流程角色回退 admin 中性.
|
||||||
@@ -104,7 +120,7 @@ export function deriveStage(role, row) {
|
|||||||
if (isTrue(row.isFinished)) return '已完结'
|
if (isTrue(row.isFinished)) return '已完结'
|
||||||
if (isTrue(row.isSettled)) return '已结算'
|
if (isTrue(row.isSettled)) return '已结算'
|
||||||
const chosen = chooseState(role, laborState(row), serviceState(row))
|
const chosen = chooseState(role, laborState(row), serviceState(row))
|
||||||
return render(role, chosen, isTrue(row.isExecuted))
|
return render(role, chosen, executionPhase(row))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,7 +134,7 @@ export function stageLabel(role, row) {
|
|||||||
* 展示阶段名 → 颜色映射 (class + el-tag type), 与 render() 措辞一一对应.
|
* 展示阶段名 → 颜色映射 (class + el-tag type), 与 render() 措辞一一对应.
|
||||||
* 颜色跟随「各角色看到的展示阶段」而非物理阶段, 避免文案与颜色错位
|
* 颜色跟随「各角色看到的展示阶段」而非物理阶段, 避免文案与颜色错位
|
||||||
* (如 sponsor 看「待审核」却因物理阶段 RECTIFYING 显示红色).
|
* (如 sponsor 看「待审核」却因物理阶段 RECTIFYING 显示红色).
|
||||||
* 规则: 待整改/已退回=红, 未执行=灰, 执行中/已执行未传材料=蓝, 待审核=橙, 通过/待结算/完结=绿.
|
* 规则: 待整改/已退回=红, 未执行=灰, 执行中/已执行/已执行未传材料=蓝, 待审核=橙, 通过/待结算/完结=绿.
|
||||||
*/
|
*/
|
||||||
const STAGE_STYLE = {
|
const STAGE_STYLE = {
|
||||||
'冻结中': { cls: 'frozen', tag: 'info' },
|
'冻结中': { cls: 'frozen', tag: 'info' },
|
||||||
@@ -128,6 +144,7 @@ const STAGE_STYLE = {
|
|||||||
'已退回': { cls: 'waiting', tag: 'danger' },
|
'已退回': { cls: 'waiting', tag: 'danger' },
|
||||||
'未执行': { cls: 'pending', tag: 'info' },
|
'未执行': { cls: 'pending', tag: 'info' },
|
||||||
'执行中': { cls: 'running', tag: 'primary' },
|
'执行中': { cls: 'running', tag: 'primary' },
|
||||||
|
'已执行': { cls: 'running', tag: 'primary' },
|
||||||
'已执行未传材料': { cls: 'running', tag: 'primary' },
|
'已执行未传材料': { cls: 'running', tag: 'primary' },
|
||||||
'待审核': { cls: 'reviewing', tag: 'warning' },
|
'待审核': { cls: 'reviewing', tag: 'warning' },
|
||||||
'审核通过': { cls: 'done', tag: 'success' },
|
'审核通过': { cls: 'done', tag: 'success' },
|
||||||
@@ -154,12 +171,12 @@ export function stageTag(role, row) {
|
|||||||
*/
|
*/
|
||||||
export const STAGE_OPTIONS = [
|
export const STAGE_OPTIONS = [
|
||||||
{ label: '未执行', value: 'NOT_STARTED' },
|
{ label: '未执行', value: 'NOT_STARTED' },
|
||||||
{ label: '执行中', value: 'RUNNING' },
|
{ label: '执行中', value: 'IN_PROGRESS' },
|
||||||
|
{ label: '已执行', value: 'RUNNING' },
|
||||||
{ label: '待合规审核', value: 'AWAITING_COMPLIANCE' },
|
{ label: '待合规审核', value: 'AWAITING_COMPLIANCE' },
|
||||||
{ label: '待支持方审核', value: 'AWAITING_SUPERVISION' },
|
{ label: '待支持方审核', value: 'AWAITING_SUPERVISION' },
|
||||||
{ label: '待整改', value: 'RECTIFYING' },
|
{ label: '待整改', value: 'RECTIFYING' },
|
||||||
{ label: '待结算', value: 'AWAITING_SETTLEMENT' },
|
{ label: '待结算', value: 'AWAITING_SETTLEMENT' },
|
||||||
{ label: '已结算', value: 'SETTLED' },
|
|
||||||
{ label: '已完结', value: 'FINISHED' },
|
{ label: '已完结', value: 'FINISHED' },
|
||||||
{ label: '冻结中', value: 'FROZEN' },
|
{ label: '冻结中', value: 'FROZEN' },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -25,6 +25,11 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button type="primary" @click="$router.push('/admin/special-plan/new')">新增</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
<el-table :data="rows" border stripe v-loading="loading">
|
<el-table :data="rows" border stripe v-loading="loading">
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
@@ -39,10 +44,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="updateTime" label="更新时间" width="170" />
|
<el-table-column prop="updateTime" label="更新时间" width="170" />
|
||||||
<el-table-column label="操作" width="140" fixed="right">
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
<template #default="{ row }"><div class="table-actions">
|
<template #default="{ row }"><div class="table-actions">
|
||||||
<el-link :underline="false" size="small" type="primary" @click="$router.push('/admin/special-plan/edit/' + row.id)">编辑</el-link>
|
<el-link :underline="false" size="small" type="primary" @click="$router.push('/admin/special-plan/edit/' + row.id)">编辑</el-link>
|
||||||
<el-link :underline="false" size="small" type="primary" :disabled="row.status !== '0'" @click="preview(row)">预览</el-link>
|
<el-link :underline="false" size="small" type="primary" :disabled="row.status !== '0'" @click="preview(row)">预览</el-link>
|
||||||
|
<el-link :underline="false" size="small" type="danger" @click="remove(row)">删除</el-link>
|
||||||
</div></template>
|
</div></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -64,6 +70,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, onMounted } from 'vue'
|
import { reactive, ref, onMounted } from 'vue'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
const TYPE_LABEL = { rich: '富文本', file: '上传文件' }
|
const TYPE_LABEL = { rich: '富文本', file: '上传文件' }
|
||||||
|
|
||||||
@@ -91,12 +98,26 @@ function preview(row) {
|
|||||||
window.open(`${import.meta.env.BASE_URL}#/special-plan/${row.id}`, '_blank')
|
window.open(`${import.meta.env.BASE_URL}#/special-plan/${row.id}`, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function remove(row) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认删除「${row.title}」?`, '删除确认', { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' })
|
||||||
|
} catch { return }
|
||||||
|
try {
|
||||||
|
await request({ url: `/business/specialPlan/${row.id}`, method: 'delete' })
|
||||||
|
ElMessage.success('已删除')
|
||||||
|
load()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.msg || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.admin-plan { padding: 16px; }
|
.admin-plan { padding: 16px; }
|
||||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||||
|
.toolbar { margin-bottom: 12px; }
|
||||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -332,7 +332,9 @@ async function loadOrgOptions() {
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
params: { orgType: createForm.roleType, pageSize: 500 }
|
params: { orgType: createForm.roleType, pageSize: 500 }
|
||||||
})
|
})
|
||||||
orgOptions.value = (r.data && r.data.rows) || r.rows || []
|
const list = (r.data && r.data.rows) || r.rows || []
|
||||||
|
// 子账号须挂到"已有主账号"的单位下: 过滤掉无主账号(userId=null)的单位, 避免 parent_user_id 落空成孤儿
|
||||||
|
orgOptions.value = list.filter(o => o.userId != null)
|
||||||
} catch (e) { /* GET 错误拦截器已统一 toast, 这里静默 */ }
|
} catch (e) { /* GET 错误拦截器已统一 toast, 这里静默 */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="stat-value">{{ stats.totalProjects }}</div>
|
<div class="stat-value">{{ stats.totalProjects }}</div>
|
||||||
<div class="stat-extra">查看详情 →</div>
|
<div class="stat-extra">查看详情 →</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="stat-card" to="/admin/projects">
|
<router-link class="stat-card" to="/admin/projects?isFinished=1">
|
||||||
<div class="stat-bar"></div>
|
<div class="stat-bar"></div>
|
||||||
<div class="stat-label">已结题项目</div>
|
<div class="stat-label">已结题项目</div>
|
||||||
<div class="stat-value">{{ stats.finishedProjects }}</div>
|
<div class="stat-value">{{ stats.finishedProjects }}</div>
|
||||||
@@ -38,19 +38,8 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 各角色用户数分布 + 快捷入口, 双栏布局 -->
|
<!-- 快捷入口 -->
|
||||||
<div class="content-grid">
|
<div class="content-grid">
|
||||||
<section class="content-card">
|
|
||||||
<h2 class="section-title">各角色用户数</h2>
|
|
||||||
<div class="role-grid">
|
|
||||||
<div v-for="r in roleStats" :key="r.code" class="role-card">
|
|
||||||
<div class="role-name">{{ r.label }}</div>
|
|
||||||
<div class="role-count">{{ r.count }}</div>
|
|
||||||
<div class="role-bar" :style="{ width: r.pct + '%' }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="content-card">
|
<section class="content-card">
|
||||||
<h2 class="section-title">快捷入口</h2>
|
<h2 class="section-title">快捷入口</h2>
|
||||||
<div class="quick-grid">
|
<div class="quick-grid">
|
||||||
@@ -88,25 +77,26 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 消息通知 (NoticeList 组件内部已含 SSE 实时刷新 + 详情 dialog + 全部已读 + 分页) -->
|
||||||
|
<section class="content-card message-card">
|
||||||
|
<h2 class="section-title">消息通知<a class="more" @click.prevent="$router.push('/admin/messages')">更多 →</a></h2>
|
||||||
|
<NoticeList :pageable="true" :show-header="false" />
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { listUser, listByRole } from '@/api/system'
|
import { listUser } from '@/api/system'
|
||||||
import { bizList } from '@/api/public'
|
import { bizList } from '@/api/public'
|
||||||
|
import NoticeList from '@/components/NoticeList.vue'
|
||||||
import { User, Document, Calendar, UserFilled, Star, OfficeBuilding, Files, Bell } from '@element-plus/icons-vue'
|
import { User, Document, Calendar, UserFilled, Star, OfficeBuilding, Files, Bell } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const stats = ref({
|
const stats = ref({
|
||||||
totalUsers: 0, roleCount: 0, totalProjects: 0, finishedProjects: 0, totalMeetings: 0
|
totalUsers: 0, roleCount: 0, totalProjects: 0, finishedProjects: 0, totalMeetings: 0
|
||||||
})
|
})
|
||||||
const roleStats = ref([])
|
|
||||||
|
|
||||||
const ROLE_LABEL = {
|
|
||||||
admin: '后台管理员', manager: '合规人员',
|
|
||||||
doctor: '评审专家', executor: '执行人', sponsor: '支持方'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
@@ -114,21 +104,8 @@ async function load() {
|
|||||||
const u = await listUser({ pageNum: 1, pageSize: 1 })
|
const u = await listUser({ pageNum: 1, pageSize: 1 })
|
||||||
stats.value.totalUsers = (u.data && u.data.total) || 0
|
stats.value.totalUsers = (u.data && u.data.total) || 0
|
||||||
|
|
||||||
// 各角色用户数
|
// 角色类型数 = 业务角色 4 类 (不含 admin)
|
||||||
const all = []
|
stats.value.roleCount = 4
|
||||||
const codes = Object.keys(ROLE_LABEL)
|
|
||||||
for (const code of codes) {
|
|
||||||
try {
|
|
||||||
const r = await listByRole({ pageNum: 1, pageSize: 1, roleType: code })
|
|
||||||
all.push({ code, label: ROLE_LABEL[code], count: (r.data && r.data.total) || 0 })
|
|
||||||
} catch (e) {
|
|
||||||
all.push({ code, label: ROLE_LABEL[code], count: 0 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 计算占比 (用于进度条)
|
|
||||||
const max = Math.max(1, ...all.map(r => r.count))
|
|
||||||
roleStats.value = all.map(r => ({ ...r, pct: Math.round((r.count / max) * 100) }))
|
|
||||||
stats.value.roleCount = codes.length
|
|
||||||
|
|
||||||
// 项目数
|
// 项目数
|
||||||
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
|
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
|
||||||
@@ -204,11 +181,9 @@ onMounted(load)
|
|||||||
background: var(--brand-slate-50);
|
background: var(--brand-slate-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— 双栏内容区 —— */
|
/* —— 内容区 (单卡片, 快捷入口独占) —— */
|
||||||
.content-grid {
|
.content-grid {
|
||||||
display: grid;
|
display: block;
|
||||||
grid-template-columns: 2fr 1fr;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
}
|
||||||
.content-card {
|
.content-card {
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
@@ -228,43 +203,16 @@ onMounted(load)
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— 角色分布: 6 列网格 —— */
|
/* —— 消息通知卡片 —— */
|
||||||
.role-grid {
|
.message-card { margin-top: 16px; }
|
||||||
display: grid;
|
.message-card .section-title { display: flex; justify-content: space-between; align-items: center; }
|
||||||
grid-template-columns: repeat(3, 1fr);
|
.message-card .more { font-size: 12px; color: var(--brand-primary); text-decoration: none; cursor: pointer; font-weight: 400; }
|
||||||
gap: 12px;
|
.message-card .more:hover { text-decoration: underline; }
|
||||||
}
|
|
||||||
.role-card {
|
|
||||||
background: var(--brand-slate-50);
|
|
||||||
border: 1px solid var(--brand-slate-200);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 14px 16px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.role-name {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
.role-count {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
line-height: 1.1;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.role-bar {
|
|
||||||
height: 4px;
|
|
||||||
background: var(--brand-primary);
|
|
||||||
border-radius: 2px;
|
|
||||||
transition: width 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 快捷入口: 4 列网格, 图标 + 文字 —— */
|
/* —— 快捷入口: 4 列网格, 图标 + 文字 —— */
|
||||||
.quick-grid {
|
.quick-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.quick-item {
|
.quick-item {
|
||||||
@@ -293,7 +241,6 @@ onMounted(load)
|
|||||||
/* —— 响应式: 5 卡变 3/2/1 列 —— */
|
/* —— 响应式: 5 卡变 3/2/1 列 —— */
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.stats-grid { grid-template-columns: repeat(3, 1fr); }
|
.stats-grid { grid-template-columns: repeat(3, 1fr); }
|
||||||
.content-grid { grid-template-columns: 1fr; }
|
|
||||||
}
|
}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* 容器内边距收窄 (不影响 admin-layout 的 main padding) */
|
/* 容器内边距收窄 (不影响 admin-layout 的 main padding) */
|
||||||
@@ -315,21 +262,12 @@ onMounted(load)
|
|||||||
.stat-value { font-size: 22px !important; padding-bottom: 10px; }
|
.stat-value { font-size: 22px !important; padding-bottom: 10px; }
|
||||||
.stat-extra { font-size: 11px !important; padding: 8px 12px; }
|
.stat-extra { font-size: 11px !important; padding: 8px 12px; }
|
||||||
|
|
||||||
/* 双栏内容: 单列堆叠 */
|
|
||||||
.content-grid { grid-template-columns: 1fr; gap: 12px; }
|
|
||||||
|
|
||||||
/* 内容卡: padding 收窄 */
|
/* 内容卡: padding 收窄 */
|
||||||
.content-card { padding: 14px 16px !important; border-radius: 4px !important; }
|
.content-card { padding: 14px 16px !important; border-radius: 4px !important; }
|
||||||
.section-title { font-size: 14px !important; margin-bottom: 12px !important; }
|
.section-title { font-size: 14px !important; margin-bottom: 12px !important; }
|
||||||
|
|
||||||
/* 角色卡: 2 列 */
|
/* 快捷入口: 2 列紧凑 */
|
||||||
.role-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
|
.quick-grid { grid-template-columns: repeat(2, 1fr); gap: 6px; }
|
||||||
.role-card { padding: 10px 12px !important; }
|
|
||||||
.role-name { font-size: 11px !important; margin-bottom: 4px; }
|
|
||||||
.role-count { font-size: 18px !important; margin-bottom: 6px; }
|
|
||||||
|
|
||||||
/* 快捷入口: 1 列紧凑 (8 项太多, 1 列更易点) */
|
|
||||||
.quick-grid { grid-template-columns: 1fr; gap: 6px; }
|
|
||||||
.quick-item { padding: 10px 12px !important; font-size: 13px; }
|
.quick-item { padding: 10px 12px !important; font-size: 13px; }
|
||||||
.quick-icon { font-size: 16px !important; }
|
.quick-icon { font-size: 16px !important; }
|
||||||
}
|
}
|
||||||
@@ -337,7 +275,6 @@ onMounted(load)
|
|||||||
/* —— 超窄屏 (≤480px): KPI 单列 —— */
|
/* —— 超窄屏 (≤480px): KPI 单列 —— */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.stats-grid { grid-template-columns: 1fr; }
|
.stats-grid { grid-template-columns: 1fr; }
|
||||||
.role-grid { grid-template-columns: 1fr; }
|
|
||||||
.stat-value { font-size: 20px !important; }
|
.stat-value { font-size: 20px !important; }
|
||||||
.content-card { padding: 12px 14px !important; }
|
.content-card { padding: 12px 14px !important; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,16 +15,7 @@
|
|||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<!-- 左侧 banner -->
|
<!-- 左侧 banner -->
|
||||||
<div class="banner-section">
|
<div class="banner-section">
|
||||||
<div class="banner-inner">
|
<div class="banner-inner" :style="{ backgroundImage: `url(${loginBg})` }"></div>
|
||||||
<span class="banner-tag">2025-2030</span>
|
|
||||||
<h2 class="banner-title">协同创新 共建共享<br>整合医学发展新格局</h2>
|
|
||||||
<div class="banner-divider"></div>
|
|
||||||
<p class="banner-desc">
|
|
||||||
聚焦医学整合创新,系统推进七大专项计划,<br>
|
|
||||||
全面构建覆盖诊疗、科研、人才、管理、公益、<br>
|
|
||||||
政学协作与组织建设的协同发展体系。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧登录卡片 -->
|
<!-- 右侧登录卡片 -->
|
||||||
@@ -175,6 +166,7 @@ import { reactive, ref, onMounted, onUnmounted } from 'vue'
|
|||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
import loginBg from '@/assets/login.jpg'
|
||||||
import { login, getInfo, getCaptcha, sendLoginSms, smsLogin } from '@/api/auth'
|
import { login, getInfo, getCaptcha, sendLoginSms, smsLogin } from '@/api/auth'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { useAsyncLock } from '@/utils/useAsyncLock'
|
import { useAsyncLock } from '@/utils/useAsyncLock'
|
||||||
@@ -279,13 +271,13 @@ async function afterLogin(token, displayName) {
|
|||||||
ElMessage.success(`欢迎,${displayName}`)
|
ElMessage.success(`欢迎,${displayName}`)
|
||||||
// 角色不在角色首页映射里 → 拒绝
|
// 角色不在角色首页映射里 → 拒绝
|
||||||
if (!roleHome[role]) return router.replace({ name: 'login' })
|
if (!roleHome[role]) return router.replace({ name: 'login' })
|
||||||
// 所有角色统一跳门户首页 '/' (业务方 2026-08-22 要求, 各自角色菜单从导航栏进入)
|
|
||||||
// 带 redirect 回跳 (401/守卫带过来的原页面), 但必须属于当前角色 (否则跳过去被踢回 login)
|
// 带 redirect 回跳 (401/守卫带过来的原页面), 但必须属于当前角色 (否则跳过去被踢回 login)
|
||||||
const redirect = route.query.redirect
|
const redirect = route.query.redirect
|
||||||
if (redirect && redirectBelongsToRole(String(redirect), role)) {
|
if (redirect && redirectBelongsToRole(String(redirect), role)) {
|
||||||
return router.replace(String(redirect))
|
return router.replace(String(redirect))
|
||||||
}
|
}
|
||||||
router.replace('/')
|
// 登录默认跳各自角色工作台首页 (不再跳门户首页 '/')
|
||||||
|
router.replace(roleHome[role])
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchMode(mode) {
|
function switchMode(mode) {
|
||||||
@@ -351,7 +343,7 @@ const roleHome = {
|
|||||||
admin: '/admin/workbench',
|
admin: '/admin/workbench',
|
||||||
manager: '/manager/workbench',
|
manager: '/manager/workbench',
|
||||||
doctor: '/doctor/home',
|
doctor: '/doctor/home',
|
||||||
executor: '/executor/submissions',
|
executor: '/executor/overview',
|
||||||
sponsor: '/sponsor/home',
|
sponsor: '/sponsor/home',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,16 +481,25 @@ onUnmounted(() => {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body { margin: 0; padding: 0; }
|
||||||
|
/* PortalLayout 的 padding-top: 72px 对登录页是冗余的 (登录页有自己的 header 占位), 上移抵消 */
|
||||||
|
.login-page { margin-top: -72px; }
|
||||||
|
|
||||||
|
/* mobile 下 PortalLayout padding-top 收窄到 56px, 这里同步收窄, 让 login header 完整露出不被裁剪 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.login-page { margin-top: -56px; }
|
||||||
|
}
|
||||||
|
|
||||||
.login-page{
|
.login-page{
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
color: #333;
|
color: #333;
|
||||||
background: #f5f6f8;
|
background: linear-gradient(135deg, #f5f7fb 0%, #eef2f7 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部导航 */
|
/* 顶部导航: 背景延展到视口左右边缘, 内容保留 60px 内边距 */
|
||||||
.login-page .header{
|
.login-page .header{
|
||||||
height: 64px;
|
height: 64px;
|
||||||
padding: 0 60px;
|
padding: 0 60px;
|
||||||
@@ -506,6 +507,7 @@ onUnmounted(() => {
|
|||||||
border-bottom: 1px solid #e5e7eb;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page .logo{
|
.login-page .logo{
|
||||||
@@ -555,93 +557,65 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 40px 60px;
|
padding: 60px 80px;
|
||||||
gap: 60px;
|
gap: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page .banner-section{
|
.login-page .banner-section{
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 560px;
|
max-width: 640px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page .banner-inner{
|
.login-page .banner-inner{
|
||||||
background: var(--brand-primary);
|
position: relative;
|
||||||
color: #fff;
|
background-color: var(--brand-primary);
|
||||||
padding: 56px 56px;
|
background-size: cover;
|
||||||
height: 440px;
|
background-position: center;
|
||||||
display: flex;
|
background-repeat: no-repeat;
|
||||||
flex-direction: column;
|
height: 520px;
|
||||||
justify-content: center;
|
border-radius: 16px;
|
||||||
}
|
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.18);
|
||||||
|
overflow: hidden;
|
||||||
.login-page .banner-tag{
|
|
||||||
display: inline-block;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-page .banner-title{
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.4;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-page .banner-divider{
|
|
||||||
width: 48px;
|
|
||||||
height: 2px;
|
|
||||||
background: #fff;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-page .banner-desc{
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.9;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 登录卡片 */
|
/* 登录卡片 */
|
||||||
.login-page .login-section{
|
.login-page .login-section{
|
||||||
width: 400px;
|
width: 440px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page .login-card{
|
.login-page .login-card{
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
padding: 40px 40px;
|
padding: 48px 44px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page .login-title{
|
.login-page .login-title{
|
||||||
font-size: 22px;
|
font-size: 26px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 10px;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page .login-subtitle{
|
.login-page .login-subtitle{
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
margin-bottom: 28px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 登录模式切换 tabs */
|
/* 登录模式切换 tabs */
|
||||||
.login-page .login-tabs{
|
.login-page .login-tabs{
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 24px;
|
gap: 28px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 28px;
|
||||||
border-bottom: 1px solid #e5e7eb;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
}
|
}
|
||||||
.login-page .login-tabs .tab{
|
.login-page .login-tabs .tab{
|
||||||
padding: 8px 0;
|
padding: 10px 0;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
@@ -657,24 +631,26 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-page .form-group{
|
.login-page .form-group{
|
||||||
margin-bottom: 18px;
|
margin-bottom: 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page .form-input{
|
.login-page .form-input{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 44px;
|
height: 50px;
|
||||||
padding: 0 14px 0 42px;
|
padding: 0 16px 0 46px;
|
||||||
border: 1px solid #d1d5db;
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page .form-input:focus{
|
.login-page .form-input:focus{
|
||||||
border-color: var(--brand-primary);
|
border-color: var(--brand-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page .form-input::placeholder{
|
.login-page .form-input::placeholder{
|
||||||
@@ -683,14 +659,16 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.login-page .input-icon{
|
.login-page .input-icon{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 14px;
|
left: 16px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 16px;
|
width: 18px;
|
||||||
height: 16px;
|
height: 18px;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
.login-page .form-group:focus-within .input-icon{ color: var(--brand-primary); }
|
||||||
|
|
||||||
.login-page .captcha-group{
|
.login-page .captcha-group{
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -711,14 +689,15 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-page .captcha-image{
|
.login-page .captcha-image{
|
||||||
width: 110px;
|
width: 120px;
|
||||||
height: 44px;
|
height: 50px;
|
||||||
border: 1px solid #d1d5db;
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 20px;
|
font-size: 22px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 4px;
|
letter-spacing: 4px;
|
||||||
color: var(--brand-primary);
|
color: var(--brand-primary);
|
||||||
@@ -730,8 +709,8 @@ onUnmounted(() => {
|
|||||||
.login-page .form-bottom-links{
|
.login-page .form-bottom-links{
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: 14px;
|
margin-top: 16px;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page .form-bottom-links .bottom-link{
|
.login-page .form-bottom-links .bottom-link{
|
||||||
@@ -745,20 +724,28 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.login-page .login-btn{
|
.login-page .login-btn{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 44px;
|
height: 52px;
|
||||||
background: var(--brand-primary);
|
background: var(--brand-primary);
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 15px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
letter-spacing: 4px;
|
letter-spacing: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-top: 8px;
|
margin-top: 12px;
|
||||||
transition: background 0.2s;
|
border-radius: 8px;
|
||||||
|
transition: background 0.2s, transform 0.2s, box-shadow 0.2s;
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page .login-btn:hover{
|
.login-page .login-btn:hover{
|
||||||
background: var(--brand-primary-deep);
|
background: var(--brand-primary-deep);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page .login-btn:active{
|
||||||
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部 */
|
/* 底部 */
|
||||||
@@ -792,7 +779,9 @@ onUnmounted(() => {
|
|||||||
padding: 32px 36px;
|
padding: 32px 36px;
|
||||||
width: 440px;
|
width: 440px;
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
|
border-radius: 12px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page .modal-title{
|
.login-page .modal-title{
|
||||||
@@ -880,12 +869,12 @@ onUnmounted(() => {
|
|||||||
@media (min-width: 769px) and (max-width: 1024px) {
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
.main-content {
|
.main-content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 30px 30px;
|
padding: 40px 40px;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
}
|
}
|
||||||
.banner-section { width: 100%; max-width: 100%; }
|
.banner-section { width: 100%; max-width: 100%; }
|
||||||
.banner-inner { height: auto; padding: 40px 30px; }
|
.banner-inner { height: 360px; }
|
||||||
.login-section { width: 100%; max-width: 400px; }
|
.login-section { width: 100%; max-width: 440px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
@@ -895,9 +884,8 @@ onUnmounted(() => {
|
|||||||
.footer { padding: 16px 20px; }
|
.footer { padding: 16px 20px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === 用户要求: 左侧保持 440px 不动, 右侧登录卡片高度追平左侧, 两栏在 main-content 中居中 === */
|
/* === 用户要求: 两栏在 main-content 中居中, 卡片高度自适应 === */
|
||||||
.login-page .main-content { align-items: center; }
|
.login-page .main-content { align-items: center; }
|
||||||
.login-page .login-card { height: 440px; display: flex; flex-direction: column; justify-content: center; }
|
|
||||||
|
|
||||||
/* === 注册选择类别弹窗 (原型 1:1 抄 /home/john/ry8080/proto/html/登录.html) === */
|
/* === 注册选择类别弹窗 (原型 1:1 抄 /home/john/ry8080/proto/html/登录.html) === */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
@@ -916,6 +904,7 @@ onUnmounted(() => {
|
|||||||
width: 440px;
|
width: 440px;
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
.modal-title { font-size: 18px; font-weight: 600; color: #1a1a1a; margin-bottom: 20px; text-align: center; }
|
.modal-title { font-size: 18px; font-weight: 600; color: #1a1a1a; margin-bottom: 20px; text-align: center; }
|
||||||
@@ -943,6 +932,7 @@ onUnmounted(() => {
|
|||||||
flex: 1; height: 40px;
|
flex: 1; height: 40px;
|
||||||
border: none; cursor: pointer;
|
border: none; cursor: pointer;
|
||||||
font-size: 14px; font-weight: 500;
|
font-size: 14px; font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
.modal-btn.secondary { background: #f5f7fa; color: #606266; }
|
.modal-btn.secondary { background: #f5f7fa; color: #606266; }
|
||||||
@@ -968,12 +958,13 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
.sms-btn {
|
.sms-btn {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 110px;
|
width: 120px;
|
||||||
height: 44px;
|
height: 50px;
|
||||||
border: 1px solid #d1d5db;
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
color: var(--brand-primary);
|
color: var(--brand-primary);
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
@@ -992,6 +983,6 @@ onUnmounted(() => {
|
|||||||
.login-page .main-content { flex-direction: column !important; padding: 20px !important; gap: 0 !important; }
|
.login-page .main-content { flex-direction: column !important; padding: 20px !important; gap: 0 !important; }
|
||||||
.login-page .banner-section { display: none !important; }
|
.login-page .banner-section { display: none !important; }
|
||||||
.login-page .login-section { width: 100% !important; max-width: 100% !important; padding: 0 !important; flex: 0 0 auto !important; }
|
.login-page .login-section { width: 100% !important; max-width: 100% !important; padding: 0 !important; flex: 0 0 auto !important; }
|
||||||
.login-page .login-card { width: calc(100% - 10px) !important; max-width: none !important; margin: 0 auto !important; height: auto !important; min-height: 0 !important; padding: 24px 20px !important; }
|
.login-page .login-card { width: calc(100% - 10px) !important; max-width: none !important; margin: 0 auto !important; height: auto !important; min-height: 0 !important; padding: 24px 20px !important; box-shadow: none !important; border: none !important; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -37,6 +37,10 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="联系人姓名" prop="realName">
|
||||||
|
<el-input v-model="form.realName" placeholder="请输入联系人姓名" maxlength="30" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="手机号码" prop="phone">
|
<el-form-item label="手机号码" prop="phone">
|
||||||
<el-input v-model="form.phone" placeholder="请输入手机号码" maxlength="11" />
|
<el-input v-model="form.phone" placeholder="请输入手机号码" maxlength="11" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -110,6 +114,7 @@ const agreed = ref(false)
|
|||||||
const form = reactive({
|
const form = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
orgId: null,
|
orgId: null,
|
||||||
|
realName: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
smsCode: '',
|
smsCode: '',
|
||||||
password: '',
|
password: '',
|
||||||
@@ -123,6 +128,7 @@ const rules = {
|
|||||||
{ pattern: /^[A-Za-z0-9_]+$/, message: '只能包含字母/数字/下划线', trigger: 'blur' }
|
{ pattern: /^[A-Za-z0-9_]+$/, message: '只能包含字母/数字/下划线', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
orgId: [{ required: true, message: '请选择企业', trigger: 'change' }],
|
orgId: [{ required: true, message: '请选择企业', trigger: 'change' }],
|
||||||
|
realName: [{ required: true, message: '请输入联系人姓名', trigger: 'blur' }],
|
||||||
phone: [
|
phone: [
|
||||||
{ required: true, message: '请输入手机号码', trigger: 'blur' },
|
{ required: true, message: '请输入手机号码', trigger: 'blur' },
|
||||||
{ pattern: /^1\d{10}$/, message: '手机号格式错误', trigger: 'blur' }
|
{ pattern: /^1\d{10}$/, message: '手机号格式错误', trigger: 'blur' }
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ async function loadUserProfile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
form.name = store.user?.userName || ''
|
form.name = store.user?.nickName || ''
|
||||||
form.phone = store.user?.phonenumber || ''
|
form.phone = store.user?.phonenumber || ''
|
||||||
await Promise.all([loadUserProfile(), loadExpertProfile()])
|
await Promise.all([loadUserProfile(), loadExpertProfile()])
|
||||||
snapshot = ref(JSON.parse(JSON.stringify(form)))
|
snapshot = ref(JSON.parse(JSON.stringify(form)))
|
||||||
|
|||||||
@@ -14,24 +14,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 待参加的会议 + 待签署的协议: 仅审核通过的医生可见 -->
|
<!-- 待签署的协议: 仅审核通过的医生可见 -->
|
||||||
<div class="cols-row" v-if="store.expertAuditApproved">
|
<div class="section" v-if="store.expertAuditApproved">
|
||||||
<div class="section">
|
|
||||||
<h2 class="section-title">
|
|
||||||
待参加的会议
|
|
||||||
<a class="more" @click.prevent="$router.push('/doctor/meetings')">更多 →</a>
|
|
||||||
</h2>
|
|
||||||
<ul class="simple-list">
|
|
||||||
<li class="simple-item" v-for="m in upcomingMeetings" :key="m.meetingId" @click="$router.push('/doctor/meetings')">
|
|
||||||
<div class="item-main">
|
|
||||||
<span class="item-title">{{ m.meetingName || m.title }}</span>
|
|
||||||
</div>
|
|
||||||
<span class="item-status">{{ formatTime(m.startTime) }}</span>
|
|
||||||
</li>
|
|
||||||
<li v-if="!upcomingMeetings.length" class="empty">暂无待参加会议</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="section">
|
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
待签署的协议
|
待签署的协议
|
||||||
<a class="more" @click.prevent="$router.push('/doctor/submissions')">更多 →</a>
|
<a class="more" @click.prevent="$router.push('/doctor/submissions')">更多 →</a>
|
||||||
@@ -52,7 +36,6 @@
|
|||||||
<li v-if="!pendingAgreements.length" class="empty">暂无待签协议</li>
|
<li v-if="!pendingAgreements.length" class="empty">暂无待签协议</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 通知消息 (NoticeList 组件内部已含 SSE 实时刷新 + 详情 dialog + 全部已读) -->
|
<!-- 通知消息 (NoticeList 组件内部已含 SSE 实时刷新 + 详情 dialog + 全部已读) -->
|
||||||
<section class="section">
|
<section class="section">
|
||||||
@@ -60,7 +43,7 @@
|
|||||||
通知消息
|
通知消息
|
||||||
<a class="more" @click.prevent="$router.push('/doctor/messages')">更多 →</a>
|
<a class="more" @click.prevent="$router.push('/doctor/messages')">更多 →</a>
|
||||||
</h2>
|
</h2>
|
||||||
<NoticeList :limit="5" :show-header="false" />
|
<NoticeList :pageable="true" :show-header="false" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 二维码弹窗 -->
|
<!-- 二维码弹窗 -->
|
||||||
@@ -87,18 +70,17 @@ import { useUserStore } from '@/store/user'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
import { getMyExpertProfile } from '@/api/business/expert'
|
import { getMyExpertProfile } from '@/api/business/expert'
|
||||||
import { listUnsignedMeetingProtocols, listInvitedMeetings } from '@/api/business/meetingAttendee'
|
import { listUnsignedMeetingProtocols } from '@/api/business/meetingAttendee'
|
||||||
import NoticeList from '@/components/NoticeList.vue'
|
import NoticeList from '@/components/NoticeList.vue'
|
||||||
|
|
||||||
const store = useUserStore()
|
const store = useUserStore()
|
||||||
|
|
||||||
const upcomingMeetings = ref([])
|
|
||||||
const pendingAgreements = ref([])
|
const pendingAgreements = ref([])
|
||||||
// 专家真实姓名 (从 biz_expert.name 拿, 不显示 sys_user.userName (登录账号/手机号))
|
// 专家真实姓名 (从 biz_expert.name 拿, 不显示 sys_user.userName (登录账号/手机号))
|
||||||
const expertName = ref('')
|
const expertName = ref('')
|
||||||
// 兜底显示名: 优先专家真实姓名 > nickName > userName > '专家'
|
// 兜底显示名: 优先专家真实姓名 > nickName > userName > '专家'
|
||||||
const displayName = computed(() =>
|
const displayName = computed(() =>
|
||||||
expertName.value || store.user?.nickName || store.user?.userName || '专家'
|
expertName.value || store.user?.nickName || '专家'
|
||||||
)
|
)
|
||||||
|
|
||||||
const nowTime = ref('')
|
const nowTime = ref('')
|
||||||
@@ -154,16 +136,6 @@ async function copyQrcodeUrl() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(t) {
|
|
||||||
if (!t) return ''
|
|
||||||
const d = new Date(t)
|
|
||||||
const today = new Date()
|
|
||||||
const diff = Math.floor((d - today) / 86400000)
|
|
||||||
if (diff === 0) return d.toTimeString().slice(0, 5)
|
|
||||||
if (diff === 1) return '明天'
|
|
||||||
return `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateClock() {
|
function updateClock() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
nowTime.value = now.toTimeString().slice(0, 5)
|
nowTime.value = now.toTimeString().slice(0, 5)
|
||||||
@@ -177,13 +149,8 @@ async function load() {
|
|||||||
expertName.value = data?.name || ''
|
expertName.value = data?.name || ''
|
||||||
} catch (e) { expertName.value = '' }
|
} catch (e) { expertName.value = '' }
|
||||||
|
|
||||||
// 待参加会议 + 待签署协议: 仅审核通过的医生才拉 (未通过时 2 个 pannel 隐藏)
|
// 待签署协议: 仅审核通过的医生才拉 (未通过时 panel 隐藏)
|
||||||
if (store.expertAuditApproved) {
|
if (store.expertAuditApproved) {
|
||||||
try {
|
|
||||||
const { data } = await listInvitedMeetings()
|
|
||||||
upcomingMeetings.value = (Array.isArray(data) ? data : []).slice(0, 5)
|
|
||||||
} catch (e) { upcomingMeetings.value = [] }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await listUnsignedMeetingProtocols()
|
const { data } = await listUnsignedMeetingProtocols()
|
||||||
pendingAgreements.value = (data || []).slice(0, 5).map(s => ({
|
pendingAgreements.value = (data || []).slice(0, 5).map(s => ({
|
||||||
@@ -195,7 +162,6 @@ async function load() {
|
|||||||
}))
|
}))
|
||||||
} catch (e) { pendingAgreements.value = [] }
|
} catch (e) { pendingAgreements.value = [] }
|
||||||
} else {
|
} else {
|
||||||
upcomingMeetings.value = []
|
|
||||||
pendingAgreements.value = []
|
pendingAgreements.value = []
|
||||||
}
|
}
|
||||||
// 通知列表已抽到 <NoticeList> 组件, 本页不再处理
|
// 通知列表已抽到 <NoticeList> 组件, 本页不再处理
|
||||||
@@ -215,7 +181,7 @@ onBeforeUnmount(() => {
|
|||||||
.doctor-home { padding: 16px 20px; }
|
.doctor-home { padding: 16px 20px; }
|
||||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||||
.welcome-bar { background: var(--brand-primary); border-radius: 4px; padding: 20px 24px; color: #fff; display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
.welcome-bar { background: var(--brand-primary); border-radius: 4px; padding: 20px 24px; color: #fff; display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
||||||
.welcome-text h2 { font-size: 20px; font-weight: 600; margin-bottom: 6px; }
|
.welcome-text h2 { font-size: 20px; font-weight: 600; margin-bottom: 6px; color: #fff; }
|
||||||
.welcome-text p { font-size: 13px; opacity: 0.85; }
|
.welcome-text p { font-size: 13px; opacity: 0.85; }
|
||||||
.welcome-time .now { font-size: 14px; font-weight: 500; }
|
.welcome-time .now { font-size: 14px; font-weight: 500; }
|
||||||
.welcome-time .date { font-size: 12px; opacity: 0.75; margin-top: 4px; }
|
.welcome-time .date { font-size: 12px; opacity: 0.75; margin-top: 4px; }
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<el-option v-for="o in STAGE_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
<el-option v-for="o in STAGE_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item><el-button type="primary" @click="load">查找</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
<el-form-item><el-button type="primary" @click="load">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<el-table :data="rows" v-loading="loading" stripe border>
|
<el-table :data="rows" v-loading="loading" stripe border>
|
||||||
@@ -23,12 +23,20 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="日程" width="130" align="center">
|
<el-table-column label="日程" width="130" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<div class="table-actions">
|
||||||
<el-link :underline="false" type="primary" :disabled="!row.scheduleUrl" @click="onPreview(row.scheduleUrl, '日程海报')">查看</el-link>
|
<el-link :underline="false" type="primary" :disabled="!row.scheduleUrl" @click="onPreview(row.scheduleUrl, '日程海报')">查看</el-link>
|
||||||
<el-link :underline="false" :disabled="!row.scheduleUrl" @click="onDownload(row.scheduleUrl, `${row.meetingName || '会议'}_日程海报`)">下载</el-link></template></el-table-column>
|
<el-link :underline="false" :disabled="!row.scheduleUrl" @click="onDownload(row.scheduleUrl, `${row.meetingName || '会议'}_日程海报`)">下载</el-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="邀请函" width="130" align="center">
|
<el-table-column label="邀请函" width="130" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<div class="table-actions">
|
||||||
<el-link :underline="false" type="primary" :disabled="!row.projectInvitationUrl" @click="onPreview(row.projectInvitationUrl, '邀请函')">查看</el-link>
|
<el-link :underline="false" type="primary" :disabled="!row.projectInvitationUrl" @click="onPreview(row.projectInvitationUrl, '邀请函')">查看</el-link>
|
||||||
<el-link :underline="false" :disabled="!row.projectInvitationUrl" @click="onDownload(row.projectInvitationUrl, `${row.meetingName || '会议'}_邀请函`)">下载</el-link></template></el-table-column>
|
<el-link :underline="false" :disabled="!row.projectInvitationUrl" @click="onDownload(row.projectInvitationUrl, `${row.meetingName || '会议'}_邀请函`)">下载</el-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="签署状态" width="100" align="center">
|
<el-table-column label="签署状态" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.attendeeLaborProtocol ? 'success' : 'info'" size="small">{{ row.attendeeLaborProtocol ? '已签署' : '未签署' }}</el-tag>
|
<el-tag :type="row.attendeeLaborProtocol ? 'success' : 'info'" size="small">{{ row.attendeeLaborProtocol ? '已签署' : '未签署' }}</el-tag>
|
||||||
|
|||||||
@@ -6,13 +6,21 @@
|
|||||||
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
|
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
|
||||||
<el-form-item label="项目名称"><el-input v-model="q.projectName" placeholder="输入项目名称" clearable /></el-form-item>
|
<el-form-item label="项目名称"><el-input v-model="q.projectName" placeholder="输入项目名称" clearable /></el-form-item>
|
||||||
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="输入会议名称" clearable /></el-form-item>
|
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="输入会议名称" clearable /></el-form-item>
|
||||||
<el-form-item><el-button type="primary" @click="load">查找</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
<el-form-item><el-button type="primary" @click="load">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<el-table :data="rows" v-loading="loading" stripe border>
|
<el-table :data="rows" v-loading="loading" stripe border>
|
||||||
<el-table-column prop="projectNo" label="项目编号" width="170" />
|
<el-table-column prop="projectNo" label="项目编号" width="170" />
|
||||||
<el-table-column prop="projectName" label="项目名称" min-width="240" show-overflow-tooltip />
|
<el-table-column prop="projectName" label="项目名称" min-width="240" show-overflow-tooltip>
|
||||||
<el-table-column prop="meetingName" label="会议名称" min-width="220" show-overflow-tooltip />
|
<template #default="{ row }">
|
||||||
|
<el-link :underline="false" type="primary" @click="onView(row)">{{ row.projectName }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="meetingName" label="会议名称" min-width="220" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link :underline="false" type="primary" @click="onView(row)">{{ row.meetingName }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="100">
|
<el-table-column label="状态" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="status">{{ row.status || '已报名' }}</span>
|
<span class="status">{{ row.status || '已报名' }}</span>
|
||||||
|
|||||||
@@ -5,7 +5,20 @@
|
|||||||
<el-button type="primary" @click="goBack">返回</el-button>
|
<el-button type="primary" @click="goBack">返回</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else v-loading="loading">
|
<div v-else v-loading="loading">
|
||||||
<el-form :model="form" :rules="rules" ref="formRef" label-position="top" class="sign-fill-form">
|
<!-- 已签署: 再次打开签署链接, 直接展示劳务协议 PDF (复用 publicity 的 OSS 代理预览) -->
|
||||||
|
<div v-if="signed" class="signed-pdf">
|
||||||
|
<div class="sign-header">
|
||||||
|
<div class="sign-header-title">{{ meetingName || '劳务协议' }}</div>
|
||||||
|
<div v-if="periodDisplay" class="sign-header-period">期数:{{ periodDisplay }}</div>
|
||||||
|
</div>
|
||||||
|
<iframe v-if="laborPdfUrl" :src="proxyUrl(laborPdfUrl)" class="signed-pdf-frame"></iframe>
|
||||||
|
<div v-else class="signed-pdf-empty">协议已签署,暂无可展示的 PDF</div>
|
||||||
|
<div class="signed-pdf-actions">
|
||||||
|
<el-button v-if="laborPdfUrl" type="primary" @click="downloadPdf">下载劳务协议</el-button>
|
||||||
|
<el-button @click="goBack">返回</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-form v-else :model="form" :rules="rules" ref="formRef" label-position="top" class="sign-fill-form">
|
||||||
<!-- 大标题: 会议名称 + 期数 -->
|
<!-- 大标题: 会议名称 + 期数 -->
|
||||||
<div class="sign-header">
|
<div class="sign-header">
|
||||||
<div class="sign-header-title">{{ meetingName || '劳务协议签署' }}</div>
|
<div class="sign-header-title">{{ meetingName || '劳务协议签署' }}</div>
|
||||||
@@ -106,6 +119,9 @@ const notInvited = ref(false)
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
|
// 已签署状态: 再次打开签署链接时直接展示 PDF
|
||||||
|
const signed = ref(false)
|
||||||
|
const laborPdfUrl = ref('')
|
||||||
|
|
||||||
// 劳务形式 (checkboxOther 多选)
|
// 劳务形式 (checkboxOther 多选)
|
||||||
const laborFormOptions = ref([])
|
const laborFormOptions = ref([])
|
||||||
@@ -173,7 +189,7 @@ async function ensureLogin() {
|
|||||||
userStore.setUser({
|
userStore.setUser({
|
||||||
userId: u.userId,
|
userId: u.userId,
|
||||||
userName: u.userName || u.nickName || '',
|
userName: u.userName || u.nickName || '',
|
||||||
nickName: u.nickName || u.userName || '',
|
nickName: u.nickName || '',
|
||||||
phonenumber: u.phonenumber || '',
|
phonenumber: u.phonenumber || '',
|
||||||
accountType: u.accountType || 'MAIN',
|
accountType: u.accountType || 'MAIN',
|
||||||
parentUserId: u.parentUserId || null,
|
parentUserId: u.parentUserId || null,
|
||||||
@@ -231,6 +247,21 @@ async function load() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const { data } = await getSignInfo(attendeeId.value)
|
const { data } = await getSignInfo(attendeeId.value)
|
||||||
|
// 已签署: 再次打开签署链接 → 整页跳转到 OSS proxy 的 PDF 地址 (全屏显示, 不用 iframe 内嵌)
|
||||||
|
if (data.signed || (data.laborProtocol && data.laborProtocol.trim())) {
|
||||||
|
const url = data.laborProtocol
|
||||||
|
if (url) {
|
||||||
|
window.location.href = proxyUrl(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 无 PDF URL 兜底: 仍留在本页显示"已签署但无可展示 PDF"
|
||||||
|
signed.value = true
|
||||||
|
laborPdfUrl.value = ''
|
||||||
|
meetingName.value = data.meetingName || ''
|
||||||
|
periodNo.value = data.periodNo ?? null
|
||||||
|
totalPeriods.value = data.totalPeriods ?? null
|
||||||
|
return
|
||||||
|
}
|
||||||
// 预填 (current 优先, 否则用 defaults)
|
// 预填 (current 优先, 否则用 defaults)
|
||||||
const cur = data.current || {}
|
const cur = data.current || {}
|
||||||
const def = data.defaults || {}
|
const def = data.defaults || {}
|
||||||
@@ -326,6 +357,23 @@ async function onSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OSS 代理预览 (同 publicity / doctor/Meetings.vue): 重写 Content-Disposition 为 inline, 隐藏工具栏撑满宽度
|
||||||
|
function proxyUrl(url) {
|
||||||
|
if (!url) return url
|
||||||
|
if (url.includes('hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com')) {
|
||||||
|
return import.meta.env.VITE_APP_BASE_API + '/common/oss/proxy?url=' + encodeURIComponent(url) + '#toolbar=0&zoom=page-width'
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
function downloadPdf() {
|
||||||
|
if (!laborPdfUrl.value) return
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = laborPdfUrl.value
|
||||||
|
a.download = `${meetingName.value || '劳务协议'}.pdf`
|
||||||
|
a.target = '_blank'
|
||||||
|
document.body.appendChild(a); a.click(); document.body.removeChild(a)
|
||||||
|
}
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
router.push('/doctor/home')
|
router.push('/doctor/home')
|
||||||
}
|
}
|
||||||
@@ -335,13 +383,20 @@ onMounted(init)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 手机端全屏表单 (无 navbar / 无 page-card) */
|
/* 手机端全屏表单 (无 navbar / 无 page-card) */
|
||||||
.sign-fill { padding: 16px; }
|
.sign-fill { padding: 0; }
|
||||||
|
.sign-fill-form { padding: 16px; }
|
||||||
|
|
||||||
/* 大标题: 会议名称 + 期数 */
|
/* 大标题: 会议名称 + 期数 */
|
||||||
.sign-header { margin: 4px 0 20px; padding-bottom: 14px; border-bottom: 1px solid #f0f0f0; }
|
.sign-header { margin: 4px 0 20px; padding-bottom: 14px; border-bottom: 1px solid #f0f0f0; }
|
||||||
.sign-header-title { font-size: 20px; font-weight: 600; color: #1a1a1a; line-height: 1.4; }
|
.sign-header-title { font-size: 20px; font-weight: 600; color: #1a1a1a; line-height: 1.4; }
|
||||||
.sign-header-period { margin-top: 6px; font-size: 14px; color: #595959; }
|
.sign-header-period { margin-top: 6px; font-size: 14px; color: #595959; }
|
||||||
|
|
||||||
|
/* 已签署: 直接展示 PDF (宽度 100%, 高度不限制) */
|
||||||
|
.signed-pdf { display: flex; flex-direction: column; }
|
||||||
|
.signed-pdf-frame { width: 100%; height: 424vw; border: 0; }
|
||||||
|
.signed-pdf-empty { padding: 48px 16px; text-align: center; color: #909399; }
|
||||||
|
.signed-pdf-actions { margin-top: 16px; display: flex; gap: 12px; }
|
||||||
|
|
||||||
/* 未受邀提示 */
|
/* 未受邀提示 */
|
||||||
.not-invited { padding: 60px 20px; text-align: center; }
|
.not-invited { padding: 60px 20px; text-align: center; }
|
||||||
.not-invited-text { font-size: 16px; color: #606266; margin-bottom: 20px; }
|
.not-invited-text { font-size: 16px; color: #606266; margin-bottom: 20px; }
|
||||||
|
|||||||
@@ -115,9 +115,12 @@ onMounted(load)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 整页面板 (详情页标准: max-width 1200px) */
|
/* 整页面板 (与 Submissions.vue 列表页风格一致: 16px/20px padding, 6px 圆角, 1px 浅灰描边) */
|
||||||
.doctor-submission-detail { max-width: 1200px; padding: 16px; }
|
.doctor-submission-detail { background: #fff; max-width: 1200px; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 20px; }
|
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||||
|
/* 主按钮品牌色 (与列表页保持一致) */
|
||||||
|
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
|
||||||
|
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
|
||||||
|
|
||||||
/* 只读表单样式 - 模拟 el-input 视觉, 但只显示文字 */
|
/* 只读表单样式 - 模拟 el-input 视觉, 但只显示文字 */
|
||||||
.readonly-form :deep(.el-form-item) { margin-bottom: 22px; }
|
.readonly-form :deep(.el-form-item) { margin-bottom: 22px; }
|
||||||
@@ -140,84 +143,71 @@ onMounted(load)
|
|||||||
|
|
||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
移动端适配 (≤768px)
|
移动端适配 (≤768px) — 参考 executor/meetings/new MeetingNew.vue
|
||||||
- 卡片 padding 收窄
|
不动 el-form-item__label 内部样式 (float/width/height), 只改容器布局,
|
||||||
- 工具栏横向滚动
|
让 Element Plus 自带的 label-width=120px 右对齐 + input 高度自然撑开 label
|
||||||
- filter-form: label 与输入框横向
|
|
||||||
- 表格字号收紧
|
|
||||||
======================================== */
|
======================================== */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* 卡片 padding */
|
/* 卡片 padding (与列表页统一) */
|
||||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
.doctor-submission-detail { padding: 12px !important; border-radius: 4px !important; }
|
||||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||||
|
|
||||||
/* 工具栏: 横向滚动 */
|
/* 双列 → 单列:
|
||||||
.toolbar {
|
- el-row 强制 block (避免 flex 横排)
|
||||||
flex-wrap: nowrap !important;
|
- el-col 强制 100% 宽
|
||||||
overflow-x: auto;
|
间距来源: 桌面样式 .readonly-form :deep(.el-form-item) { margin-bottom: 22px }
|
||||||
-webkit-overflow-scrolling: touch;
|
(第 126 行), 手机端直接继承, 不再额外加 padding 避免双倍间距 */
|
||||||
padding-bottom: 6px;
|
.readonly-form :deep(.el-row) {
|
||||||
margin-bottom: 8px !important;
|
display: block !important;
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
}
|
||||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
.readonly-form :deep(.el-col) {
|
||||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
width: 100% !important;
|
||||||
.toolbar :deep(.action-btn),
|
max-width: 100% !important;
|
||||||
.toolbar :deep(.el-button) {
|
flex: 0 0 100% !important;
|
||||||
flex-shrink: 0;
|
display: block !important;
|
||||||
font-size: 12px !important;
|
|
||||||
padding: 0 10px !important;
|
|
||||||
height: 30px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* filter-form: 横向 (label 左 + input 右) */
|
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||||||
.filter-form {
|
保留桌面已有的 22px margin-bottom (不要清零, 那是行间/字段间间距的关键),
|
||||||
|
改 flex-start 避免多行 label 被居中.
|
||||||
|
不动 __label 的 float/width/height/line-height — Element Plus 自带 label-width=120px
|
||||||
|
会自然右对齐 + 跟随 content 行高, 不会参差不齐 */
|
||||||
|
.readonly-form :deep(.el-form-item) {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: column !important;
|
align-items: flex-start !important;
|
||||||
align-items: stretch !important;
|
|
||||||
gap: 10px !important;
|
|
||||||
}
|
|
||||||
.filter-form :deep(.el-form-item) {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
}
|
||||||
.filter-form :deep(.el-form-item__label) {
|
.readonly-form :deep(.el-form-item__content) {
|
||||||
float: none !important;
|
|
||||||
width: auto !important;
|
|
||||||
min-width: 80px !important;
|
|
||||||
text-align: right !important;
|
|
||||||
padding: 0 8px 0 0 !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
color: var(--el-text-color-regular) !important;
|
|
||||||
line-height: 32px !important;
|
|
||||||
height: 32px !important;
|
|
||||||
}
|
|
||||||
.filter-form :deep(.el-form-item__content) {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
line-height: 32px !important;
|
|
||||||
flex: 1 !important;
|
flex: 1 !important;
|
||||||
min-width: 0 !important;
|
min-width: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 强制所有控件全宽 */
|
/* 只读控件全宽, display:block 让 textarea 也按 block 拉伸 */
|
||||||
.filter-form :deep(.el-select),
|
.readonly-form :deep(.el-input),
|
||||||
.filter-form :deep(.el-input),
|
.readonly-form :deep(.el-textarea) {
|
||||||
.filter-form :deep(.el-date-editor),
|
|
||||||
.filter-form :deep(.el-button),
|
|
||||||
.filter-form :deep(.el-cascader) {
|
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
min-width: 0 !important;
|
min-width: 0 !important;
|
||||||
margin-left: 0 !important;
|
|
||||||
margin-right: 0 !important;
|
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
.filter-form :deep(.el-button + .el-button) {
|
.readonly-cell { flex: 1; min-width: 0; font-size: 14px; }
|
||||||
margin-top: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表格字号收紧 */
|
/* 返回按钮 — 左右并排 (与 MeetingNew 的 form-actions 一致) */
|
||||||
:deep(.el-table) { font-size: 12px !important; }
|
.detail-actions {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
gap: 0 !important;
|
||||||
|
margin-top: 16px !important;
|
||||||
|
padding-top: 16px !important;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.detail-actions :deep(.el-button) {
|
||||||
|
flex: 1 1 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
.detail-actions :deep(.el-button + .el-button) {
|
||||||
|
margin-left: 8px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -60,12 +60,13 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
<el-form-item>
|
<!-- 操作按钮 — 从 el-form 提出来, 避免 Element Plus form-item 容器影响按钮布局 -->
|
||||||
|
<div class="detail-actions">
|
||||||
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
|
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
|
||||||
<el-button @click="confirmCancel">取消</el-button>
|
<el-button @click="confirmCancel">取消</el-button>
|
||||||
</el-form-item>
|
</div>
|
||||||
</el-form>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -198,90 +199,73 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 整页面板 (与 admin/ExpertNew.vue 一致: max-width 1200px) */
|
/* 整页面板 (与 Submissions.vue 列表页风格一致) */
|
||||||
.doctor-submission-new { max-width: 1200px; padding: 16px; }
|
.doctor-submission-new { background: #fff; max-width: 1200px; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||||
|
/* 主按钮品牌色 (与列表页保持一致) */
|
||||||
|
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
|
||||||
|
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
|
||||||
|
|
||||||
|
/* 操作按钮区 (从 el-form 提出来, 桌面/移动端一致布局) */
|
||||||
|
.detail-actions { display: flex; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f0f0f0; }
|
||||||
|
|
||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
移动端适配 (≤768px)
|
移动端适配 (≤768px) — 参考 executor/meetings/new MeetingNew
|
||||||
- 卡片 padding 收窄
|
不动 el-form-item__label 内部样式 (float/width/height), 让 Element Plus 自带
|
||||||
- 工具栏横向滚动
|
label-width=120px 右对齐 + input 高度自然撑开 label, 不会参差不齐
|
||||||
- filter-form: label 与输入框横向
|
|
||||||
- 表格字号收紧
|
|
||||||
======================================== */
|
======================================== */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* 卡片 padding */
|
/* 卡片 padding (与列表页统一) */
|
||||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
.doctor-submission-new { padding: 12px !important; border-radius: 4px !important; }
|
||||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||||
|
|
||||||
/* 工具栏: 横向滚动 */
|
/* 双列 → 单列: el-row 强制 block, el-col 100% 宽 */
|
||||||
.toolbar {
|
.doctor-submission-new :deep(.el-row) { display: block !important; }
|
||||||
flex-wrap: nowrap !important;
|
.doctor-submission-new :deep(.el-col) {
|
||||||
overflow-x: auto;
|
width: 100% !important;
|
||||||
-webkit-overflow-scrolling: touch;
|
max-width: 100% !important;
|
||||||
padding-bottom: 6px;
|
flex: 0 0 100% !important;
|
||||||
margin-bottom: 8px !important;
|
display: block !important;
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
|
||||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
|
||||||
.toolbar :deep(.action-btn),
|
|
||||||
.toolbar :deep(.el-button) {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 12px !important;
|
|
||||||
padding: 0 10px !important;
|
|
||||||
height: 30px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* filter-form: 横向 (label 左 + input 右) */
|
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||||||
.filter-form {
|
不动 __label 的 float/width/height/line-height,
|
||||||
|
不动 form-item 的 margin-bottom (桌面默认 0, 手机端继承, 间距靠 el-form-item 自带 + el-row 默认布局) */
|
||||||
|
.doctor-submission-new :deep(.el-form-item) {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: column !important;
|
align-items: flex-start !important;
|
||||||
align-items: stretch !important;
|
|
||||||
gap: 10px !important;
|
|
||||||
}
|
|
||||||
.filter-form :deep(.el-form-item) {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
}
|
||||||
.filter-form :deep(.el-form-item__label) {
|
.doctor-submission-new :deep(.el-form-item__content) {
|
||||||
float: none !important;
|
|
||||||
width: auto !important;
|
|
||||||
min-width: 80px !important;
|
|
||||||
text-align: right !important;
|
|
||||||
padding: 0 8px 0 0 !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
color: var(--el-text-color-regular) !important;
|
|
||||||
line-height: 32px !important;
|
|
||||||
height: 32px !important;
|
|
||||||
}
|
|
||||||
.filter-form :deep(.el-form-item__content) {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
line-height: 32px !important;
|
|
||||||
flex: 1 !important;
|
flex: 1 !important;
|
||||||
min-width: 0 !important;
|
min-width: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 强制所有控件全宽 */
|
/* 全部控件全宽, display:block 强制 select/input 也按 block 拉伸 (与列表页一致) */
|
||||||
.filter-form :deep(.el-select),
|
.doctor-submission-new :deep(.el-select),
|
||||||
.filter-form :deep(.el-input),
|
.doctor-submission-new :deep(.el-input),
|
||||||
.filter-form :deep(.el-date-editor),
|
.doctor-submission-new :deep(.el-textarea),
|
||||||
.filter-form :deep(.el-button),
|
.doctor-submission-new :deep(.el-date-editor),
|
||||||
.filter-form :deep(.el-cascader) {
|
.doctor-submission-new :deep(.el-cascader) {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
min-width: 0 !important;
|
min-width: 0 !important;
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
.filter-form :deep(.el-button + .el-button) {
|
|
||||||
margin-top: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表格字号收紧 */
|
/* 保存/取消按钮 — 独立 .detail-actions, flex 横排占满 */
|
||||||
:deep(.el-table) { font-size: 12px !important; }
|
.detail-actions {
|
||||||
|
display: flex !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
margin-top: 16px !important;
|
||||||
|
padding-top: 16px !important;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.detail-actions :deep(.el-button) {
|
||||||
|
flex: 1 1 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -22,14 +22,14 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态">
|
<el-form-item label="状态">
|
||||||
<el-select v-model="q.status" placeholder="请选择" clearable style="width: 140px">
|
<el-select v-model="q.status" placeholder="请选择" clearable style="width: 140px">
|
||||||
<el-option label="待提交" value="0" />
|
<el-option label="未提交" value="0" />
|
||||||
<el-option label="待审核" value="1" />
|
<el-option label="待审核" value="1" />
|
||||||
<el-option label="通过" value="2" />
|
<el-option label="通过" value="2" />
|
||||||
<el-option label="拒绝" value="3" />
|
<el-option label="拒绝" value="3" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="输入备注" clearable style="width: 200px" /></el-form-item>
|
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="输入备注" clearable style="width: 200px" /></el-form-item>
|
||||||
<el-form-item><el-button type="primary" @click="load">查找</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
<el-form-item><el-button type="primary" @click="load">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
@@ -48,8 +48,10 @@
|
|||||||
<el-table-column label="设计文件" width="140">
|
<el-table-column label="设计文件" width="140">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<template v-if="row.designFileUrl">
|
<template v-if="row.designFileUrl">
|
||||||
|
<div class="table-actions">
|
||||||
<el-link :underline="false" type="primary" @click="openPreview(row.designFileUrl, '设计文件')">查看</el-link>
|
<el-link :underline="false" type="primary" @click="openPreview(row.designFileUrl, '设计文件')">查看</el-link>
|
||||||
<el-link type="primary" :href="row.designFileUrl" target="_blank">下载</el-link>
|
<el-link type="primary" :href="row.designFileUrl" target="_blank">下载</el-link>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -59,6 +61,20 @@
|
|||||||
<audit-status-tag :status="row.status" />
|
<audit-status-tag :status="row.status" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="是否结算" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" :type="row.isSettled==='Y' ? 'success' : 'warning'">
|
||||||
|
{{ row.isSettled==='Y' ? '已结算' : '未结算' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="projectNo" label="项目编号" width="140" align="center" />
|
||||||
|
<el-table-column label="创建时间" width="160" align="center">
|
||||||
|
<template #default="{ row }">{{ row.createTime || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="审核意见" min-width="180" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.auditOpinion || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
<template #default="{ row }"><div class="table-actions">
|
<template #default="{ row }"><div class="table-actions">
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
|
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
|
||||||
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
|
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
|
||||||
<el-table-column label="操作" :width="isAdmin ? 320 : 240" fixed="right">
|
<el-table-column label="操作" :width="isAdmin ? 240 : 180" fixed="right">
|
||||||
<template #default="{ row }"><div class="table-actions">
|
<template #default="{ row }"><div class="table-actions">
|
||||||
<!-- 查看: 两角色都有 (manager 原版本有, admin 原版本用 alert, 这里统一用 dialog 更清晰) -->
|
<!-- 查看: 两角色都有 (manager 原版本有, admin 原版本用 alert, 这里统一用 dialog 更清晰) -->
|
||||||
<el-link :underline="false" type="primary" @click="onView(row)">查看</el-link>
|
<el-link :underline="false" type="primary" @click="onView(row)">查看</el-link>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status === '1' ? '禁用' : '正常' }}</el-tag>
|
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status === '1' ? '禁用' : '正常' }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="340" fixed="right">
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
<template #default="{ row }"><div class="table-actions">
|
<template #default="{ row }"><div class="table-actions">
|
||||||
<!-- 查看 (只读详情): 两角色都有, 跳独立详情页 -->
|
<!-- 查看 (只读详情): 两角色都有, 跳独立详情页 -->
|
||||||
<el-link :underline="false" type="primary" @click="goView(row)">查看</el-link>
|
<el-link :underline="false" type="primary" @click="goView(row)">查看</el-link>
|
||||||
|
|||||||
@@ -149,83 +149,33 @@ onMounted(loadDetail)
|
|||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
移动端适配 (≤768px)
|
移动端适配 (≤768px)
|
||||||
- 卡片 padding 收窄
|
|
||||||
- 工具栏横向滚动
|
|
||||||
- filter-form: label 与输入框横向
|
|
||||||
- 表格字号收紧
|
|
||||||
======================================== */
|
======================================== */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* 卡片 padding */
|
/* 卡片 padding */
|
||||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||||
|
|
||||||
/* 工具栏: 横向滚动 */
|
/* form-card 内部 padding 收窄 */
|
||||||
.toolbar {
|
:deep(.form-card .el-card__body) { padding: 12px !important; }
|
||||||
flex-wrap: nowrap !important;
|
|
||||||
overflow-x: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
padding-bottom: 6px;
|
|
||||||
margin-bottom: 8px !important;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
|
||||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
|
||||||
.toolbar :deep(.action-btn),
|
|
||||||
.toolbar :deep(.el-button) {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 12px !important;
|
|
||||||
padding: 0 10px !important;
|
|
||||||
height: 30px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* filter-form: 横向 (label 左 + input 右) */
|
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||||||
.filter-form {
|
不动 __label — Element Plus 自带 label-width=100px 自然对齐 */
|
||||||
|
:deep(.el-form-item) {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: column !important;
|
align-items: flex-start !important;
|
||||||
align-items: stretch !important;
|
|
||||||
gap: 10px !important;
|
|
||||||
}
|
|
||||||
.filter-form :deep(.el-form-item) {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
}
|
||||||
.filter-form :deep(.el-form-item__label) {
|
:deep(.el-form-item__content) {
|
||||||
float: none !important;
|
|
||||||
width: auto !important;
|
|
||||||
min-width: 80px !important;
|
|
||||||
text-align: right !important;
|
|
||||||
padding: 0 8px 0 0 !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
color: var(--el-text-color-regular) !important;
|
|
||||||
line-height: 32px !important;
|
|
||||||
height: 32px !important;
|
|
||||||
}
|
|
||||||
.filter-form :deep(.el-form-item__content) {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
line-height: 32px !important;
|
|
||||||
flex: 1 !important;
|
flex: 1 !important;
|
||||||
min-width: 0 !important;
|
min-width: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 强制所有控件全宽 */
|
/* 底部按钮: 占满整行 (返回按钮单独占一行) */
|
||||||
.filter-form :deep(.el-select),
|
.form-actions {
|
||||||
.filter-form :deep(.el-input),
|
margin-top: 12px !important;
|
||||||
.filter-form :deep(.el-date-editor),
|
}
|
||||||
.filter-form :deep(.el-button),
|
.form-actions :deep(.el-button) {
|
||||||
.filter-form :deep(.el-cascader) {
|
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
min-width: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
margin-right: 0 !important;
|
|
||||||
display: block !important;
|
|
||||||
}
|
}
|
||||||
.filter-form :deep(.el-button + .el-button) {
|
|
||||||
margin-top: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表格字号收紧 */
|
|
||||||
:deep(.el-table) { font-size: 12px !important; }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -185,83 +185,36 @@ onMounted(() => {
|
|||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
移动端适配 (≤768px)
|
移动端适配 (≤768px)
|
||||||
- 卡片 padding 收窄
|
|
||||||
- 工具栏横向滚动
|
|
||||||
- filter-form: label 与输入框横向
|
|
||||||
- 表格字号收紧
|
|
||||||
======================================== */
|
======================================== */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* 卡片 padding */
|
/* 卡片 padding */
|
||||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||||
|
|
||||||
/* 工具栏: 横向滚动 */
|
/* form-card 内部 padding 收窄 */
|
||||||
.toolbar {
|
:deep(.form-card .el-card__body) { padding: 12px !important; }
|
||||||
flex-wrap: nowrap !important;
|
|
||||||
overflow-x: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
padding-bottom: 6px;
|
|
||||||
margin-bottom: 8px !important;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
|
||||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
|
||||||
.toolbar :deep(.action-btn),
|
|
||||||
.toolbar :deep(.el-button) {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 12px !important;
|
|
||||||
padding: 0 10px !important;
|
|
||||||
height: 30px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* filter-form: 横向 (label 左 + input 右) */
|
/* form-item: 横向 label + content, flex-start 让 error message 占独立行
|
||||||
.filter-form {
|
不动 __label — Element Plus 自带 label-width=100px 自然对齐 */
|
||||||
|
:deep(.el-form-item) {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: column !important;
|
align-items: flex-start !important;
|
||||||
align-items: stretch !important;
|
|
||||||
gap: 10px !important;
|
|
||||||
}
|
|
||||||
.filter-form :deep(.el-form-item) {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
}
|
||||||
.filter-form :deep(.el-form-item__label) {
|
:deep(.el-form-item__content) {
|
||||||
float: none !important;
|
|
||||||
width: auto !important;
|
|
||||||
min-width: 80px !important;
|
|
||||||
text-align: right !important;
|
|
||||||
padding: 0 8px 0 0 !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
color: var(--el-text-color-regular) !important;
|
|
||||||
line-height: 32px !important;
|
|
||||||
height: 32px !important;
|
|
||||||
}
|
|
||||||
.filter-form :deep(.el-form-item__content) {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
line-height: 32px !important;
|
|
||||||
flex: 1 !important;
|
flex: 1 !important;
|
||||||
min-width: 0 !important;
|
min-width: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 强制所有控件全宽 */
|
/* 底部按钮: 等宽并排 */
|
||||||
.filter-form :deep(.el-select),
|
.form-actions {
|
||||||
.filter-form :deep(.el-input),
|
flex-wrap: wrap !important;
|
||||||
.filter-form :deep(.el-date-editor),
|
gap: 8px !important;
|
||||||
.filter-form :deep(.el-button),
|
margin-top: 12px !important;
|
||||||
.filter-form :deep(.el-cascader) {
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
margin-right: 0 !important;
|
|
||||||
display: block !important;
|
|
||||||
}
|
}
|
||||||
.filter-form :deep(.el-button + .el-button) {
|
.form-actions :deep(.el-button) {
|
||||||
margin-top: 8px !important;
|
flex: 1 1 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格字号收紧 */
|
|
||||||
:deep(.el-table) { font-size: 12px !important; }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ async function onSave() {
|
|||||||
await formRef.value.validate()
|
await formRef.value.validate()
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
await request({ url: '/system/user/profile', method: 'put', data: { nickName: form.nickName, phonenumber: form.phonenumber, sex: profile.value.sex } })
|
await request({ url: '/business/person/profile', method: 'put', data: { name: form.nickName, phone: form.phonenumber } })
|
||||||
if (form.newPassword) {
|
if (form.newPassword) {
|
||||||
await request({ url: '/system/user/profile/updatePwd', method: 'put', data: { oldPassword: form.oldPassword, newPassword: form.newPassword } })
|
await request({ url: '/system/user/profile/updatePwd', method: 'put', data: { oldPassword: form.oldPassword, newPassword: form.newPassword } })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<el-form-item label="会议名称"><el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" /></el-form-item>
|
<el-form-item label="会议名称"><el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" /></el-form-item>
|
||||||
<el-form-item label="专家姓名"><el-input v-model="q.expertName" clearable placeholder="输入专家姓名" style="width:160px" /></el-form-item>
|
<el-form-item label="专家姓名"><el-input v-model="q.expertName" clearable placeholder="输入专家姓名" style="width:160px" /></el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="load">查找</el-button>
|
<el-button type="primary" @click="load">查询</el-button>
|
||||||
<el-button @click="reset">重置</el-button>
|
<el-button @click="reset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="请输入会议名称" clearable style="width:160px" /></el-form-item>
|
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="请输入会议名称" clearable style="width:160px" /></el-form-item>
|
||||||
<el-form-item label="期数"><el-input-number v-model="q.periodNo" :min="1" placeholder="期数" style="width:140px" /></el-form-item>
|
<el-form-item label="期数"><el-input-number v-model="q.periodNo" :min="1" placeholder="期数" style="width:140px" /></el-form-item>
|
||||||
<el-form-item label="会议时间">
|
<el-form-item label="会议时间">
|
||||||
<el-date-picker v-model="q.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="开始时间" style="width:170px" />
|
<el-date-picker v-model="q.startTime" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" style="width:170px" />
|
||||||
<span class="date-sep">至</span>
|
<span class="date-sep">至</span>
|
||||||
<el-date-picker v-model="q.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="结束时间" style="width:170px" />
|
<el-date-picker v-model="q.endTime" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" style="width:170px" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="项目形式">
|
<el-form-item label="项目形式">
|
||||||
<el-select v-model="q.projectForm" placeholder="请选择" clearable style="width:140px">
|
<el-select v-model="q.projectForm" placeholder="请选择" clearable style="width:140px">
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="请输入备注" clearable style="width:140px" /></el-form-item>
|
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="请输入备注" clearable style="width:140px" /></el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="load">查找</el-button>
|
<el-button type="primary" @click="load">查询</el-button>
|
||||||
<el-button @click="reset">重置</el-button>
|
<el-button @click="reset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -40,14 +40,20 @@
|
|||||||
<el-table-column prop="projectNo" label="项目编号" width="170" fixed />
|
<el-table-column prop="projectNo" label="项目编号" width="170" fixed />
|
||||||
<el-table-column prop="meetingId" label="会议ID" width="120" align="center" />
|
<el-table-column prop="meetingId" label="会议ID" width="120" align="center" />
|
||||||
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
||||||
<el-table-column prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link :underline="false" type="primary" @click="onView(row)">{{ row.meetingName }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="会议开始时间" width="160" align="center">
|
<el-table-column label="会议开始时间" width="160" align="center">
|
||||||
<template #default="{ row }">{{ fmtTime(row.startTime) }}</template>
|
<template #default="{ row }">{{ fmtTime(row.startTime) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="会议结束时间" width="160" align="center">
|
<el-table-column label="会议结束时间" width="160" align="center">
|
||||||
<template #default="{ row }">{{ fmtTime(row.endTime) }}</template>
|
<template #default="{ row }">{{ fmtTime(row.endTime) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="totalPeriods" label="总期数" width="80" align="center" />
|
<el-table-column label="总期数" width="80" align="center">
|
||||||
|
<template #default="{ row }">{{ row.assignedSessions ?? row.totalPeriods ?? '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="期数" width="80" align="center">
|
<el-table-column label="期数" width="80" align="center">
|
||||||
<template #default="{ row }">{{ row.periodNo ? '第 ' + row.periodNo + ' 期' : '-' }}</template>
|
<template #default="{ row }">{{ row.periodNo ? '第 ' + row.periodNo + ' 期' : '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -57,11 +63,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
<el-table-column label="操作" width="240" fixed="right" align="center">
|
||||||
<template #default="{ row }"><div class="table-actions">
|
<template #default="{ row }"><div class="table-actions">
|
||||||
<el-link :underline="false" type="primary" @click="onView(row)">查看</el-link>
|
<el-link :underline="false" type="primary" @click="onView(row)">查看</el-link>
|
||||||
<el-link :underline="false" type="primary" @click="onUpload(row)">编辑材料</el-link>
|
<el-link :underline="false" type="primary" @click="onEdit(row)">编辑</el-link>
|
||||||
<el-link :underline="false" type="primary" @click="onEdit(row)">修改</el-link>
|
<el-link :underline="false" type="primary" @click="onUpload(row)">上传材料</el-link>
|
||||||
|
<el-link :underline="false" type="primary" @click="onCopy(row)">复制</el-link>
|
||||||
</div></template>
|
</div></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</GrTable>
|
</GrTable>
|
||||||
@@ -101,7 +108,11 @@ function fmtTime(d) {
|
|||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const { data } = await bizList('meeting', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
const params = { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize }
|
||||||
|
// 时间范围含两端: 开始日 00:00:00 ~ 结束日 23:59:59
|
||||||
|
if (params.startTime) params.startTime = params.startTime + ' 00:00:00'
|
||||||
|
if (params.endTime) params.endTime = params.endTime + ' 23:59:59'
|
||||||
|
const { data } = await bizList('meeting', params)
|
||||||
rows.value = data?.rows || []
|
rows.value = data?.rows || []
|
||||||
page.total = data?.total || 0
|
page.total = data?.total || 0
|
||||||
} catch { rows.value = []; page.total = 0 }
|
} catch { rows.value = []; page.total = 0 }
|
||||||
@@ -135,6 +146,11 @@ function onEdit(row) {
|
|||||||
router.push({ name: 'executor-meetings-new', query: { meetingId: row.meetingId, projectId: row.projectId || '' } })
|
router.push({ name: 'executor-meetings-new', query: { meetingId: row.meetingId, projectId: row.projectId || '' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 复制会议: 跳 executor-meetings-new (MeetingNew.vue 公共页), mode=copy → 后端 POST /business/meeting
|
||||||
|
function onCopy(row) {
|
||||||
|
router.push({ name: 'executor-meetings-new', query: { meetingId: row.meetingId, projectId: row.projectId || '', mode: 'copy' } })
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => { readQueryFromRoute(); load() })
|
onMounted(() => { readQueryFromRoute(); load() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -294,82 +294,40 @@ onMounted(() => {
|
|||||||
/* ========================================
|
/* ========================================
|
||||||
移动端适配 (≤768px)
|
移动端适配 (≤768px)
|
||||||
- 卡片 padding 收窄
|
- 卡片 padding 收窄
|
||||||
- 工具栏横向滚动
|
- el-row 双列 → 单列
|
||||||
- filter-form: label 与输入框横向
|
- form-item label 左 + content/content 横向并排 (不压控件内部样式, 留 error 提示行)
|
||||||
- 表格字号收紧
|
- 保存/取消按钮左右并排等宽
|
||||||
======================================== */
|
======================================== */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* 卡片 padding */
|
|
||||||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||||||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||||||
|
:deep(.form-card .el-card__body) { padding: 16px 12px !important; }
|
||||||
|
|
||||||
/* 工具栏: 横向滚动 */
|
/* 双列 el-row → 单列 */
|
||||||
.toolbar {
|
.new-person :deep(.el-row) { display: block !important; }
|
||||||
flex-wrap: nowrap !important;
|
.new-person :deep(.el-col) {
|
||||||
overflow-x: auto;
|
width: 100% !important;
|
||||||
-webkit-overflow-scrolling: touch;
|
max-width: 100% !important;
|
||||||
padding-bottom: 6px;
|
flex: 0 0 100% !important;
|
||||||
margin-bottom: 8px !important;
|
display: block !important;
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
}
|
||||||
.toolbar::-webkit-scrollbar { height: 4px; }
|
/* form-item 横向: label 左 + content 右, flex-start 保留 error 提示行 */
|
||||||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
.new-person :deep(.el-form-item) {
|
||||||
.toolbar :deep(.action-btn),
|
|
||||||
.toolbar :deep(.el-button) {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 12px !important;
|
|
||||||
padding: 0 10px !important;
|
|
||||||
height: 30px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* filter-form: 横向 (label 左 + input 右) */
|
|
||||||
.filter-form {
|
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: column !important;
|
align-items: flex-start !important;
|
||||||
align-items: stretch !important;
|
|
||||||
gap: 10px !important;
|
|
||||||
}
|
|
||||||
.filter-form :deep(.el-form-item) {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 14px !important;
|
||||||
}
|
}
|
||||||
.filter-form :deep(.el-form-item__label) {
|
.new-person :deep(.el-form-item__content) {
|
||||||
float: none !important;
|
|
||||||
width: auto !important;
|
|
||||||
min-width: 80px !important;
|
|
||||||
text-align: right !important;
|
|
||||||
padding: 0 8px 0 0 !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
color: var(--el-text-color-regular) !important;
|
|
||||||
line-height: 32px !important;
|
|
||||||
height: 32px !important;
|
|
||||||
}
|
|
||||||
.filter-form :deep(.el-form-item__content) {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
line-height: 32px !important;
|
|
||||||
flex: 1 !important;
|
flex: 1 !important;
|
||||||
min-width: 0 !important;
|
min-width: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 强制所有控件全宽 */
|
/* 保存/取消按钮: 左右并排等宽 */
|
||||||
.filter-form :deep(.el-select),
|
.form-actions { margin-top: 12px !important; }
|
||||||
.filter-form :deep(.el-input),
|
.form-actions :deep(.el-button) {
|
||||||
.filter-form :deep(.el-date-editor),
|
flex: 1 1 0 !important;
|
||||||
.filter-form :deep(.el-button),
|
width: 0 !important;
|
||||||
.filter-form :deep(.el-cascader) {
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
margin-right: 0 !important;
|
|
||||||
display: block !important;
|
|
||||||
}
|
}
|
||||||
.filter-form :deep(.el-button + .el-button) {
|
|
||||||
margin-top: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表格字号收紧 */
|
|
||||||
:deep(.el-table) { font-size: 12px !important; }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -29,13 +29,14 @@
|
|||||||
<!-- 消息通知 (NoticeList 组件内部已含 SSE 实时刷新 + 详情 dialog + 全部已读) -->
|
<!-- 消息通知 (NoticeList 组件内部已含 SSE 实时刷新 + 详情 dialog + 全部已读) -->
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="section-title">消息通知<a class="more" @click.prevent="$router.push('/executor/messages')">更多 →</a></div>
|
<div class="section-title">消息通知<a class="more" @click.prevent="$router.push('/executor/messages')">更多 →</a></div>
|
||||||
<NoticeList :limit="50" />
|
<NoticeList :pageable="true" :show-header="false" />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
import { bizList } from '@/api/public'
|
import { bizList } from '@/api/public'
|
||||||
import NoticeList from '@/components/NoticeList.vue'
|
import NoticeList from '@/components/NoticeList.vue'
|
||||||
|
|
||||||
@@ -46,22 +47,21 @@ const stats = ref({
|
|||||||
|
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
try {
|
try {
|
||||||
// 项目总数量 (跟 manager 一样调普通 list, 后续若需要按 executor 隔离再换 executorList 接口)
|
// 项目统计走 executorList: 后端按当前账号隔离 (MAIN=本执行单位全部, SUB=自己负责的项目)
|
||||||
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
|
const projectCount = async (extra = {}) => {
|
||||||
stats.value.totalProjects = ps.total || ps.data?.total || 0
|
const { data } = await request.get('/business/project/executorList', { params: { pageNum: 1, pageSize: 1, ...extra } })
|
||||||
// 已结题项目
|
return data?.total || 0
|
||||||
const cp = await bizList('project', { pageNum: 1, pageSize: 1, isFinished: '1' })
|
}
|
||||||
stats.value.completedProjects = cp.total || cp.data?.total || 0
|
stats.value.totalProjects = await projectCount()
|
||||||
// 会议总数量
|
stats.value.settledProjects = await projectCount({ isSettled: 'Y' })
|
||||||
|
stats.value.completedProjects = await projectCount({ isFinished: '1' })
|
||||||
|
// 会议统计走 /business/meeting/list (后端已按 roleType MAIN/SUB 隔离)
|
||||||
const ms = await bizList('meeting', { pageNum: 1, pageSize: 1 })
|
const ms = await bizList('meeting', { pageNum: 1, pageSize: 1 })
|
||||||
const meetingTotal = ms.total || ms.data?.total || 0
|
const meetingTotal = ms.total || ms.data?.total || 0
|
||||||
// 已执行会议 = currentStage='RUNNING' (阶段已过开始时间, 执行方未提交)
|
// 已执行会议 = currentStage='RUNNING' (阶段已过开始时间, 执行方未提交)
|
||||||
const em = await bizList('meeting', { pageNum: 1, pageSize: 1, currentStage: 'RUNNING' })
|
const em = await bizList('meeting', { pageNum: 1, pageSize: 1, currentStage: 'RUNNING' })
|
||||||
stats.value.executedMeetings = em.total || em.data?.total || 0
|
stats.value.executedMeetings = em.total || em.data?.total || 0
|
||||||
stats.value.pendingMeetings = Math.max(0, meetingTotal - stats.value.executedMeetings)
|
stats.value.pendingMeetings = Math.max(0, meetingTotal - stats.value.executedMeetings)
|
||||||
// 已结算项目
|
|
||||||
const sp = await bizList('project', { pageNum: 1, pageSize: 1, isSettled: 'Y' })
|
|
||||||
stats.value.settledProjects = sp.total || sp.data?.total || 0
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('loadStats failed', e)
|
console.warn('loadStats failed', e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<el-form-item label="部门"><el-input v-model="q.department" placeholder="部门" clearable style="width:140px" /></el-form-item>
|
<el-form-item label="部门"><el-input v-model="q.department" placeholder="部门" clearable style="width:140px" /></el-form-item>
|
||||||
<el-form-item label="职务"><el-input v-model="q.position" placeholder="职务" clearable style="width:140px" /></el-form-item>
|
<el-form-item label="职务"><el-input v-model="q.position" placeholder="职务" clearable style="width:140px" /></el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="loadList">查找</el-button>
|
<el-button type="primary" @click="loadList">查询</el-button>
|
||||||
<el-button @click="reset">重置</el-button>
|
<el-button @click="reset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -43,17 +43,27 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="300" fixed="right">
|
<el-table-column label="操作" width="130" fixed="right">
|
||||||
<template #default="{ row }"><div class="table-actions">
|
<template #default="{ row }"><div class="table-actions">
|
||||||
<el-link :underline="false" size="small" type="primary" @click="onView(row)">查看</el-link>
|
<el-link :underline="false" size="small" type="primary" @click="onView(row)">查看</el-link>
|
||||||
<el-link :underline="false" size="small" type="primary" @click="goEdit(row)">编辑</el-link>
|
<!-- 次要操作折叠到"更多"下拉, 避免操作列过宽 -->
|
||||||
<!-- 重置密码: 重置为默认 123456 (与新建人员默认密码一致) -->
|
<el-dropdown trigger="hover" @command="(cmd) => onMoreAction(cmd, row)">
|
||||||
<el-link :underline="false" v-if="row.userId" size="small" type="primary" @click="onResetPwd(row)">重置密码</el-link>
|
<el-link :underline="false" size="small" type="primary" class="op-dropdown">
|
||||||
<!-- 本人不显示禁用/恢复按钮 (跟 sponsor 一样, 避免主账号把自己禁用) -->
|
更多<el-icon class="op-caret"><ArrowDown /></el-icon>
|
||||||
<template v-if="row.userId !== store.user?.userId">
|
</el-link>
|
||||||
<el-link :underline="false" v-if="row.status === '0'" size="small" type="danger" @click="onToggleStatus(row, '禁用')">禁用</el-link>
|
<template #dropdown>
|
||||||
<el-link :underline="false" v-else size="small" type="success" @click="onToggleStatus(row, '恢复')">恢复</el-link>
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="edit">编辑</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="row.userId" command="resetPwd">重置密码</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="row.userId !== store.user?.userId && row.status === '0'" command="disable" divided>
|
||||||
|
<span style="color:#DC2626">禁用</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="row.userId !== store.user?.userId && row.status !== '0'" command="enable" divided>
|
||||||
|
<span style="color:#16A34A">恢复</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
</div></template>
|
</div></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</GrTable>
|
</GrTable>
|
||||||
@@ -115,6 +125,7 @@ import { bizUpdate, resetPersonPassword } from '@/api/public'
|
|||||||
import { listExecutorPerson } from '@/api/business/person'
|
import { listExecutorPerson } from '@/api/business/person'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { ArrowDown } from '@element-plus/icons-vue'
|
||||||
import { accountTypeRoleLabel, accountTypeRoleTagType } from '@/utils/roleMap'
|
import { accountTypeRoleLabel, accountTypeRoleTagType } from '@/utils/roleMap'
|
||||||
import GrTable from '@/components/GrTable.vue'
|
import GrTable from '@/components/GrTable.vue'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
@@ -156,6 +167,14 @@ function goEdit(row) { router.push(`/executor/people/edit/${row.personId}`) }
|
|||||||
|
|
||||||
function onView(row) { router.push(`/executor/people/detail/${row.personId}`) }
|
function onView(row) { router.push(`/executor/people/detail/${row.personId}`) }
|
||||||
|
|
||||||
|
// "更多"下拉分发: 把 cmd 映射到现有 handler (操作列折叠, 避免过宽)
|
||||||
|
function onMoreAction(cmd, row) {
|
||||||
|
if (cmd === 'edit') goEdit(row)
|
||||||
|
else if (cmd === 'resetPwd') onResetPwd(row)
|
||||||
|
else if (cmd === 'disable') onToggleStatus(row, '禁用')
|
||||||
|
else if (cmd === 'enable') onToggleStatus(row, '恢复')
|
||||||
|
}
|
||||||
|
|
||||||
async function onResetPwd(row) {
|
async function onResetPwd(row) {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user